Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion frontend/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,15 @@
"confirmed": "Confirmed",
"failed": "Failed",
"refunded": "Refunded"
},
"pagination": {
"ariaLabel": "Payment history pagination",
"previous": "Previous page",
"next": "Next page",
"goToPage": "Go to page {page}",
"currentPage": "Current page, page {page}",
"pageLabel": "Page {page} of {totalPages}",
"range": "Showing {start}-{end} of {total}"
}
},
"walletSelector": {
Expand Down Expand Up @@ -453,4 +462,4 @@
"notificationLabel": "Notification: {message}",
"timestampLabel": "Timestamp: {timestamp}"
}
}
}
9 changes: 9 additions & 0 deletions frontend/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,15 @@
"confirmed": "Confirmado",
"failed": "Fallido",
"refunded": "Reembolsado"
},
"pagination": {
"ariaLabel": "Paginación del historial de pagos",
"previous": "Página anterior",
"next": "Página siguiente",
"goToPage": "Ir a la página {page}",
"currentPage": "Página actual, página {page}",
"pageLabel": "Página {page} de {totalPages}",
"range": "Mostrando {start}-{end} de {total}"
}
},
"walletSelector": {
Expand Down
9 changes: 9 additions & 0 deletions frontend/messages/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,15 @@
"confirmed": "Confirmado",
"failed": "Falhou",
"refunded": "Reembolsado"
},
"pagination": {
"ariaLabel": "Paginação do histórico de pagamentos",
"previous": "Página anterior",
"next": "Próxima página",
"goToPage": "Ir para a página {page}",
"currentPage": "Página atual, página {page}",
"pageLabel": "Página {page} de {totalPages}",
"range": "Mostrando {start}-{end} de {total}"
}
},
"walletSelector": {
Expand Down
44 changes: 13 additions & 31 deletions frontend/src/app/(authenticated)/payment-history/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useLocale, useTranslations } from "next-intl";
import Skeleton from "react-loading-skeleton";
import "react-loading-skeleton/dist/skeleton.css";
import TransactionHistoryPagination from "@/components/TransactionHistoryPagination";
import { localeToLanguageTag } from "@/i18n/config";
import { toast } from "sonner";
import {
Expand Down Expand Up @@ -43,6 +44,7 @@ interface Payment {
interface PaginatedResponse {
payments: Payment[];
total_count: number;
total_pages: number;
}

const LIMIT = 50;
Expand Down Expand Up @@ -115,6 +117,7 @@ export default function PaymentHistoryPage() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [totalCount, setTotalCount] = useState(0);
const [totalPages, setTotalPages] = useState(0);
const [selectedPayment, setSelectedPayment] = useState<string | null>(null);
const [hoveredPayment, setHoveredPayment] = useState<string | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
Expand Down Expand Up @@ -200,6 +203,7 @@ export default function PaymentHistoryPage() {
const data: PaginatedResponse = await response.json();
setPayments(data.payments ?? []);
setTotalCount(data.total_count ?? 0);
setTotalPages(data.total_pages ?? 0);
} catch (err: unknown) {
if (err instanceof Error && err.name === "AbortError") return;
setError(err instanceof Error ? err.message : t("loadFailed"));
Expand All @@ -217,9 +221,6 @@ export default function PaymentHistoryPage() {
setSelectedPayment(paymentId);
setIsSheetOpen(true);
};
const totalPages = Math.max(1, Math.ceil(totalCount / LIMIT));
const pageStart = totalCount === 0 ? 0 : (currentPage - 1) * LIMIT + 1;
const pageEnd = Math.min(currentPage * LIMIT, totalCount);

// ── Loading state ─────────────────────────────────────────────────────────────
if (loading) {
Expand Down Expand Up @@ -461,9 +462,7 @@ export default function PaymentHistoryPage() {
{/* Results count */}
<div className="flex items-center justify-between px-2">
<p className="text-xs text-[#6B6B6B] font-medium">
{totalPages > 1
? `Showing ${pageStart}-${pageEnd} of ${totalCount}`
: t("showingResults", { shown: payments.length, total: totalCount })}
{t("showingResults", { shown: payments.length, total: totalCount })}
</p>
</div>

Expand Down Expand Up @@ -534,31 +533,14 @@ export default function PaymentHistoryPage() {
</div>
)}

{totalPages > 1 && (
<div className="flex flex-col items-center justify-between gap-3 border-t border-[#F0F0F0] py-6 sm:flex-row">
<p className="text-[10px] font-bold uppercase tracking-widest text-[#A0A0A0]">
Page {currentPage} of {totalPages}
</p>
<nav className="flex items-center gap-2" aria-label="Transaction history pagination">
<button
type="button"
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage <= 1 || isFilterPending}
className="inline-flex min-h-10 items-center rounded-xl border border-[#E8E8E8] bg-white px-4 text-[10px] font-bold uppercase tracking-widest text-[#0A0A0A] transition-all hover:bg-[#F5F5F5] disabled:cursor-not-allowed disabled:opacity-40"
>
Previous
</button>
<button
type="button"
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage >= totalPages || isFilterPending}
className="inline-flex min-h-10 items-center rounded-xl bg-[#0A0A0A] px-4 text-[10px] font-bold uppercase tracking-widest text-white transition-all hover:bg-[#2A2A2A] disabled:cursor-not-allowed disabled:opacity-40"
>
Next
</button>
</nav>
</div>
)}
<TransactionHistoryPagination
page={currentPage}
totalPages={totalPages}
totalCount={totalCount}
limit={LIMIT}
onPageChange={handlePageChange}
disabled={loading || isFilterPending}
/>
</div>
</div>

Expand Down
113 changes: 113 additions & 0 deletions frontend/src/components/TransactionHistoryPagination.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import "@testing-library/jest-dom/vitest";
import { NextIntlClientProvider } from "next-intl";
import TransactionHistoryPagination from "./TransactionHistoryPagination";

const messages = {
recentPayments: {
pagination: {
ariaLabel: "Payment history pagination",
previous: "Previous page",
next: "Next page",
goToPage: "Go to page {page}",
currentPage: "Current page, page {page}",
pageLabel: "Page {page} of {totalPages}",
range: "Showing {start}-{end} of {total}",
},
},
};

function renderPagination(props: Partial<React.ComponentProps<typeof TransactionHistoryPagination>> = {}) {
const defaultProps = {
page: 1,
totalPages: 5,
totalCount: 220,
limit: 50,
onPageChange: vi.fn(),
};
const merged = { ...defaultProps, ...props };

render(
<NextIntlClientProvider locale="en" messages={messages}>
<TransactionHistoryPagination {...merged} />
</NextIntlClientProvider>,
);

return merged;
}

describe("TransactionHistoryPagination", () => {
it("renders nothing when there is only one page", () => {
const { container } = render(
<NextIntlClientProvider locale="en" messages={messages}>
<TransactionHistoryPagination page={1} totalPages={1} totalCount={10} limit={50} onPageChange={vi.fn()} />
</NextIntlClientProvider>,
);
expect(container).toBeEmptyDOMElement();
});

it("renders nothing when there are zero pages", () => {
const { container } = render(
<NextIntlClientProvider locale="en" messages={messages}>
<TransactionHistoryPagination page={1} totalPages={0} totalCount={0} limit={50} onPageChange={vi.fn()} />
</NextIntlClientProvider>,
);
expect(container).toBeEmptyDOMElement();
});

it("shows the correct result range", () => {
renderPagination({ page: 2, totalPages: 5, totalCount: 220, limit: 50 });
expect(screen.getByText("Showing 51-100 of 220")).toBeInTheDocument();
});

it("disables the previous button on the first page", () => {
renderPagination({ page: 1 });
expect(screen.getByRole("button", { name: "Previous page" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Next page" })).toBeEnabled();
});

it("disables the next button on the last page", () => {
renderPagination({ page: 5, totalPages: 5 });
expect(screen.getByRole("button", { name: "Next page" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Previous page" })).toBeEnabled();
});

it("calls onPageChange with the next page when Next is clicked", () => {
const { onPageChange } = renderPagination({ page: 2, totalPages: 5 });
fireEvent.click(screen.getByRole("button", { name: "Next page" }));
expect(onPageChange).toHaveBeenCalledWith(3);
});

it("calls onPageChange with the previous page when Previous is clicked", () => {
const { onPageChange } = renderPagination({ page: 2, totalPages: 5 });
fireEvent.click(screen.getByRole("button", { name: "Previous page" }));
expect(onPageChange).toHaveBeenCalledWith(1);
});

it("calls onPageChange with a specific page number when clicked", () => {
const { onPageChange } = renderPagination({ page: 1, totalPages: 5 });
fireEvent.click(screen.getByRole("button", { name: "Go to page 3" }));
expect(onPageChange).toHaveBeenCalledWith(3);
});

it("marks the current page with aria-current", () => {
renderPagination({ page: 3, totalPages: 5 });
const current = screen.getByRole("button", { name: "Current page, page 3" });
expect(current).toHaveAttribute("aria-current", "page");
});

it("collapses long page ranges with an ellipsis", () => {
renderPagination({ page: 5, totalPages: 12 });
expect(screen.getAllByText("…").length).toBeGreaterThan(0);
expect(screen.getByRole("button", { name: "Go to page 1" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Go to page 12" })).toBeInTheDocument();
});

it("disables all controls when disabled prop is set", () => {
renderPagination({ page: 2, totalPages: 5, disabled: true });
expect(screen.getByRole("button", { name: "Previous page" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Next page" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Go to page 1" })).toBeDisabled();
});
});
Loading
Loading