Skip to content

feat: /deputes and /votes skeletons using ContentSkeleton (MON-215) - #302

Merged
Walid-peach merged 2 commits into
masterfrom
feature/mon-215-deputes-and-votes-skeletons-using-contentskeleton-mon-207
Aug 3, 2026
Merged

feat: /deputes and /votes skeletons using ContentSkeleton (MON-215)#302
Walid-peach merged 2 commits into
masterfrom
feature/mon-215-deputes-and-votes-skeletons-using-contentskeleton-mon-207

Conversation

@Walid-peach

@Walid-peach Walid-peach commented Aug 3, 2026

Copy link
Copy Markdown
Owner

What

Wires MON-214's ContentSkeleton/AsyncStatus primitives into /deputes and /votes: route-level loading.tsx skeletons matching each page's real row grid, plus a timing-policy-gated skeleton for /votes' client-side filter/pagination refetches.

Why

Neither page had a layout-matched loading treatment. /votes showed a bare "Chargement…" string on every filter/pagination change (no timing discipline, so even instant responses could flash it), and both pages fell back to the generic root loading.tsx ("Chargement…" text) during initial navigation, with no relationship to the final card/row geometry.

Re-reading the actual code surfaced a scope correction worth calling out: DeputiesClient.tsx has no client-side loading state at all. The deputies list is filtered client-side (useMemo over initial.items, already fetched server-side) - there's no useSWR/fetch call the client ever awaits after the page mounts, unlike /votes. The issue's scope bullet ("Replace the bare 'Chargement…' text ... DeputiesClient.tsx") assumed a loading state there that doesn't exist in the current code. The one loading moment /deputes actually has is the route-level Suspense fallback while the server awaits fetchAllDeputies() - so that's where its skeleton lives.

Changes

  • frontend/src/app/deputes/DeputyRowSkeleton.tsx / frontend/src/app/votes/VoteRowSkeleton.tsx - row skeletons mirroring each page's real grid (avatar/name/department + group badge + arrow for deputies; date/title+subtitle/theme badge/result badge+meter+arrow for votes), built from MON-214's SkeletonBlock.
  • frontend/src/app/deputes/loading.tsx / frontend/src/app/votes/loading.tsx - new route-level skeletons (hero shell + ContentSkeleton-wrapped row list) that override the generic root loading.tsx fallback for these two routes during initial navigation.
  • frontend/src/app/votes/VotesClient.tsx - the isLoading branch (previously a bare "Chargement…" string) now runs through MON-214's useLoadingPhase: 'none' shows nothing, 'inline' (200ms-1.5s) shows a compact AsyncStatus, 'content' (past 1.5s) shows the full VoteRowSkeleton list. VoteRowSkeleton is shared with votes/loading.tsx so both stay in sync with the real layout.

Testing

  • npx jest VotesClient VotesLoading DeputiesLoading - 14 tests: route-skeleton aria-busy/status role/sr-only label/decorative rows for both new loading.tsx files, plus three new VotesClient tests exercising the actual timing gate with fake timers (no loader under 200ms, inline status between 200ms-1.5s, full skeleton past 1.5s). Existing VotesClient/DeputiesClient tests still pass unmodified in behavior.
  • npm test - full suite, 222/222 passing (one run hit an unrelated Jest worker SIGSEGV infra flake; a retry with --runInBand passed clean).
  • npm run lint / npm run build - clean.
  • Manual verification against the live production API (frontend/.env.local pointed at NEXT_PUBLIC_API_URL=https://monelu-production.up.railway.app, the documented default) via a scripted Playwright check: confirmed role="status" + aria-busy="true" + non-zero skeleton blocks are present immediately after navigating to both /votes and /deputes, and that both settle to zero skeleton blocks with real rows rendered. Screenshot of the /votes skeleton mid-navigation confirms the row grid lines up with the settled page (image attached to the Linear comment / shared with the requester).

Risks / Notes

  • No DB/schema/deploy-config changes.
  • DeputiesClient.tsx's render logic is intentionally untouched - see the scope-correction note above (it now exports its PAGE_SIZE constant so deputes/loading.tsx can match it, but nothing about its own behavior changed). If a client-side loading state is ever added there (e.g. a future server-backed search), it should reuse DeputyRowSkeleton + useLoadingPhase the same way VotesClient.tsx does here.
  • Review caught that votes/loading.tsx's skeleton row count (originally a hardcoded 10) didn't match VotesClient's real PAGE_SIZE (50, rendered with no client-side windowing) - fixed by having both loading.tsx files import PAGE_SIZE from their respective client component instead of a separate placeholder constant, plus a test on each asserting the row counts stay equal.

Breaking Changes

None.

Route-level loading.tsx skeletons for /deputes and /votes, matching each
page's real row grid (avatar/name/department, date/title/theme/result),
replacing the generic root loading.tsx fallback for these two routes.

/deputes has no client-side loading state to gate - the list is filtered
client-side over data already fetched server-side - so its skeleton only
applies to the route-level initial-navigation case. /votes does have a
client-side refetch (useSWR on filter/pagination changes): its bare
"Chargement..." text is replaced with the same VoteRowSkeleton, gated by
MON-214's useLoadingPhase so fast responses never flash a loader and
200ms-1.5s shows a subtle inline AsyncStatus before the full skeleton.
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
mon-elu Ready Ready Preview Aug 3, 2026 9:46pm

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

🧪 dbt data-health check — ✅ passed

21:44:33  52 of 62 PASS source_not_null_raw_votes_vote_id ................................ [PASS in 0.86s]
21:44:33  56 of 62 START test source_unique_raw_deputies_deputy_id ....................... [RUN]
21:44:33  53 of 62 PASS source_not_null_raw_votes_voted_at ............................... [PASS in 0.86s]
21:44:33  57 of 62 START test source_unique_raw_votes_vote_id ............................ [RUN]
21:44:33  54 of 62 PASS source_relationships_raw_vote_positions_deputy_id__deputy_id__source_raw_deputies_  [PASS in 1.01s]
21:44:33  58 of 62 START test unique_mart_deputy_scorecard_deputy_id ..................... [RUN]
21:44:33  56 of 62 PASS source_unique_raw_deputies_deputy_id ............................. [PASS in 0.93s]
21:44:33  59 of 62 START test unique_mart_party_alignment_deputy_id ...................... [RUN]
21:44:34  55 of 62 PASS source_relationships_raw_vote_positions_vote_id__vote_id__source_raw_votes_  [PASS in 1.25s]
21:44:34  60 of 62 START test unique_mart_vote_summary_vote_id ........................... [RUN]
21:44:34  57 of 62 PASS source_unique_raw_votes_vote_id .................................. [PASS in 0.88s]
21:44:34  61 of 62 START test unique_stg_deputies_deputy_id .............................. [RUN]
21:44:34  58 of 62 PASS unique_mart_deputy_scorecard_deputy_id ........................... [PASS in 0.88s]
21:44:34  62 of 62 START test unique_stg_votes_vote_id ................................... [RUN]
21:44:34  59 of 62 PASS unique_mart_party_alignment_deputy_id ............................ [PASS in 0.87s]
21:44:35  60 of 62 PASS unique_mart_vote_summary_vote_id ................................. [PASS in 0.92s]
21:44:35  61 of 62 PASS unique_stg_deputies_deputy_id .................................... [PASS in 0.90s]
21:44:35  62 of 62 PASS unique_stg_votes_vote_id ......................................... [PASS in 0.86s]
21:44:36  
21:44:36  Finished running 62 data tests in 0 hours 0 minutes and 21.78 seconds (21.78s).
21:44:36  
21:44:36  Completed successfully
21:44:36  
21:44:36  Done. PASS=62 WARN=0 ERROR=0 SKIP=0 TOTAL=62

Validates prod data health. A failure means current prod data is stale/broken, not necessarily that this PR is wrong.

@Walid-peach Walid-peach left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attention Score

60/100 - NEEDS YOUR ATTENTION

  • Base 100
  • -30: 1 Must Fix finding
  • -10: Testing / Validation Gaps section is non-empty
    Reason to take a closer look: the route-level /votes skeleton under-represents the real initial page height by up to 5x, which produces exactly the visible layout jump this issue's acceptance criteria explicitly rules out.

Summary

This PR wires MON-214's ContentSkeleton/AsyncStatus primitives into /deputes and /votes: new route-level loading.tsx skeletons for initial navigation, plus a timing-policy-gated skeleton replacing /votes' bare "Chargement…" text for client-side filter/pagination refetches. The scope correction on DeputiesClient.tsx (no client-side loading state exists there, so nothing to gate beyond the route-level skeleton) is well-reasoned and clearly documented in both the code and the PR description. One row-count mismatch needs fixing before this meets its own "no material CLS" acceptance criterion.

Must Fix

  • frontend/src/app/votes/loading.tsx:9 sets ROW_COUNT = 10, but votes/page.tsx fetches api.votes.list({ limit: 50 }) and renders all of it with no client-side windowing (VotesClient.tsx maps over the full votes array). On a cache-miss/first navigation to /votes with no filters applied — the default, most common case, not an edge case — the skeleton shows 10 rows, then the real page mounts with up to 50, growing the list section roughly 5x taller. That's a large, visible layout jump on exactly the surface this PR's own acceptance criterion ("Loading and final content transitions introduce no material cumulative layout shift") targets. frontend/src/app/deputes/loading.tsx doesn't have this problem — its ROW_COUNT = 10 already matches DeputiesClient.tsx's real PAGE_SIZE = 10. Suggest bumping votes/loading.tsx's ROW_COUNT to 50 to match the real initial render (the in-page VotesClient.tsx refetch skeleton's smaller SKELETON_ROW_COUNT = 8 is a different, lower-risk situation — the hero/header are already on screen and stable during a refetch, so a partial row-count mismatch there is far less jarring than an entire page growing after first paint).

Should Fix

None.

Nice to Have

  • Route-level loading.tsx (both files) has no way to honor the "no loader up to ~200ms" tier of the timing policy — Next.js renders the Suspense fallback as soon as the boundary is hit, with no client-side timer to delay it the way useLoadingPhase does for VotesClient.tsx's in-page case. This isn't a regression (the previous generic root loading.tsx had the same zero-delay characteristic), and there's no obvious low-effort fix within the Next.js App Router's Suspense-based loading.tsx convention, but worth a one-line note in the PR description or a follow-up issue so it doesn't read as an oversight later.
  • frontend/src/app/votes/VotesClient.tsx's new AsyncStatus inline-phase branch wraps a single status in its own <div style={{ padding: '20px 0' }}> — consistent with the removed bare-text branch's own wrapping div, so no behavior change, just flagging that AsyncStatus's className prop is being used to add flex justify-center rather than the parent div, which works but reads slightly indirect on first glance.

Testing / Validation Gaps

The Must Fix above wasn't caught by the automated tests, since none of the three new VotesClient loading-phase tests or the VotesLoading/DeputiesLoading route-skeleton tests assert row count against the real page's data size — they only check aria-busy, the role="status"/sr-only label, and that skeleton blocks are aria-hidden. Worth adding an assertion (e.g. VotesLoading renders the same row count as VotesPage's PAGE_SIZE) so a future edit to either constant doesn't silently drift them apart again.

Documentation / Reviewer Notes

The PR description's manual-verification section (live production API, Playwright script confirming aria-busy/skeleton presence on navigation) is a genuinely strong signal beyond the unit tests — worth preserving as the pattern for future MON-207 sub-issues (MON-216, MON-217) rather than relying on unit tests alone for a visual/timing-sensitive feature like this.

Verdict

Needs changes before merge

@Walid-peach Walid-peach added the needs-attention PR review score below threshold - maintainer should look closely label Aug 3, 2026
/votes' loading.tsx skeleton showed 10 rows while VotesPage renders up to
PAGE_SIZE=50 with no client-side windowing - a cache-miss navigation would
grow the list ~5x once real data landed, a large visible layout jump on
the exact surface this issue's "no material CLS" criterion targets.

Both loading.tsx files now import PAGE_SIZE from their client component
(VotesClient/DeputiesClient, both now exporting it) instead of a separate
placeholder constant, so the two can't drift apart again. Added a test on
each asserting the skeleton row count equals PAGE_SIZE.
@Walid-peach Walid-peach added safe-to-skim PR review score above threshold - agent review likely sufficient and removed needs-attention PR review score below threshold - maintainer should look closely labels Aug 3, 2026
@Walid-peach

Copy link
Copy Markdown
Owner Author

Applied the Must Fix from the review: votes/loading.tsx's ROW_COUNT now equals VotesClient's real PAGE_SIZE (50, was a hardcoded 10) instead of an arbitrary placeholder, closing the CLS gap on a cache-miss /votes navigation. Also applied the row-count parity fix to deputes/loading.tsx for consistency (it already matched by coincidence at 10, now it's derived from DeputiesClient's exported PAGE_SIZE so it can't silently drift). Added a test on each route-skeleton asserting row count === PAGE_SIZE, addressing the Testing/Validation Gap the review called out.

CI green again after the fix (lint, tests, build).

@Walid-peach
Walid-peach merged commit 215910b into master Aug 3, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

safe-to-skim PR review score above threshold - agent review likely sufficient

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant