Skip to content

feat: implement leaderboard page with infinite scrolling, filter pers… - #192

Closed
TheBigWealth89 wants to merge 2 commits into
boundlessfi:mainfrom
TheBigWealth89:issue/184
Closed

TheBigWealth89 wants to merge 2 commits into
boundlessfi:mainfrom
TheBigWealth89:issue/184

Conversation

@TheBigWealth89

@TheBigWealth89 TheBigWealth89 commented Apr 25, 2026

Copy link
Copy Markdown

PR: Leaderboard Dynamic Auth & Routing Fix

Overview

This PR implements dynamic authenticated user highlighting on the leaderboard, addresses unauthenticated/loading states, and fixes a routing bug where profile links were incorrectly pointing to /user/[id] instead of /profile/[id].

Key Changes

1. Centralized Routing (lib/routes.ts)

  • Introduced a new ROUTES constant to eliminate hardcoded path strings.
  • Added ROUTES.PROFILE(userId), ROUTES.AUTH, and ROUTES.LEADERBOARD.

2. Leaderboard Page (app/leaderboard/page.tsx)

  • Dynamic Auth: Replaced the hardcoded currentUserId = "user-1" with authClient.useSession().
  • FOUC Prevention: Utilizes isPending from the auth session to defer row highlighting and sidebar rendering until the user's status is confirmed.
  • Routing Fix: Migrated navigation to use ROUTES.PROFILE(userId).

3. Accessible Table Highlighting (components/leaderboard/leaderboard-table.tsx)

  • Visual Improvements: Added a primary-colored left-border accent and a subtle background tint to the current user's row.
  • "You" Badge: Replaced plain text with a UI badge that includes aria-label and sr-only text for screen readers.
  • Smart Scroll: Added a useEffect with a scrollIntoView call that fires once on mount, but only if the current user is ranked lower than the top 10 (keeping the fold clean for top performers).

4. Polished Sidebar State (components/leaderboard/user-rank-sidebar.tsx)

  • Loading State: Now accepts isSessionPending to show a skeleton loader instead of the "Sign In" CTA while auth status is resolving.
  • Guest CTA: Replaced the bare "Connect your wallet" text with a polished card featuring a Trophy icon, descriptive text, and a direct link to the auth page.

Files Modified

  • [NEW] lib/routes.ts
  • [MODIFY] app/leaderboard/page.tsx
  • [MODIFY] components/leaderboard/leaderboard-table.tsx
  • [MODIFY] components/leaderboard/user-rank-sidebar.tsx

Verification

  • Verified zero TypeScript errors using npx tsc --noEmit.
  • Manually tested in-browser:
    • Unauthenticated users see the polished Sign-In card.
    • Authenticated users see their rank card and highlighted row.
    • Clicking any row navigates correctly to /profile/[userId].

Summary by CodeRabbit

Release Notes

  • New Features

    • Integrated user authentication with session support for personalized leaderboard experience
    • Added automatic smooth scrolling to highlight the current user's position in the leaderboard
    • Implemented sign-in prompts for unauthenticated users
  • Accessibility

    • Improved screen reader and assistive technology support for leaderboard rankings

@vercel

vercel Bot commented Apr 25, 2026

Copy link
Copy Markdown

@TheBigWealth89 is attempting to deploy a commit to the Threadflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Apr 25, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@TheBigWealth89 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 55 minutes and 54 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 55 minutes and 54 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9b4cfed5-b853-44fd-a52d-1e1785798e71

📥 Commits

Reviewing files that changed from the base of the PR and between 146ac3b and 51ba802.

📒 Files selected for processing (1)
  • app/leaderboard/page.tsx
📝 Walkthrough

Walkthrough

The PR adds a GraphQL API endpoint for leaderboard queries, refactors leaderboard components to authenticate users via session, implements auto-scrolling to the current user's table row, and introduces centralized route definitions throughout the application.

Changes

Cohort / File(s) Summary
GraphQL API
app/api/graphql/route.ts
New mock GraphQL POST endpoint parsing query and variables; routes leaderboard and user rank queries to helper functions; returns structured JSON responses with pagination/tier support; handles errors and exceptions.
Centralized Routes
lib/routes.ts
New constants file exporting ROUTES object with /auth, /leaderboard paths and PROFILE(userId) helper function.
Leaderboard Features
app/leaderboard/page.tsx, components/leaderboard/leaderboard-table.tsx, components/leaderboard/user-rank-sidebar.tsx
Components refactored to derive current user from auth session instead of hardcoded ID; added isSessionPending prop to suppress unauthenticated UX flashes; auto-scrolls to current user's row when rank >10; improved accessibility with aria-current and "You" badge; sign-in CTA replaces static wallet text; navigation updated to use centralized ROUTES definitions.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Page as Leaderboard Page
    participant Auth as Auth Session
    participant GraphQL as GraphQL API
    participant MockData as Mock Helpers

    Client->>Page: Load leaderboard
    Page->>Auth: Validate session
    
    alt Session pending
        Auth-->>Page: Session resolving
        Page->>Page: Suppress userId (undefined)
        Page->>Client: Render skeleton
    else Session resolved
        Auth-->>Page: userId (or null)
        Page->>GraphQL: POST leaderboard query
        GraphQL->>MockData: getMockLeaderboard(page, limit, tier)
        MockData-->>GraphQL: Ranked entries + totalCount
        GraphQL-->>Page: data.leaderboard response
        
        alt User found in entries
            Page->>Page: Scroll to user's row
            Page->>Client: Render table with highlight
        else User not in entries
            Page->>Client: Render table without scroll
        end
    end
Loading
sequenceDiagram
    participant Client
    participant Sidebar as User Rank Sidebar
    participant Auth as Auth Session
    participant GraphQL as GraphQL API
    participant MockData as Mock Helpers

    Client->>Sidebar: Render sidebar
    Sidebar->>Auth: Check session status
    
    alt isSessionPending === true
        Auth-->>Sidebar: Session loading
        Sidebar->>Client: Show skeleton
    else No userId
        Auth-->>Sidebar: Unauthenticated
        Sidebar->>Client: Show sign-in CTA
    else userId exists
        Sidebar->>GraphQL: POST userLeaderboardRank query
        GraphQL->>MockData: getMockUserRank(userId)
        MockData-->>GraphQL: User rank data
        GraphQL-->>Sidebar: data.userLeaderboardRank
        Sidebar->>Client: Render rank card
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • Benjtalkshow

Poem

🐰 A rabbit hops through session flows,

With GraphQL that brightly glows,

Routes centralized, auth so keen,

Auto-scroll finds users unseen,

Accessibility badges shine—the UI grows! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main objective of implementing the leaderboard page with infinite scrolling and filter persistence, and is aligned with the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (4)
app/api/graphql/route.ts (1)

11-16: filters other than tier are silently dropped.

The leaderboard hook may grow filters (search, time range, etc.) but this mock only forwards tier to getMockLeaderboard. That's fine for now since getMockLeaderboard only supports filterTier, but consumers debugging "why doesn't my filter work?" will have a hard time. Consider either narrowing the destructure to make the contract explicit, or adding a brief comment noting that only tier is honored by the mock.

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

In `@app/api/graphql/route.ts` around lines 11 - 16, The current destructuring
pulls filters and pagination but silently ignores any filter keys other than
tier when calling getMockLeaderboard (variables -> { filters, pagination } ->
tier), which can confuse callers; either narrow the destructure to explicitly
extract only tier from filters (e.g., pull tier from filters directly) so the
contract is explicit, or add a concise inline comment above the call to
getMockLeaderboard stating that the mock only honors the tier filter today;
update uses of variables, page, limit, and tier around the getMockLeaderboard
invocation accordingly.
lib/routes.ts (1)

5-14: LGTM — consider migrating remaining hardcoded routes in a follow-up.

The ROUTES shape is clean and as const preserves literal types. Several other call sites still hardcode /auth and /leaderboard (e.g., components/global-navbar.tsx, components/leaderboard/mini-leaderboard.tsx, components/ui/global-resizable-navbar.tsx, app/(auth)/auth/magic-link/verify/page.tsx); migrating those to ROUTES.AUTH / ROUTES.LEADERBOARD in a follow-up will complete the centralization.

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

In `@lib/routes.ts` around lines 5 - 14, Several files still use hardcoded route
strings; replace literal "/auth" and "/leaderboard" occurrences with the
centralized constants ROUTES.AUTH and ROUTES.LEADERBOARD respectively. Update
references in components/global-navbar.tsx,
components/leaderboard/mini-leaderboard.tsx,
components/ui/global-resizable-navbar.tsx and
app/(auth)/auth/magic-link/verify/page.tsx to import ROUTES from lib/routes and
use ROUTES.AUTH or ROUTES.LEADERBOARD (or ROUTES.PROFILE(userId) where
applicable) so all routing strings are centralized and type-safe.
components/leaderboard/leaderboard-table.tsx (1)

166-168: Redundant role="row" on TableRow.

<TableRow> already renders a <tr> with the implicit role="row", so the explicit attribute is a no-op. If the intent of overriding the role was to communicate the row's clickability, role="row" doesn't convey that — a screen-reader-friendly approach is to keep the implicit row semantics and rely on the existing tabIndex/onKeyDown/aria-current, or render an inner <button> for the activation target. Easiest fix is to drop the prop.

🧹 Suggested cleanup
                 tabIndex={onRowClick ? 0 : undefined}
-                role={onRowClick ? "row" : undefined}
                 onClick={onRowClick ? () => onRowClick(entry) : undefined}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/leaderboard/leaderboard-table.tsx` around lines 166 - 168, Remove
the redundant role="row" prop from the TableRow render (the implicit <tr>
already has that role); keep the existing conditional tabIndex and onClick
behavior tied to onRowClick and, if needed for accessibility, ensure keyboard
activation via the component's onKeyDown or by rendering an inner <button> for
the interactive target instead of overriding role. Target the TableRow element
using the TableRow render block where tabIndex={onRowClick ? 0 : undefined},
role={...}, and onClick={...} and simply drop the role prop.
components/leaderboard/user-rank-sidebar.tsx (1)

199-227: TODO acknowledged — track removal of mock thresholds before launch.

The fallback math (currentTierPoints ?? totalScore and nextTierThreshold ?? totalScore * 1.5) is reasonable as a placeholder, but totalScore * 1.5 will always render a non-empty progress bar at ~67% for any logged-in user, which can be misleading. Consider hiding the progress block entirely when the API hasn't supplied real thresholds, or returning null from the IIFE when the API fields are missing.

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

In `@components/leaderboard/user-rank-sidebar.tsx` around lines 199 - 227, The
progress bar should be hidden when the API hasn't provided real threshold data;
update the IIFE so it returns null unless contributor.stats.nextTierThreshold is
present (i.e., only render the Progress block when
contributor.stats.nextTierThreshold != null), remove the fallback that uses
contributor.totalScore * 1.5 for nextTierThreshold, and keep using
contributor.stats.currentTierPoints ?? contributor.totalScore to compute
progressPercent against the real nextTierThreshold; reference
contributor.stats.nextTierThreshold, contributor.stats.currentTierPoints,
contributor.totalScore, and progressPercent to locate where to change the logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/api/graphql/route.ts`:
- Around line 6-10: The handler reads body and then does query.includes(...),
which throws if query is missing or not a string; update the GraphQL route
handler to validate that query exists and is a string before calling .includes
(e.g., check typeof query === "string"), and if the check fails return a 400 Bad
Request with a clear error message; keep the existing logic for handling
leaderboard queries (the same includes checks) only after the validation passes
so malformed requests no longer produce a 500.
- Around line 10-43: The current routing based on query.includes(...) is brittle
and misorders branches; update the handler to derive the GraphQL operation name
once (e.g., extract the operation name from the `query` string via a simple
regex like /^\s*(query|mutation)\s+([A-Za-z0-9_]+)/ or by using a lightweight
parser) and switch on that name instead of substring checks, then route to
getMockUserRank(userId) when the operation name === "UserLeaderboardRank" and to
getMockLeaderboard(page, limit, tier) when the operation name === "Leaderboard";
alternatively, if you want the smallest change, simply check the more specific
`UserLeaderboardRank` branch (calling getMockUserRank) before the broader
`leaderboard` branch to avoid accidental matches.

In `@components/leaderboard/leaderboard-table.tsx`:
- Around line 70-96: The effect currently sets hasScrolledRef.current = true and
never resets it, so after the first auto-scroll future filter changes (which
produce new entries) won't re-trigger scrolling; update the component to reset
hasScrolledRef.current to false whenever the filter-driven entry set changes
(e.g., when timeframe, tier, or tags change or when entries identity changes) so
the useEffect that reads hasScrolledRef, currentUserId, currentUserRowRef and
entries can run the scroll logic again; locate where filters or entries are
updated and set hasScrolledRef.current = false there (or include the specific
filter values in the useEffect dependency array and reset the ref at the start).

In `@components/leaderboard/user-rank-sidebar.tsx`:
- Around line 80-85: The current JSX nests a <Button> inside a <Link> which
creates invalid HTML (<a><button>) — change to use the shadcn asChild pattern by
rendering the Link as the Button's child: wrap the Link (href={ROUTES.AUTH})
inside <Button asChild variant="default" size="sm" className="w-full"> so the
Button forwards its styling/behavior to the anchor element and you end up with a
single interactive <a> element; preserve the w-full classes and Sign In label
when moving Link inside Button.

---

Nitpick comments:
In `@app/api/graphql/route.ts`:
- Around line 11-16: The current destructuring pulls filters and pagination but
silently ignores any filter keys other than tier when calling getMockLeaderboard
(variables -> { filters, pagination } -> tier), which can confuse callers;
either narrow the destructure to explicitly extract only tier from filters
(e.g., pull tier from filters directly) so the contract is explicit, or add a
concise inline comment above the call to getMockLeaderboard stating that the
mock only honors the tier filter today; update uses of variables, page, limit,
and tier around the getMockLeaderboard invocation accordingly.

In `@components/leaderboard/leaderboard-table.tsx`:
- Around line 166-168: Remove the redundant role="row" prop from the TableRow
render (the implicit <tr> already has that role); keep the existing conditional
tabIndex and onClick behavior tied to onRowClick and, if needed for
accessibility, ensure keyboard activation via the component's onKeyDown or by
rendering an inner <button> for the interactive target instead of overriding
role. Target the TableRow element using the TableRow render block where
tabIndex={onRowClick ? 0 : undefined}, role={...}, and onClick={...} and simply
drop the role prop.

In `@components/leaderboard/user-rank-sidebar.tsx`:
- Around line 199-227: The progress bar should be hidden when the API hasn't
provided real threshold data; update the IIFE so it returns null unless
contributor.stats.nextTierThreshold is present (i.e., only render the Progress
block when contributor.stats.nextTierThreshold != null), remove the fallback
that uses contributor.totalScore * 1.5 for nextTierThreshold, and keep using
contributor.stats.currentTierPoints ?? contributor.totalScore to compute
progressPercent against the real nextTierThreshold; reference
contributor.stats.nextTierThreshold, contributor.stats.currentTierPoints,
contributor.totalScore, and progressPercent to locate where to change the logic.

In `@lib/routes.ts`:
- Around line 5-14: Several files still use hardcoded route strings; replace
literal "/auth" and "/leaderboard" occurrences with the centralized constants
ROUTES.AUTH and ROUTES.LEADERBOARD respectively. Update references in
components/global-navbar.tsx, components/leaderboard/mini-leaderboard.tsx,
components/ui/global-resizable-navbar.tsx and
app/(auth)/auth/magic-link/verify/page.tsx to import ROUTES from lib/routes and
use ROUTES.AUTH or ROUTES.LEADERBOARD (or ROUTES.PROFILE(userId) where
applicable) so all routing strings are centralized and type-safe.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 42f4cbc5-444d-4bbe-9d2b-928ba70e666a

📥 Commits

Reviewing files that changed from the base of the PR and between 2062df7 and 146ac3b.

📒 Files selected for processing (5)
  • app/api/graphql/route.ts
  • app/leaderboard/page.tsx
  • components/leaderboard/leaderboard-table.tsx
  • components/leaderboard/user-rank-sidebar.tsx
  • lib/routes.ts

Comment thread app/api/graphql/route.ts
Comment on lines +6 to +10
const body = await request.json();
const { query, variables } = body;

// Simple mock GraphQL server handling the leaderboard queries
if (query.includes("query Leaderboard") || query.includes("leaderboard")) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Validate query shape before calling .includes.

If a client sends a body without a query field (or with a non-string query), line 10 throws TypeError: Cannot read properties of undefined (reading 'includes'), which is caught and surfaced as a generic 500. A malformed request should be a 400.

🛡️ Proposed fix
     const body = await request.json();
     const { query, variables } = body;
+
+    if (typeof query !== "string") {
+      return NextResponse.json(
+        { error: "Missing or invalid 'query' field" },
+        { status: 400 },
+      );
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const body = await request.json();
const { query, variables } = body;
// Simple mock GraphQL server handling the leaderboard queries
if (query.includes("query Leaderboard") || query.includes("leaderboard")) {
const body = await request.json();
const { query, variables } = body;
if (typeof query !== "string") {
return NextResponse.json(
{ error: "Missing or invalid 'query' field" },
{ status: 400 },
);
}
// Simple mock GraphQL server handling the leaderboard queries
if (query.includes("query Leaderboard") || query.includes("leaderboard")) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/graphql/route.ts` around lines 6 - 10, The handler reads body and
then does query.includes(...), which throws if query is missing or not a string;
update the GraphQL route handler to validate that query exists and is a string
before calling .includes (e.g., check typeof query === "string"), and if the
check fails return a 400 Bad Request with a clear error message; keep the
existing logic for handling leaderboard queries (the same includes checks) only
after the validation passes so malformed requests no longer produce a 500.

Comment thread app/api/graphql/route.ts
Comment on lines +10 to +43
if (query.includes("query Leaderboard") || query.includes("leaderboard")) {
const { filters, pagination } = variables || {};
const page = pagination?.page || 1;
const limit = pagination?.limit || 20;
const tier = filters?.tier;

const mockData = getMockLeaderboard(page, limit, tier);

return NextResponse.json({
data: {
leaderboard: {
entries: mockData.data.map((entry, index) => ({
rank: (page - 1) * limit + index + 1,
contributor: entry,
})),
totalCount: mockData.total,
},
},
});
}

if (
query.includes("query UserLeaderboardRank") ||
query.includes("userLeaderboardRank")
) {
const { userId } = variables || {};
const rankData = getMockUserRank(userId);

return NextResponse.json({
data: {
userLeaderboardRank: rankData,
},
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fragile query routing — relies on case-sensitive substring collisions and wrong branch order.

Dispatching by query.includes(...) over the raw GraphQL document is brittle:

  • The leaderboard branch is checked first and falls back to a very broad lowercase "leaderboard" match. It only fails to swallow the UserLeaderboardRank query by the accident that JS .includes is case-sensitive ("userLeaderboardRank" has a capital L, so it doesn't contain lowercase "leaderboard"). Any future rename, comment, fragment, or introspection query containing the word "leaderboard" (e.g. in a description or selection alias) will be misrouted into the leaderboard handler and never reach the rank handler.
  • Either swap the checks (most specific first — UserLeaderboardRank before Leaderboard) or, preferably, parse the operation name once and switch on it.
♻️ Suggested fix — check the more specific operation first and match the operation name directly
-    // Simple mock GraphQL server handling the leaderboard queries
-    if (query.includes("query Leaderboard") || query.includes("leaderboard")) {
-      const { filters, pagination } = variables || {};
-      const page = pagination?.page || 1;
-      const limit = pagination?.limit || 20;
-      const tier = filters?.tier;
-
-      const mockData = getMockLeaderboard(page, limit, tier);
-
-      return NextResponse.json({
-        data: {
-          leaderboard: {
-            entries: mockData.data.map((entry, index) => ({
-              rank: (page - 1) * limit + index + 1,
-              contributor: entry,
-            })),
-            totalCount: mockData.total,
-          },
-        },
-      });
-    }
-
-    if (
-      query.includes("query UserLeaderboardRank") ||
-      query.includes("userLeaderboardRank")
-    ) {
-      const { userId } = variables || {};
-      const rankData = getMockUserRank(userId);
-
-      return NextResponse.json({
-        data: {
-          userLeaderboardRank: rankData,
-        },
-      });
-    }
+    // Match by operation name; check the more specific operation first.
+    const operationMatch = /\b(?:query|mutation)\s+(\w+)/.exec(query);
+    const operationName = operationMatch?.[1];
+
+    if (operationName === "UserLeaderboardRank") {
+      const { userId } = variables || {};
+      const rankData = getMockUserRank(userId);
+      return NextResponse.json({
+        data: { userLeaderboardRank: rankData },
+      });
+    }
+
+    if (operationName === "Leaderboard") {
+      const { filters, pagination } = variables || {};
+      const page = pagination?.page || 1;
+      const limit = pagination?.limit || 20;
+      const tier = filters?.tier;
+
+      const mockData = getMockLeaderboard(page, limit, tier);
+      return NextResponse.json({
+        data: {
+          leaderboard: {
+            entries: mockData.data.map((entry, index) => ({
+              rank: (page - 1) * limit + index + 1,
+              contributor: entry,
+            })),
+            totalCount: mockData.total,
+          },
+        },
+      });
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/graphql/route.ts` around lines 10 - 43, The current routing based on
query.includes(...) is brittle and misorders branches; update the handler to
derive the GraphQL operation name once (e.g., extract the operation name from
the `query` string via a simple regex like
/^\s*(query|mutation)\s+([A-Za-z0-9_]+)/ or by using a lightweight parser) and
switch on that name instead of substring checks, then route to
getMockUserRank(userId) when the operation name === "UserLeaderboardRank" and to
getMockLeaderboard(page, limit, tier) when the operation name === "Leaderboard";
alternatively, if you want the smallest change, simply check the more specific
`UserLeaderboardRank` branch (calling getMockUserRank) before the broader
`leaderboard` branch to avoid accidental matches.

Comment on lines +70 to +96
useEffect(() => {
if (
hasScrolledRef.current ||
!currentUserId ||
!currentUserRowRef.current
) {
return;
}

return (
<TableRow
key={entry.contributor.id}
className={cn(
"border-b border-border/60 hover:bg-muted/20",
isCurrentUser && "bg-secondary/40",
onRowClick && "cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary focus:z-10 relative"
)}
tabIndex={onRowClick ? 0 : undefined}
role={onRowClick ? "row" : undefined}
onClick={onRowClick ? () => onRowClick(entry) : undefined}
onKeyDown={onRowClick ? (e) => handleKeyDown(e, entry) : undefined}
>
<TableCell className="text-center font-medium">
<div className="flex justify-center">
<RankBadge rank={entry.rank} />
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-3">
<Avatar className="h-9 w-9 border border-border/60">
<AvatarImage src={entry.contributor.avatarUrl || undefined} />
<AvatarFallback className="bg-secondary text-secondary-foreground">{entry.contributor.displayName[0]}</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<span className={cn("font-semibold text-foreground", isCurrentUser && "text-primary")}>
{entry.contributor.displayName}
{isCurrentUser && " (You)"}
</span>
<div className="flex gap-1 md:hidden">
<span className="text-xs text-muted-foreground">{entry.contributor.tier}</span>
</div>
<div className="flex gap-1 mt-1 md:hidden">
{entry.contributor.topTags.slice(0, 3).map(tag => (
<span key={tag} className="text-[10px] bg-muted px-1 rounded">{tag}</span>
))}
</div>
</div>
</div>
{/* Desktop tags */}
<div className="hidden md:flex gap-1 mt-2">
{entry.contributor.topTags.slice(0, 3).map(tag => (
<Badge key={tag} variant="secondary" className="text-[10px] px-1 h-5 font-normal">
{tag}
</Badge>
))}
</div>
</TableCell>
<TableCell className="hidden md:table-cell text-foreground">
<TierBadge tier={entry.contributor.tier} />
</TableCell>
<TableCell className="text-right font-mono text-foreground font-medium">
{entry.contributor.totalScore.toLocaleString()}
</TableCell>
<TableCell className="text-right hidden sm:table-cell text-foreground">
{entry.contributor.stats.totalCompleted}
</TableCell>
<TableCell className="text-right hidden lg:table-cell font-mono text-foreground">
${entry.contributor.stats.totalEarnings.toLocaleString()}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end">
<StreakBadge streak={entry.contributor.stats.currentStreak} />
</div>
</TableCell>
</TableRow>
);
})}
{isFetchingNextPage && (
<TableRow>
<TableCell colSpan={7} className="text-center py-4">
<div className="flex items-center justify-center text-muted-foreground text-sm">
Loading more...
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
{hasNextPage && <div ref={loadMoreRef} className="h-4" />}
</div>
const userEntry = entries.find(
(e) => e.contributor.userId === currentUserId,
);
const userRank = userEntry?.rank ?? 0;

if (userRank > 10) {
currentUserRowRef.current.scrollIntoView({
behavior: "smooth",
// "nearest" scrolls the minimum distance needed — avoids
// aggressively snapping the hero header offscreen.
block: "nearest",
});
}

// Mark as done regardless of whether we scrolled, so future
// re-renders (e.g. next-page loads) don't re-trigger.
hasScrolledRef.current = true;
}, [currentUserId, entries]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Auto-scroll won't re-trigger when filters change.

hasScrolledRef is only set to true (it's never reset), so once the user has been scrolled to once, switching timeframe/tier/tags produces a brand-new entries list whose new row position will not be auto-scrolled to. If that's intentional (avoid jarring jumps on filter changes), consider documenting it; otherwise reset the flag when the filter-driven entries identity changes.

♻️ Optional fix: reset on filter-driven entry-set changes
   useEffect(() => {
     if (
       hasScrolledRef.current ||
       !currentUserId ||
       !currentUserRowRef.current
     ) {
       return;
     }
     // ...
   }, [currentUserId, entries]);
+
+  // Re-arm the one-shot scroll when the underlying dataset is replaced
+  // (e.g. user changes timeframe/tier/tags filters).
+  // Pass a `resetKey` prop derived from filters from the parent, or compare
+  // entries[0]?.contributor.id to a previous ref.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/leaderboard/leaderboard-table.tsx` around lines 70 - 96, The
effect currently sets hasScrolledRef.current = true and never resets it, so
after the first auto-scroll future filter changes (which produce new entries)
won't re-trigger scrolling; update the component to reset hasScrolledRef.current
to false whenever the filter-driven entry set changes (e.g., when timeframe,
tier, or tags change or when entries identity changes) so the useEffect that
reads hasScrolledRef, currentUserId, currentUserRowRef and entries can run the
scroll logic again; locate where filters or entries are updated and set
hasScrolledRef.current = false there (or include the specific filter values in
the useEffect dependency array and reset the ref at the start).

Comment on lines +80 to +85
{/* Link wraps Button to preserve correct anchor semantics */}
<Link href={ROUTES.AUTH} className="w-full">
<Button variant="default" size="sm" className="w-full">
Sign In
</Button>
</Link>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the project's Button component supports `asChild` (shadcn convention via Radix Slot).
fd -t f 'button.tsx' components/ui | xargs rg -nP -C2 '\basChild\b|@radix-ui/react-slot|Slot\s*[:=]'

Repository: boundlessfi/bounties

Length of output: 531


Invalid HTML / a11y: nested interactive elements (<a><button>).

Link renders an anchor and Button renders a <button>, producing <a><button>Sign In</button></a>. Per the HTML spec, interactive content must not contain other interactive content; this also causes inconsistent focus and announcement behavior across assistive tech. Use the shadcn asChild pattern to render a single <a> with button styling.

🛠️ Proposed fix
-          {/* Link wraps Button to preserve correct anchor semantics */}
-          <Link href={ROUTES.AUTH} className="w-full">
-            <Button variant="default" size="sm" className="w-full">
-              Sign In
-            </Button>
-          </Link>
+          {/* Render a single <a> styled as a button via Radix Slot (asChild). */}
+          <Button asChild variant="default" size="sm" className="w-full">
+            <Link href={ROUTES.AUTH}>Sign In</Link>
+          </Button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/leaderboard/user-rank-sidebar.tsx` around lines 80 - 85, The
current JSX nests a <Button> inside a <Link> which creates invalid HTML
(<a><button>) — change to use the shadcn asChild pattern by rendering the Link
as the Button's child: wrap the Link (href={ROUTES.AUTH}) inside <Button asChild
variant="default" size="sm" className="w-full"> so the Button forwards its
styling/behavior to the anchor element and you end up with a single interactive
<a> element; preserve the w-full classes and Sign In label when moving Link
inside Button.

@Benjtalkshow

Copy link
Copy Markdown
Contributor

@TheBigWealth89
Can you link the issue you are working on here?

@TheBigWealth89

Copy link
Copy Markdown
Author

What do you mean?

@Benjtalkshow

Copy link
Copy Markdown
Contributor

What do you mean?

Kindly Tag the issue number here using #issueNumber

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.

2 participants