Skip to content

Night Shift: fix leaderboard type safety (6 as-any)#25

Closed
EtanHey wants to merge 1 commit intomasterfrom
nightshift/2026-03-02-1525
Closed

Night Shift: fix leaderboard type safety (6 as-any)#25
EtanHey wants to merge 1 commit intomasterfrom
nightshift/2026-03-02-1525

Conversation

@EtanHey
Copy link
Owner

@EtanHey EtanHey commented Mar 2, 2026

Automated improvement by Golems Night Shift.

fix leaderboard type safety (6 as-any)

Summary by CodeRabbit

Release Notes

  • New Features
    • Added pagination controls to leaderboard for browsing larger ranking lists.
    • Current user now highlighted and visible in leaderboard rankings, even outside top positions.
    • Enhanced leaderboard display with improved medal indicators and type-specific score formatting (days for streaks, points for progress).

Replace 6 `as any` casts with proper StreakEntry/ProgressEntry types.
Split rendering by selectedType so TypeScript can narrow correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Mar 2, 2026

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

Project Deployment Actions Updated (UTC)
songscript Building Building Preview, Comment Mar 2, 2026 2:10am

@qodo-code-review
Copy link

Review Summary by Qodo

Remove type safety issues in leaderboard components

🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Remove 6 as any type casts in leaderboard components
• Add explicit StreakEntry and ProgressEntry type definitions
• Split rendering logic by selectedType for proper type narrowing
• Extract reusable row rendering components to improve maintainability
Diagram
flowchart LR
  A["Leaderboard Components<br/>with as-any casts"] -->|"Define explicit types"| B["StreakEntry &<br/>ProgressEntry types"]
  A -->|"Split by selectedType"| C["Type-safe rendering<br/>per leaderboard type"]
  A -->|"Extract components"| D["MiniRow & renderRow<br/>helper functions"]
  B --> E["Type-safe code<br/>without casts"]
  C --> E
  D --> E
Loading

Grey Divider

File Changes

1. src/components/dashboard/LeaderboardMiniSection.tsx 🐞 Bug fix +112/-50

Type-safe mini leaderboard with extracted component

• Added explicit StreakEntry and ProgressEntry type definitions
• Extracted MiniRow component to eliminate inline JSX duplication
• Split leaderboard rendering into separate streakData.map() and progressData.map() blocks for
 type narrowing
• Removed 3 as any casts by using typed entries directly

src/components/dashboard/LeaderboardMiniSection.tsx


2. src/routes/_authed/leaderboard.tsx 🐞 Bug fix +117/-79

Type-safe full leaderboard with helper function

• Added explicit StreakEntry and ProgressEntry type definitions
• Extracted renderRow helper function to reduce code duplication
• Split leaderboard rendering into separate streakData.map() and progressData.map() blocks for
 proper type narrowing
• Removed 3 as any casts by accessing typed properties directly
• Improved code formatting and readability

src/routes/_authed/leaderboard.tsx


Grey Divider

Qodo Logo

@qodo-code-review
Copy link

qodo-code-review bot commented Mar 2, 2026

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (1) 📎 Requirement gaps (0)

Grey Divider


Action required

1. renderRow missing tests 📘 Rule violation ⛯ Reliability
Description
A new renderRow helper was introduced in the leaderboard route as part of the type-safety refactor
without adding any automated tests. This increases regression risk for ranking/flag/score rendering
across leaderboard modes.
Code

src/routes/_authed/leaderboard.tsx[R92-123]

+  const renderRow = (
+    rank: number,
+    displayName: string,
+    flag: string,
+    scoreText: string,
+    isUser: boolean,
+    medal: string | null,
+  ) => (
+    <div
+      key={`${displayName}-${rank}`}
+      className={`flex items-center gap-4 p-4 rounded-lg transition-colors ${
+        isUser ? "bg-primary/10 border border-primary/20" : "hover:bg-muted/50"
+      }`}
+    >
+      <div className="w-8 text-center font-mono text-sm text-muted-foreground">
+        {rank}
+      </div>
+      <div className="w-6 text-center">
+        {medal && <span className="text-lg">{medal}</span>}
+      </div>
+      <div className="flex items-center gap-3 flex-1">
+        <span className="text-lg">{flag}</span>
+        <span className="font-medium">
+          {displayName}
+          {isUser && <span className="ml-2 text-primary">⭐</span>}
+        </span>
+      </div>
+      <div className="text-right">
+        <div className="font-mono font-medium">{scoreText}</div>
+      </div>
+    </div>
+  );
Evidence
PR Compliance ID 2 requires tests for newly introduced helpers/utilities, and PR Compliance ID 4
requires regression tests for bug fixes when feasible. The PR adds renderRow and refactors the
mapping/rendering logic to use it, but no test was added to validate the corrected type-safe
rendering.

CLAUDE.md
CLAUDE.md
src/routes/_authed/leaderboard.tsx[92-123]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A new helper (`renderRow`) and a refactor of leaderboard row rendering were added without automated test coverage.

## Issue Context
This PR is a leaderboard type-safety bug fix. The compliance checklist requires tests for new helpers and regression tests for bug fixes when feasible.

## Fix Focus Areas
- src/routes/_authed/leaderboard.tsx[92-123]
- src/routes/_authed/leaderboard.tsx[213-235]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Period toggle ineffective 🐞 Bug ✓ Correctness
Description
The leaderboard page offers Weekly/Monthly/All-time and passes period into Convex queries, but the
backend leaderboard queries ignore period, so the UI filter is misleading and results won’t
change. Users will see “This Week/This Month” labels without any corresponding data change.
Code

src/routes/_authed/leaderboard.tsx[R46-60]

  const { data: streakData = [], isLoading: streakLoading } = useQuery(
-    convexQuery(api.leaderboard.getStreakLeaderboard, { 
-      limit, 
+    convexQuery(api.leaderboard.getStreakLeaderboard, {
+      limit,
      offset,
-      period: selectedPeriod 
-    })
+      period: selectedPeriod,
+    }),
  );

  const { data: progressData = [], isLoading: progressLoading } = useQuery(
-    convexQuery(api.leaderboard.getProgressLeaderboard, { 
-      limit, 
+    convexQuery(api.leaderboard.getProgressLeaderboard, {
+      limit,
      offset,
-      period: selectedPeriod 
-    })
+      period: selectedPeriod,
+    }),
  );
Evidence
Frontend passes period: selectedPeriod and renders the period toggle/label, but Convex query
handlers destructure only {limit, offset} and never read period, so the passed value cannot
affect results.

src/routes/_authed/leaderboard.tsx[45-68]
src/routes/_authed/leaderboard.tsx[153-187]
convex/leaderboard.ts[145-156]
convex/leaderboard.ts[228-235]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The leaderboard UI exposes Weekly/Monthly/All-time, but the backend queries ignore the `period` argument, so changing the period does not change the data.

### Issue Context
Frontend passes `period: selectedPeriod` into Convex queries and displays period labels, so users expect filtering.

### Fix Focus Areas
- src/routes/_authed/leaderboard.tsx[45-68]
- convex/leaderboard.ts[145-156]
- convex/leaderboard.ts[228-235]
- convex/leaderboard.ts[271-279]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. DisplayName check wrong 🐞 Bug ✓ Correctness
Description
hasDisplayName is computed as true when userInfo is undefined or null, which can incorrectly
render the leaderboard and hide the “Set display name” CTA (at least during loading/error states).
This happens because optional chaining yields undefined, and undefined !== null is true.
Code

src/components/dashboard/LeaderboardMiniSection.tsx[85]

  const hasDisplayName = userInfo?.displayName !== null;
-  const leaderboardData = selectedType === "streak" ? streakData : progressData;
Evidence
The UI’s check uses optional chaining and compares only against null, so undefined is treated as
“has display name”. The backend explicitly returns null for unauthenticated users, and React Query
commonly yields undefined before data arrives; in both cases the current check becomes true and
suppresses the CTA.

src/components/dashboard/LeaderboardMiniSection.tsx[80-86]
convex/leaderboard.ts[129-140]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`hasDisplayName` currently becomes `true` when `userInfo` is `undefined` (loading) or `null` (unauthenticated), because the check only compares to `null`. This can render the leaderboard (and hide the “Set display name” CTA) when we don’t yet know whether the user has a display name.

### Issue Context
Backend `getUserInfo` can return `null` when unauthenticated; React Query can return `undefined` before data resolves.

### Fix Focus Areas
- src/components/dashboard/LeaderboardMiniSection.tsx[80-90]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

4. Type duplication drift risk 🐞 Bug ⛯ Reliability
Description
StreakEntry/ProgressEntry are duplicated across files and applied as map-callback annotations;
this is correct today but can drift from backend return shapes without an obvious compile-time
signal. This is a maintainability/type-safety risk rather than an immediate runtime bug.
Code

src/components/dashboard/LeaderboardMiniSection.tsx[R134-163]

+            {selectedType === "streak"
+              ? streakData.map((user: StreakEntry) => {
+                  const isCurrentUser =
+                    hasDisplayName && userRank && user.rank === userRank.rank;
+                  const medal = getMedalIcon(user.rank);
+                  return (
+                    <MiniRow
+                      key={`${user.displayName}-${user.rank}`}
+                      rank={user.rank}
+                      displayName={user.displayName}
+                      scoreText={`${user.streak || 0} days`}
+                      medal={medal}
+                      isCurrentUser={!!isCurrentUser}
+                    />
+                  );
+                })
+              : progressData.map((user: ProgressEntry) => {
+                  const isCurrentUser =
+                    hasDisplayName && userRank && user.rank === userRank.rank;
+                  const medal = getMedalIcon(user.rank);
+                  return (
+                    <MiniRow
+                      key={`${user.displayName}-${user.rank}`}
+                      rank={user.rank}
+                      displayName={user.displayName}
+                      scoreText={`${user.score || 0} pts`}
+                      medal={medal}
+                      isCurrentUser={!!isCurrentUser}
+                    />
+                  );
Evidence
Both UI modules define their own entry shapes and then assert those shapes in rendering. If the
backend response changes (rename fields, change nullability), these local types may not be updated
in lockstep.

src/components/dashboard/LeaderboardMiniSection.tsx[18-30]
src/components/dashboard/LeaderboardMiniSection.tsx[134-163]
src/routes/_authed/leaderboard.tsx[23-35]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Leaderboard entry types are duplicated in multiple UI files and asserted in map callbacks, which can drift from backend response shapes over time.

### Issue Context
The PR aims to improve type safety by removing `as any`, but duplicated manual types still require ongoing manual synchronization.

### Fix Focus Areas
- src/components/dashboard/LeaderboardMiniSection.tsx[16-31]
- src/routes/_authed/leaderboard.tsx[20-36]
- src/components/dashboard/LeaderboardMiniSection.tsx[134-163]
- src/routes/_authed/leaderboard.tsx[213-235]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@coderabbitai
Copy link

coderabbitai bot commented Mar 2, 2026

📝 Walkthrough

Walkthrough

This PR refactors the leaderboard UI components by introducing typed data structures (StreakEntry, ProgressEntry) for better type safety, adding a new MiniRow component to the mini leaderboard section, consolidating row rendering logic into a reusable helper, and implementing client-side pagination in the main leaderboard view.

Changes

Cohort / File(s) Summary
Mini Leaderboard Section
src/components/dashboard/LeaderboardMiniSection.tsx
Introduces StreakEntry and ProgressEntry types, adds MiniRow component for rendering individual entries with medals and score formatting (days for streak, points for progress), and refactors data rendering to map per-type data through the new component while adding current-user row display for users outside top 5 but within top 50.
Main Leaderboard Route
src/routes/_authed/leaderboard.tsx
Introduces StreakEntry and ProgressEntry types, adds currentPage state for client-side pagination with Previous/Next controls, creates shared renderRow helper function for consistent row rendering across leaderboard types, splits data display logic by type (streak vs progress), and implements current-user highlighting with isCurrentUser utility function.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 A leaderboard hops with prettier rows,
With medals and types, the data now flows,
Pagination clicks—past and next take their turn,
The current user's rank we boldly discern,
Streak days and points in their rightful place! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main objective of the PR: fixing type safety in leaderboard-related code by removing 6 as-any casts and introducing proper types.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch nightshift/2026-03-02-1525

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment on lines +92 to +123
const renderRow = (
rank: number,
displayName: string,
flag: string,
scoreText: string,
isUser: boolean,
medal: string | null,
) => (
<div
key={`${displayName}-${rank}`}
className={`flex items-center gap-4 p-4 rounded-lg transition-colors ${
isUser ? "bg-primary/10 border border-primary/20" : "hover:bg-muted/50"
}`}
>
<div className="w-8 text-center font-mono text-sm text-muted-foreground">
{rank}
</div>
<div className="w-6 text-center">
{medal && <span className="text-lg">{medal}</span>}
</div>
<div className="flex items-center gap-3 flex-1">
<span className="text-lg">{flag}</span>
<span className="font-medium">
{displayName}
{isUser && <span className="ml-2 text-primary">⭐</span>}
</span>
</div>
<div className="text-right">
<div className="font-mono font-medium">{scoreText}</div>
</div>
</div>
);

Choose a reason for hiding this comment

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

Action required

1. renderrow missing tests 📘 Rule violation ⛯ Reliability

A new renderRow helper was introduced in the leaderboard route as part of the type-safety refactor
without adding any automated tests. This increases regression risk for ranking/flag/score rendering
across leaderboard modes.
Agent Prompt
## Issue description
A new helper (`renderRow`) and a refactor of leaderboard row rendering were added without automated test coverage.

## Issue Context
This PR is a leaderboard type-safety bug fix. The compliance checklist requires tests for new helpers and regression tests for bug fixes when feasible.

## Fix Focus Areas
- src/routes/_authed/leaderboard.tsx[92-123]
- src/routes/_authed/leaderboard.tsx[213-235]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines 46 to 60
const { data: streakData = [], isLoading: streakLoading } = useQuery(
convexQuery(api.leaderboard.getStreakLeaderboard, {
limit,
convexQuery(api.leaderboard.getStreakLeaderboard, {
limit,
offset,
period: selectedPeriod
})
period: selectedPeriod,
}),
);

const { data: progressData = [], isLoading: progressLoading } = useQuery(
convexQuery(api.leaderboard.getProgressLeaderboard, {
limit,
convexQuery(api.leaderboard.getProgressLeaderboard, {
limit,
offset,
period: selectedPeriod
})
period: selectedPeriod,
}),
);

Choose a reason for hiding this comment

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

Action required

2. Period toggle ineffective 🐞 Bug ✓ Correctness

The leaderboard page offers Weekly/Monthly/All-time and passes period into Convex queries, but the
backend leaderboard queries ignore period, so the UI filter is misleading and results won’t
change. Users will see “This Week/This Month” labels without any corresponding data change.
Agent Prompt
### Issue description
The leaderboard UI exposes Weekly/Monthly/All-time, but the backend queries ignore the `period` argument, so changing the period does not change the data.

### Issue Context
Frontend passes `period: selectedPeriod` into Convex queries and displays period labels, so users expect filtering.

### Fix Focus Areas
- src/routes/_authed/leaderboard.tsx[45-68]
- convex/leaderboard.ts[145-156]
- convex/leaderboard.ts[228-235]
- convex/leaderboard.ts[271-279]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/routes/_authed/leaderboard.tsx (1)

37-289: ⚠️ Potential issue | 🟠 Major

Missing test file: Create leaderboard.test.tsx with required coverage.

The LeaderboardPage component is missing its test file. Per coding guidelines, components with logic must have corresponding test files following the naming convention. Create src/routes/_authed/leaderboard.test.tsx with coverage for:

  • Filter changes (selectedType, selectedPeriod state updates)
  • Pagination (currentPage increment/decrement, button disabled states)
  • Rank rendering and user highlighting (isCurrentUser logic, medal icons)
  • Data loading states and empty state
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/_authed/leaderboard.tsx` around lines 37 - 289, Add a missing test
file for the LeaderboardPage component by creating
src/routes/_authed/leaderboard.test.tsx and implement unit/interaction tests
that cover: updating filters (simulate toggling selectedType and selectedPeriod
and assert LeaderboardPage rerenders accordingly), pagination (simulate clicking
Next/Previous, assert currentPage changes and button disabled states), rank
rendering and user highlighting (verify renderRow output via LeaderboardPage,
assert isCurrentUser behavior shows the user highlight and getMedalIcon returns
appropriate emojis for ranks 1–3), and loading/empty states (mock the convex
queries to return loading, empty arrays, and populated data and assert skeletons
and "No users" message). Target the component functions/props named
LeaderboardPage, isCurrentUser, getMedalIcon, renderRow and mock
api.leaderboard.getStreakLeaderboard/getProgressLeaderboard/getUserRank to
achieve required coverage.
src/components/dashboard/LeaderboardMiniSection.tsx (1)

60-205: ⚠️ Potential issue | 🟠 Major

Add LeaderboardMiniSection.test.tsx—this component requires test coverage per guidelines.

The component has multiple conditional rendering paths (display-name gate, type toggle between streak/progress data, current-user highlighting, and rank display logic) but no corresponding test file. Per coding guidelines, all components with logic must have a test file following the naming convention ComponentName.test.tsx.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/LeaderboardMiniSection.tsx` around lines 60 - 205,
Add a new test file LeaderboardMiniSection.test.tsx that covers the component
LeaderboardMiniSection: mock the convex queries (streakData, progressData,
userRank, userInfo) and test the four conditional paths — (1) when
userInfo.displayName is null show the "Set Display Name" gate and link, (2)
toggling selectedType between "streak" and "progress" renders streakData vs
progressData rows (verify MiniRow props like rank/displayName/scoreText), (3)
ensure top-3 ranks render medal icons via getMedalIcon and (4) when
userRank.rank > 5 && <= 50 render the separate current-user rank block showing
userInfo.displayName and correct score text; also include a case asserting
isCurrentUser highlights the correct row (matching userRank.rank). Use the
component name LeaderboardMiniSection, helper MiniRow, and state key
selectedType when triggering toggles and name the file exactly
LeaderboardMiniSection.test.tsx.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/dashboard/LeaderboardMiniSection.tsx`:
- Around line 150-160: The MiniRow is rendering raw fractional progress scores
(user.score) causing UI mismatch with the rounded full leaderboard; update the
mapping in the progressData.map (and the other similar mapping around the
MiniRow usage) to format the score consistently—compute a display string by
rounding or fixing to a consistent number format (e.g., Math.round or
toFixed(0/1)) and pass that formatted value into the MiniRow via scoreText
instead of raw user.score so ProgressEntry/user.score is displayed consistently
across the UI.

In `@src/routes/_authed/leaderboard.tsx`:
- Around line 139-141: When changing leaderboard filters you must reset
pagination to avoid invalid offsets: update the onValueChange handler that calls
setSelectedType and the handler that calls setSelectedPeriod to also call
setCurrentPage(1) (or the first page number used by the component) so
currentPage is reset whenever filters change; reference setSelectedType,
setSelectedPeriod and setCurrentPage to locate and modify the handlers
accordingly.
- Around line 214-223: The map over streakData recomputes rank using
offset+index (streakData.map in this file) which can drift from the
server-provided ranking; change the code to use the StreakEntry.rank field (use
user.rank) when calling renderRow, getMedalIcon and isCurrentUser instead of the
locally computed rank, and ensure isCurrentUser compares against the server rank
(or accept a rank argument) so both medal display and current-user highlighting
use the backend's authoritative rank; update both occurrences where rank is
computed from offset/index and pass user.rank through to
renderRow/getMedalIcon/isCurrentUser.

---

Outside diff comments:
In `@src/components/dashboard/LeaderboardMiniSection.tsx`:
- Around line 60-205: Add a new test file LeaderboardMiniSection.test.tsx that
covers the component LeaderboardMiniSection: mock the convex queries
(streakData, progressData, userRank, userInfo) and test the four conditional
paths — (1) when userInfo.displayName is null show the "Set Display Name" gate
and link, (2) toggling selectedType between "streak" and "progress" renders
streakData vs progressData rows (verify MiniRow props like
rank/displayName/scoreText), (3) ensure top-3 ranks render medal icons via
getMedalIcon and (4) when userRank.rank > 5 && <= 50 render the separate
current-user rank block showing userInfo.displayName and correct score text;
also include a case asserting isCurrentUser highlights the correct row (matching
userRank.rank). Use the component name LeaderboardMiniSection, helper MiniRow,
and state key selectedType when triggering toggles and name the file exactly
LeaderboardMiniSection.test.tsx.

In `@src/routes/_authed/leaderboard.tsx`:
- Around line 37-289: Add a missing test file for the LeaderboardPage component
by creating src/routes/_authed/leaderboard.test.tsx and implement
unit/interaction tests that cover: updating filters (simulate toggling
selectedType and selectedPeriod and assert LeaderboardPage rerenders
accordingly), pagination (simulate clicking Next/Previous, assert currentPage
changes and button disabled states), rank rendering and user highlighting
(verify renderRow output via LeaderboardPage, assert isCurrentUser behavior
shows the user highlight and getMedalIcon returns appropriate emojis for ranks
1–3), and loading/empty states (mock the convex queries to return loading, empty
arrays, and populated data and assert skeletons and "No users" message). Target
the component functions/props named LeaderboardPage, isCurrentUser,
getMedalIcon, renderRow and mock
api.leaderboard.getStreakLeaderboard/getProgressLeaderboard/getUserRank to
achieve required coverage.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 93c1185 and 4cd5464.

📒 Files selected for processing (2)
  • src/components/dashboard/LeaderboardMiniSection.tsx
  • src/routes/_authed/leaderboard.tsx
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TanStack Start framework with Bun runtime for the application

Tests must pass locally via bun run test before committing code, as Husky pre-commit hooks will block commits with failing tests

Files:

  • src/components/dashboard/LeaderboardMiniSection.tsx
  • src/routes/_authed/leaderboard.tsx
**/*.tsx

📄 CodeRabbit inference engine (CLAUDE.md)

Components with logic MUST have corresponding test files following the naming convention ComponentName.test.tsx

Files:

  • src/components/dashboard/LeaderboardMiniSection.tsx
  • src/routes/_authed/leaderboard.tsx
🧠 Learnings (6)
📓 Common learnings
Learnt from: CR
Repo: EtanHey/songscript PR: 0
File: prd-json/AGENTS.md:0-0
Timestamp: 2026-01-23T18:12:49.193Z
Learning: Applies to prd-json/**/convex/**/*{score,progress,leaderboard}*.{ts,tsx,js} : Use Progress Score Formula: (words_learned × multiplier) + (lines_completed × multiplier × 0.5)
Learnt from: CR
Repo: EtanHey/songscript PR: 0
File: prd-json/AGENTS.md:0-0
Timestamp: 2026-01-23T18:12:49.193Z
Learning: Applies to prd-json/**/convex/**/leaderboard*.{ts,tsx,js} : Users can play without displayName, but must set displayName before appearing on the leaderboard
📚 Learning: 2026-01-23T18:12:49.193Z
Learnt from: CR
Repo: EtanHey/songscript PR: 0
File: prd-json/AGENTS.md:0-0
Timestamp: 2026-01-23T18:12:49.193Z
Learning: Applies to prd-json/**/convex/**/leaderboard*.{ts,tsx,js} : Users can play without displayName, but must set displayName before appearing on the leaderboard

Applied to files:

  • src/components/dashboard/LeaderboardMiniSection.tsx
  • src/routes/_authed/leaderboard.tsx
📚 Learning: 2026-01-23T18:12:49.193Z
Learnt from: CR
Repo: EtanHey/songscript PR: 0
File: prd-json/AGENTS.md:0-0
Timestamp: 2026-01-23T18:12:49.193Z
Learning: Applies to prd-json/**/convex/**/*{score,progress,leaderboard}*.{ts,tsx,js} : Use Progress Score Formula: (words_learned × multiplier) + (lines_completed × multiplier × 0.5)

Applied to files:

  • src/components/dashboard/LeaderboardMiniSection.tsx
  • src/routes/_authed/leaderboard.tsx
📚 Learning: 2026-01-23T18:12:38.519Z
Learnt from: CR
Repo: EtanHey/songscript PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-23T18:12:38.519Z
Learning: Applies to convex/*.ts : Use Convex for database queries, mutations, and authentication configuration

Applied to files:

  • src/routes/_authed/leaderboard.tsx
📚 Learning: 2026-01-23T18:12:49.193Z
Learnt from: CR
Repo: EtanHey/songscript PR: 0
File: prd-json/AGENTS.md:0-0
Timestamp: 2026-01-23T18:12:49.193Z
Learning: Complete all backend convex/ queries before implementing dashboard/page components

Applied to files:

  • src/routes/_authed/leaderboard.tsx
📚 Learning: 2026-01-28T12:17:53.877Z
Learnt from: CR
Repo: EtanHey/songscript PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-28T12:17:53.877Z
Learning: Applies to convex/**/*.ts : Use Convex schema.ts for database schema definitions and songs.ts for queries and mutations

Applied to files:

  • src/routes/_authed/leaderboard.tsx
🧬 Code graph analysis (2)
src/components/dashboard/LeaderboardMiniSection.tsx (1)
src/components/dashboard/index.tsx (1)
  • LeaderboardMiniSection (65-65)
src/routes/_authed/leaderboard.tsx (2)
src/components/ui/card.tsx (3)
  • CardTitle (88-88)
  • CardHeader (86-86)
  • CardContent (91-91)
src/components/LanguageFlag.tsx (1)
  • getLanguageFlagString (75-80)

Comment on lines +150 to +160
: progressData.map((user: ProgressEntry) => {
const isCurrentUser =
hasDisplayName && userRank && user.rank === userRank.rank;
const medal = getMedalIcon(user.rank);
return (
<MiniRow
key={`${user.displayName}-${user.rank}`}
rank={user.rank}
displayName={user.displayName}
scoreText={`${user.score || 0} pts`}
medal={medal}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Normalize progress score formatting to avoid raw fractional output drift.

Progress scores can be fractional; this mini section currently renders raw values while the full leaderboard rounds them. Keep formatting consistent to avoid UI mismatch.

💡 Proposed fix
-                      scoreText={`${user.score || 0} pts`}
+                      scoreText={`${Math.round(user.score || 0)} pts`}
@@
-                          : `${userRank.score} pts`}
+                          : `${Math.round(userRank.score)} pts`}

Based on learnings, progress score uses (words_learned × multiplier) + (lines_completed × multiplier × 0.5), so fractional values are expected and should be formatted consistently.

Also applies to: 186-188

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/dashboard/LeaderboardMiniSection.tsx` around lines 150 - 160,
The MiniRow is rendering raw fractional progress scores (user.score) causing UI
mismatch with the rounded full leaderboard; update the mapping in the
progressData.map (and the other similar mapping around the MiniRow usage) to
format the score consistently—compute a display string by rounding or fixing to
a consistent number format (e.g., Math.round or toFixed(0/1)) and pass that
formatted value into the MiniRow via scoreText instead of raw user.score so
ProgressEntry/user.score is displayed consistently across the UI.

Comment on lines +139 to +141
onValueChange={(value) =>
value && setSelectedType(value as LeaderboardType)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reset pagination when leaderboard filters change.

Line 139 and Line 158 handlers change type/period but keep currentPage. If the user is on a later page, switching filters can land on invalid offsets and show misleading empty results.

💡 Proposed fix
-          onValueChange={(value) =>
-            value && setSelectedType(value as LeaderboardType)
-          }
+          onValueChange={(value) => {
+            if (!value) return;
+            setSelectedType(value as LeaderboardType);
+            setCurrentPage(0);
+          }}
@@
-          onValueChange={(value) =>
-            value && setSelectedPeriod(value as TimePeriod)
-          }
+          onValueChange={(value) => {
+            if (!value) return;
+            setSelectedPeriod(value as TimePeriod);
+            setCurrentPage(0);
+          }}

Also applies to: 158-160

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/_authed/leaderboard.tsx` around lines 139 - 141, When changing
leaderboard filters you must reset pagination to avoid invalid offsets: update
the onValueChange handler that calls setSelectedType and the handler that calls
setSelectedPeriod to also call setCurrentPage(1) (or the first page number used
by the component) so currentPage is reset whenever filters change; reference
setSelectedType, setSelectedPeriod and setCurrentPage to locate and modify the
handlers accordingly.

Comment on lines +214 to +223
? streakData.map((user: StreakEntry, index: number) => {
const rank = offset + index + 1;
return renderRow(
rank,
user.displayName,
getLanguageFlagString(user.language),
`${user.streak} days`,
isCurrentUser(rank),
getMedalIcon(rank),
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use backend user.rank instead of index-derived rank.

Line 215 and Line 226 recompute rank from page offset/index even though entries include rank. That can drift from server ranking rules and break medal/current-user consistency.

💡 Proposed fix
-                ? streakData.map((user: StreakEntry, index: number) => {
-                    const rank = offset + index + 1;
+                ? streakData.map((user: StreakEntry) => {
                     return renderRow(
-                      rank,
+                      user.rank,
                       user.displayName,
                       getLanguageFlagString(user.language),
                       `${user.streak} days`,
-                      isCurrentUser(rank),
-                      getMedalIcon(rank),
+                      isCurrentUser(user.rank),
+                      getMedalIcon(user.rank),
                     );
                   })
-                : progressData.map((user: ProgressEntry, index: number) => {
-                    const rank = offset + index + 1;
+                : progressData.map((user: ProgressEntry) => {
                     return renderRow(
-                      rank,
+                      user.rank,
                       user.displayName,
                       getLanguageFlagString(user.topLanguage),
                       `${Math.round(user.score)} pts`,
-                      isCurrentUser(rank),
-                      getMedalIcon(rank),
+                      isCurrentUser(user.rank),
+                      getMedalIcon(user.rank),
                     );
                   })}

Also applies to: 225-234

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/_authed/leaderboard.tsx` around lines 214 - 223, The map over
streakData recomputes rank using offset+index (streakData.map in this file)
which can drift from the server-provided ranking; change the code to use the
StreakEntry.rank field (use user.rank) when calling renderRow, getMedalIcon and
isCurrentUser instead of the locally computed rank, and ensure isCurrentUser
compares against the server rank (or accept a rank argument) so both medal
display and current-user highlighting use the backend's authoritative rank;
update both occurrences where rank is computed from offset/index and pass
user.rank through to renderRow/getMedalIcon/isCurrentUser.

@EtanHey
Copy link
Owner Author

EtanHey commented Mar 12, 2026

Closing: Night Shift launchd agent permanently disabled on 2026-03-12.

@EtanHey EtanHey closed this Mar 12, 2026
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