Overview
src/app/airdrops/page.tsx declares its pagination state with the setter discarded:
const [page] = useState(1);
const { data, isLoading, isError, error, isFetching, refetch } = useQuery({
queryKey: ["airdrops", page],
queryFn: () => listAirdrops(page, 20),
...backendQueryRetry,
});
page can never become anything other than 1 — there is no setPage anywhere in the component, and no Prev/Next/page-number UI is rendered at all (contrast with src/app/history/page.tsx and src/app/leaderboard/page.tsx, both of which render full pagination controls over their own paginated data). The backend API this page calls is fully paginated — listAirdrops(page, limit) returns { airdrops, pagination: { page, limit, total, total_pages } } (src/lib/backend.ts:102-111) — and the page even displays the total count: {data.pagination.total} airdrop{...} total (airdrops/page.tsx:117-121). The result: any deployment with more than 20 airdrops will show a "total" number that doesn't match what's on screen, with zero way for a user to reach airdrop #21 and beyond — not a slow or awkward path, a structurally impossible one, since the only state that could ever drive a different page value doesn't exist.
Requirements
- Restore real pagination state (
useState with its setter) and wire Prev/Next (and/or numbered) controls, matching the pattern already established in HistoryPage/LeaderboardPage.
- Ensure the
queryKey correctly reflects the active page so navigating pages triggers a real refetch rather than reading stale cached data for a different page.
- Surface
data.pagination.total_pages so pagination controls know when to stop, exactly as HistoryPage's totalPages does.
Acceptance Criteria
Additional Notes
More precise references
src/app/airdrops/page.tsx:31 — confirmed the exact const [page] = useState(1); with no setter destructured, and confirmed via full-file read that page is referenced only at line 34 (queryKey) and line 35 (listAirdrops(page, 20)) — never reassigned anywhere in the component.
src/lib/backend.ts:102-111 (listAirdrops) and :102-107 (Pagination type with total_pages) — confirmed the backend contract fully supports paging.
src/app/history/page.tsx:216-263 — confirmed the existing, working pagination-controls pattern (Prev/numbered-buttons/Next, isDisabled at the boundaries) that this page should mirror.
src/app/airdrops/page.tsx:117-121 — confirmed the exact wording of the total-count display that becomes misleading once there are more than 20 airdrops.
Additional edge cases
src/lib/backend.ts:117-129 defines listAirdropRecipients(id, page, limit) and getAirdrop(id) — confirmed via grep that neither is called from any component; there is no per-airdrop detail page/route (src/app/airdrops/[id]/page.tsx does not exist) despite the backend and types fully modeling per-airdrop recipient lists (Recipient { address, amount, claimed? }). This is worth scoping alongside the pagination fix, or at minimum flagging as a follow-up, since a user currently has no way to check their own claim status for any airdrop at all — the "Airdrops" feature is list-only.
- Since
page is hardcoded to 1, the query key ["airdrops", page] is also permanently ["airdrops", 1] — even a manual code change elsewhere that tried to test a different page would collide on cache with page 1's data unless the key changes too, so any fix needs to verify the query key genuinely varies with real navigation, not just that a page variable exists.
Implementation sketch
const [page, setPage] = useState(1);
const { data, ... } = useQuery({
queryKey: ["airdrops", page],
queryFn: () => listAirdrops(page, 20),
...backendQueryRetry,
});
const totalPages = data?.pagination.total_pages ?? 1;
// ... after the list render, mirror history/page.tsx's Prev/numbered/Next Flex block,
// using `page`/`setPage`/`totalPages` instead of `currentPage`/`setPage`/`totalPages`.
Test/reproduction plan
- Mock
listAirdrops to return pagination.total_pages: 3 on page 1; render AirdropsPage; assert Next is present and enabled, Prev is disabled.
- Click Next; assert
listAirdrops was called a second time with page = 2, and the displayed list updates to the mocked page-2 data.
- With
total_pages: 1, assert no pagination controls render at all (matching the existing totalPages > 1 gating pattern used elsewhere).
Cross-references
- None of the existing 75 issues in this repo mention the Airdrops page; this is a previously unreported, self-contained gap.
Overview
src/app/airdrops/page.tsxdeclares its pagination state with the setter discarded:pagecan never become anything other than1— there is nosetPageanywhere in the component, and no Prev/Next/page-number UI is rendered at all (contrast withsrc/app/history/page.tsxandsrc/app/leaderboard/page.tsx, both of which render full pagination controls over their own paginated data). The backend API this page calls is fully paginated —listAirdrops(page, limit)returns{ airdrops, pagination: { page, limit, total, total_pages } }(src/lib/backend.ts:102-111) — and the page even displays the total count:{data.pagination.total} airdrop{...} total(airdrops/page.tsx:117-121). The result: any deployment with more than 20 airdrops will show a "total" number that doesn't match what's on screen, with zero way for a user to reach airdrop #21 and beyond — not a slow or awkward path, a structurally impossible one, since the only state that could ever drive a differentpagevalue doesn't exist.Requirements
useStatewith its setter) and wire Prev/Next (and/or numbered) controls, matching the pattern already established inHistoryPage/LeaderboardPage.queryKeycorrectly reflects the active page so navigating pages triggers a real refetch rather than reading stale cached data for a different page.data.pagination.total_pagesso pagination controls know when to stop, exactly asHistoryPage'stotalPagesdoes.Acceptance Criteria
listAirdrops(page, 20)call with the updatedpagevalue (verifiable via the query key/mock call args in a test).pagination.total_pages > 1.Additional Notes
More precise references
src/app/airdrops/page.tsx:31— confirmed the exactconst [page] = useState(1);with no setter destructured, and confirmed via full-file read thatpageis referenced only at line 34 (queryKey) and line 35 (listAirdrops(page, 20)) — never reassigned anywhere in the component.src/lib/backend.ts:102-111(listAirdrops) and:102-107(Paginationtype withtotal_pages) — confirmed the backend contract fully supports paging.src/app/history/page.tsx:216-263— confirmed the existing, working pagination-controls pattern (Prev/numbered-buttons/Next,isDisabledat the boundaries) that this page should mirror.src/app/airdrops/page.tsx:117-121— confirmed the exact wording of the total-count display that becomes misleading once there are more than 20 airdrops.Additional edge cases
src/lib/backend.ts:117-129defineslistAirdropRecipients(id, page, limit)andgetAirdrop(id)— confirmed viagrepthat neither is called from any component; there is no per-airdrop detail page/route (src/app/airdrops/[id]/page.tsxdoes not exist) despite the backend and types fully modeling per-airdrop recipient lists (Recipient { address, amount, claimed? }). This is worth scoping alongside the pagination fix, or at minimum flagging as a follow-up, since a user currently has no way to check their own claim status for any airdrop at all — the "Airdrops" feature is list-only.pageis hardcoded to1, the query key["airdrops", page]is also permanently["airdrops", 1]— even a manual code change elsewhere that tried to test a different page would collide on cache with page 1's data unless the key changes too, so any fix needs to verify the query key genuinely varies with real navigation, not just that apagevariable exists.Implementation sketch
Test/reproduction plan
listAirdropsto returnpagination.total_pages: 3on page 1; renderAirdropsPage; assert Next is present and enabled, Prev is disabled.listAirdropswas called a second time withpage = 2, and the displayed list updates to the mocked page-2 data.total_pages: 1, assert no pagination controls render at all (matching the existingtotalPages > 1gating pattern used elsewhere).Cross-references