Skip to content

feat: add leaderboard sort toggle with SSR-safe preference persistence - #142

Merged
junhoyeo merged 7 commits into
mainfrom
feature/leaderboard-sort-toggle
Jan 30, 2026
Merged

feat: add leaderboard sort toggle with SSR-safe preference persistence#142
junhoyeo merged 7 commits into
mainfrom
feature/leaderboard-sort-toggle

Conversation

@junhoyeo

@junhoyeo junhoyeo commented Jan 30, 2026

Copy link
Copy Markdown
Owner

Summary

Add a toggle to sort the leaderboard by Tokens or Cost, with server-side sorting for correct global rankings and SSR-safe preference persistence via Suspense streaming.

Features

  • Toggle UI: Blue switch component to choose between Tokens/Cost sorting
  • Server-side sorting: Correct global rankings across paginated results
  • Preference persistence: Saves to localStorage + syncs to cookie
  • No flash on reload: Suspense streams personalized content after static shell

Architecture

┌─────────────────────────────────────┐
│         Static Shell (instant)      │
│  ┌─────────────────────────────┐    │
│  │      Navigation             │    │
│  │      BlackholeHero          │    │
│  └─────────────────────────────┘    │
│  ┌─────────────────────────────┐    │
│  │   <Suspense fallback>       │    │
│  │   ┌───────────────────┐     │    │
│  │   │ LeaderboardSkeleton│    │    │
│  │   └───────────────────┘     │    │
│  │         ↓ streams in        │    │
│  │   ┌───────────────────┐     │    │
│  │   │ LeaderboardClient │     │    │  ← reads cookie, fetches with sortBy
│  │   └───────────────────┘     │    │
│  └─────────────────────────────┘    │
│  ┌─────────────────────────────┐    │
│  │      Footer                 │    │
│  └─────────────────────────────┘    │
└─────────────────────────────────────┘

Changes

UI (LeaderboardClient.tsx)

  • New Switch component with Tokens/Cost labels
  • Memoized LeaderboardRow for smooth toggling
  • Replaced all inline style={{}} with styled-components
  • Uses initialSortBy prop to prevent hydration mismatch

API (/api/leaderboard, /api/leaderboard/user/[username])

  • Added sortBy query parameter (tokens | cost)
  • Dynamic ORDER BY clause in getLeaderboard.ts
  • User rank calculated based on selected sort metric

SSR (page.tsx, useSettings.ts, constants.ts)

  • Wrapped leaderboard in <Suspense> for streaming
  • Cookie reading inside Suspense boundary (preserves static shell)
  • Client syncs localStorage to cookie on preference change
  • Shared validation in constants.ts for server/client

Commits

  1. b43eb9f - feat: add leaderboard sort toggle UI
  2. e8163d5 - perf: memoize LeaderboardRow for faster toggle
  3. f4c8015 - feat(api): add server-side sortBy param
  4. 7f627ec - fix: add type="button", validate localStorage
  5. 6ab1212 - refactor: replace inline styles with styled-components
  6. f21be27 - fix(ssr): use cookie for sortBy preference
  7. 7c1d24e - refactor(ssr): use Suspense for personalized content

Testing

  • Toggle switches between tokens/cost sorting instantly
  • Rankings are globally correct
  • Preference persists across page navigation
  • Static shell loads instantly, leaderboard streams in
  • No flash on hard reload

- Add blue Switch toggle component with smooth animations
- Extend useSettings hook with leaderboardSortBy preference (localStorage)
- Add client-side sorting in LeaderboardClient with rank recalculation
- Fix hydration error by deferring localStorage read to useEffect
- Extract LeaderboardRow as memo() component to prevent re-renders
- Use data-current-user attribute instead of transient prop
- Use data-rank attribute with CSS selectors instead of inline styles
- Add useCallback for stable row click handler
- Memoize formatted values inside row component
- Add sortBy query param to /api/leaderboard (tokens | cost)
- Add sortBy to /api/leaderboard/user/[username] for correct rank calculation
- Update getLeaderboard.ts with dynamic ORDER BY clause
- Remove client-side sorting from LeaderboardClient (server handles it)
- User rank card now reflects correct rank based on sort preference
@vercel

vercel Bot commented Jan 30, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
tokscale Ready Ready Preview, Comment Jan 30, 2026 0:08am

Request Review

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

2 issues found across 8 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/frontend/src/components/Switch.tsx">

<violation number="1" location="packages/frontend/src/components/Switch.tsx:67">
P2: Set an explicit button type to avoid accidental form submission when this switch is placed inside a form.</violation>
</file>

<file name="packages/frontend/src/lib/useSettings.ts">

<violation number="1" location="packages/frontend/src/lib/useSettings.ts:30">
P2: Validate `leaderboardSortBy` from localStorage before accepting it; otherwise an arbitrary stored value can flow into settings and break sort logic.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread packages/frontend/src/components/Switch.tsx
Comment thread packages/frontend/src/lib/useSettings.ts Outdated
- Add type="button" to Switch to prevent accidental form submission
- Validate leaderboardSortBy from localStorage before accepting it
- Move all color/bg/border styles into styled-components definitions
- Add StatValuePrimary, SubmitCount, RetryButton components
- Removes object allocations on every render for better performance
- Net reduction: 50 lines of code
- Create shared constants.ts for server/client sortBy validation
- Sync sortBy preference to cookie when user changes it
- Read cookie in page.tsx for SSR with correct sort order
- Pass initialSortBy to LeaderboardClient to prevent hydration mismatch

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/frontend/src/app/(main)/page.tsx">

<violation number="1" location="packages/frontend/src/app/(main)/page.tsx:13">
P2: Using `cookies()` opts this page into dynamic rendering, making `export const revalidate = 60` ineffective. The page will now render on every request instead of being cached.

If dynamic rendering is intentional, remove the misleading `revalidate` export. If caching is needed, consider reading the sort preference client-side only or using middleware to set a variant header.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread packages/frontend/src/app/(main)/page.tsx Outdated
@junhoyeo junhoyeo changed the title feat: add leaderboard sort toggle (tokens/cost) feat: add leaderboard sort toggle with SSR-safe preference persistence Jan 30, 2026
- Wrap leaderboard in Suspense boundary for streaming
- Move cookie reading inside Suspense (preserves static shell)
- Navigation, Hero, Footer render instantly (static)
- Leaderboard streams in with user's sort preference
- No flash on page load with correct sortBy
@junhoyeo
junhoyeo merged commit e412f50 into main Jan 30, 2026
5 checks passed
@junhoyeo
junhoyeo deleted the feature/leaderboard-sort-toggle branch January 30, 2026 12:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant