Skip to content

AirdropsPage's page state has no setter — pagination is structurally impossible, permanently hiding airdrops beyond the first 20 #131

Description

@prodbycorne

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

  • With more than 20 airdrops on the backend, a user can navigate to and view airdrop #21+ through visible Prev/Next (or numbered) controls.
  • The displayed "N airdrops total" count and the actual reachable set of airdrops are consistent (i.e. a user can, by paging through, eventually see all N).
  • Changing pages triggers a new listAirdrops(page, 20) call with the updated page value (verifiable via the query key/mock call args in a test).
  • A regression test asserts pagination controls are rendered and functional whenever pagination.total_pages > 1.

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.

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial CampaignCampaign: Official CampaignOfficial Campaign | FWC26Campaign: Official Campaign | FWC26Third CampaignCampaign: Third CampaignbugSomething isn't workingvery hardExtremely hard — deep expertise, careful design, and significant time required

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions