diff --git a/backend/src/routes/payments.js b/backend/src/routes/payments.js index 22a958a..662614d 100644 --- a/backend/src/routes/payments.js +++ b/backend/src/routes/payments.js @@ -62,9 +62,12 @@ function applyPaymentFilters(query, req) { } if (typeof search === "string" && search.trim().length > 0) { const term = search.trim().replaceAll(",", "\\,"); - query = query.or( - `id.ilike.%${term}%,description.ilike.%${term}%,recipient.ilike.%${term}%` - ); + let orQuery = `id.ilike.%${term}%,description.ilike.%${term}%,recipient.ilike.%${term}%`; + const numTerm = Number(term); + if (!isNaN(numTerm)) { + orQuery += `,amount.eq.${numTerm}`; + } + query = query.or(orQuery); } return query; } diff --git a/frontend/package.json b/frontend/package.json index 3b79383..59db5e0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,7 @@ "@walletconnect/types": "^2.23.8", "boring-avatars": "^2.0.4", "canvas-confetti": "^1.9.4", + "confetti": "^3.0.4", "event-source-polyfill": "^1.0.31", "framer-motion": "^12.38.0", "marked": "^17.0.5", diff --git a/frontend/src/components/RecentPayments.tsx b/frontend/src/components/RecentPayments.tsx index 525ec29..6c9b812 100644 --- a/frontend/src/components/RecentPayments.tsx +++ b/frontend/src/components/RecentPayments.tsx @@ -37,12 +37,13 @@ interface FilterState { asset: string; dateFrom: string; dateTo: string; + page: number; + limit: number; } type SortColumn = "status" | "amount" | "recipient" | "created_at"; type SortDirection = "asc" | "desc"; -const LIMIT = 100; const STATUS_OPTIONS = ["all", "pending", "confirmed", "failed", "refunded"] as const; const ASSET_OPTIONS = ["all", "XLM", "USDC"] as const; const DEFAULT_FILTERS: FilterState = { @@ -51,6 +52,8 @@ const DEFAULT_FILTERS: FilterState = { asset: "all", dateFrom: "", dateTo: "", + page: 1, + limit: 10, }; const DEFAULT_SORT_COLUMN: SortColumn = "created_at"; const DEFAULT_SORT_DIRECTION: SortDirection = "desc"; @@ -63,12 +66,17 @@ function toStatusLabel( } function filtersFromSearchParams(searchParams: URLSearchParams): FilterState { + const pageStr = searchParams.get("page"); + const limitStr = searchParams.get("limit"); + return { search: searchParams.get("search") ?? "", status: searchParams.get("status") ?? "all", asset: searchParams.get("asset") ?? "all", dateFrom: searchParams.get("date_from") ?? "", dateTo: searchParams.get("date_to") ?? "", + page: pageStr ? parseInt(pageStr, 10) : 1, + limit: limitStr ? parseInt(limitStr, 10) : 10, }; } @@ -109,6 +117,8 @@ function buildSearchParams( if (filters.asset !== "all") params.set("asset", filters.asset); if (filters.dateFrom) params.set("date_from", filters.dateFrom); if (filters.dateTo) params.set("date_to", filters.dateTo); + if (filters.page > 1) params.set("page", filters.page.toString()); + if (filters.limit !== 10) params.set("limit", filters.limit.toString()); if (sortColumn !== DEFAULT_SORT_COLUMN) params.set("sortColumn", sortColumn); if (sortDirection !== DEFAULT_SORT_DIRECTION) { params.set("sortDirection", sortDirection); @@ -167,7 +177,6 @@ export default function RecentPayments({ const [payments, setPayments] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const page = 1; const [totalCount, setTotalCount] = useState(0); const [selectedPayment, setSelectedPayment] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); @@ -191,8 +200,13 @@ export default function RecentPayments({ ); const handleFilterChange = useCallback( - (key: keyof FilterState, value: string) => { - updateFilters({ ...filters, [key]: value }); + (key: keyof FilterState, value: string | number) => { + const isResetAction = key !== "page"; + updateFilters({ + ...filters, + [key]: value, + ...(isResetAction ? { page: 1 } : {}) + }); }, [filters, updateFilters], ); @@ -202,6 +216,7 @@ export default function RecentPayments({ updateFilters({ ...filters, [key]: key === "status" || key === "asset" ? "all" : "", + page: 1, }); }, [filters, updateFilters], @@ -268,8 +283,6 @@ export default function RecentPayments({ const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:4000"; const params = buildSearchParams(filters, sortColumn, sortDirection); - params.set("page", page.toString()); - params.set("limit", LIMIT.toString()); const response = await fetch(`${apiUrl}/api/payments?${params.toString()}`, { headers: { @@ -1017,6 +1030,49 @@ export default function RecentPayments({ + {/* Pagination Controls */} + {totalCount > 0 && ( +
+
+ + +
+ +
+ + + Page {filters.page} of {Math.max(1, Math.ceil(totalCount / filters.limit))} + + +
+
+ )} + ; frontmatter: Record; }' but required in type 'MDXRemoteProps'.","relatedInformation":[{"file":"./node_modules/next-mdx-remote/dist/rsc.d.ts","start":303,"length":6,"messageText":"'source' is declared here.","category":3,"code":2728}],"canonicalHead":{"code":2322,"messageText":"Type '{ compiledSource: string; scope: Record; frontmatter: Record; }' is not assignable to type 'MDXRemoteProps'."}}]],[1788,[{"start":37,"length":8,"messageText":"Cannot find module 'vitest' or its corresponding type declarations.","category":1,"code":2307}]],[1795,[{"start":45,"length":25,"messageText":"Module '\"next-mdx-remote/rsc\"' has no exported member 'MDXRemoteSerializeOptions'.","category":1,"code":2305}]]],"affectedFilesPendingEmit":[1507,1607,1609,1603,1505,1618,1621,1622,1624,1773,1636,1637,1630,1776,1633,1777,1780,1783,1786,1611,434,1157,1464,1465,1787,1628,1504,1472,1788,1275,1506,1606,1599,1790,1789,1608,1635,1156,1617,1602,1631,1276,1474,1781,1627,1594,1471,1629,1615,1595,1503,1791,1620,1782,1470,1632,1271,1792,1274,1614,1623,1598,1774,1775,1784,1785,1600,1597,1589,1605,1473,1793,1601,1634,1772,1616,1794,1347,1795,1502,1778,1796,1797,1596,1612,1798,1610,1498,1463,1348,1277,1462,1619],"version":"5.9.3"} \ No newline at end of file