Skip to content

Trending to leaderboard#1739

Merged
simo6529 merged 8 commits intomainfrom
trending-to-leaderboard
Jan 14, 2026
Merged

Trending to leaderboard#1739
simo6529 merged 8 commits intomainfrom
trending-to-leaderboard

Conversation

@simo6529
Copy link
Copy Markdown
Collaborator

@simo6529 simo6529 commented Jan 14, 2026

Summary by CodeRabbit

  • New Features

    • Added a "Hot" badge to the wave leaderboard header.
    • Introduced a new "Trend" sort option for leaderboard sorting.
  • Improvements

    • Optimized leaderboard polling, prefetching, and data refresh behavior for more reliable and efficient updates.
    • Simplified sort handling and loading state logic.
  • Tests

    • Updated leaderboard tests to reflect label and interaction text changes.
  • Chores

    • Removed Mint metrics schemas from the public API spec and added production env to worktree copy.

✏️ Tip: You can customize this high-level summary in your review settings.

Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 14, 2026

📝 Walkthrough

Walkthrough

Added a TREND sort and Hot badge to the leaderboard header, refactored prefetch and polling into simplified single-page prefetch and deferred timeout-based polling, standardized test formatting, and included .env.production in worktree copy configuration. No exported API types were removed except OpenAPI schemas.

Changes

Cohort / File(s) Summary
Tests
__tests__/components/waves/leaderboard/header/WaveleaderboardSort.test.tsx
Standardized imports and string literals to double quotes; adjusted test labels/descriptions (e.g., "Current Vote", "Projected Vote", "🔥 Hot", "Newest") without behavioral change.
Leaderboard Component
components/waves/leaderboard/header/WaveleaderboardSort.tsx
Added TREND button and Hot badge UI; added useEffect cleanup for debounced prefetch; simplified prefetch to single-page pattern with updated getNextPageParam handling and SORT_DIRECTION_MAP typing; updated styling and hover prefetch triggers.
Leaderboard Hook
hooks/useWaveDropsLeaderboard.ts
Added TREND enum value and tightened SORT_DIRECTION_MAP types; removed redundant local state in favor of derived state via useMemo; replaced event-driven polling with a polling query + deferred timeout refetch flow; normalized pageParam checks; added unmount cleanup for polling timeout; adjusted isFetching semantics.
Config
scripts/worktree/copy.conf
Added .env.production to files copied when creating a worktree.
OpenAPI
openapi.yaml
Removed ApiMintMetrics and ApiMintMetricsPage schemas from components/schemas.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Component as WaveleaderboardSort
    participant Hook as useWaveDropsLeaderboard
    participant Query as QueryClient/Fetcher
    participant Polling as PollingQuery

    User->>Component: hover / mouseenter on TREND
    Component->>Hook: call debounced prefetch
    Hook->>Query: prefetchInfiniteQuery (single-page TREND)
    Query-->>Hook: prefetch complete

    User->>Component: change sort selection
    Component->>Hook: set sort
    Hook->>Polling: start/configure polling query (select -> haveNewDrops)
    Polling->>Query: poll endpoint
    Query-->>Polling: poll results
    Polling->>Hook: detect new drops -> set timeout for refetch
    Hook->>Query: refetch (on timeout)
    Query-->>Hook: updated pages
    Hook-->>Component: provide updated drops / isFetching
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • GelatoGenesis
  • ragnep

Poem

🐰 A TREND button gleams with a hop and a wink,
Hot badges flicker where leaderboards blink.
Debounced prefetch, polling with care,
Memoized drops spring from thin air.
🥕✨

🚥 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 'Trending to leaderboard' accurately reflects the main changes in the pull request, which add trending functionality to the leaderboard UI component.

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



📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cde87ce and 47da04a.

📒 Files selected for processing (1)
  • openapi.yaml
💤 Files with no reviewable changes (1)
  • openapi.yaml
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (javascript-typescript)

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


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.

Copy link
Copy Markdown

@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: 0

Caution

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

⚠️ Outside diff range comments (3)
components/waves/leaderboard/header/WaveleaderboardSort.tsx (1)

126-163: Add type="button" on buttons to prevent accidental form submission.

Proposed fix
-      <button
+      <button
+        type="button"
         className={getButtonClassName(WaveDropsLeaderboardSort.TREND)}
         onClick={() => onSortChange(WaveDropsLeaderboardSort.TREND)}
         onMouseEnter={() => prefetchSort(WaveDropsLeaderboardSort.TREND)}
       >
         🔥 Hot
       </button>
       <button
+        type="button"
         className={getButtonClassName(WaveDropsLeaderboardSort.RANK)}
         onClick={() => onSortChange(WaveDropsLeaderboardSort.RANK)}
         onMouseEnter={() => prefetchSort(WaveDropsLeaderboardSort.RANK)}
       >
         Current Vote
       </button>
       <button
+        type="button"
         className={getButtonClassName(
           WaveDropsLeaderboardSort.RATING_PREDICTION
         )}
         onClick={() => onSortChange(WaveDropsLeaderboardSort.RATING_PREDICTION)}
         onMouseEnter={() =>
           prefetchSort(WaveDropsLeaderboardSort.RATING_PREDICTION)
         }
       >
         Projected Vote
       </button>
       <button
+        type="button"
         className={getButtonClassName(WaveDropsLeaderboardSort.CREATED_AT)}
         onClick={() => onSortChange(WaveDropsLeaderboardSort.CREATED_AT)}
         onMouseEnter={() => prefetchSort(WaveDropsLeaderboardSort.CREATED_AT)}
       >
         Newest
       </button>
openapi.yaml (1)

5739-5822: ApiCommunityMetrics now has new required fields—verify backend always returns them (breaking change otherwise).

hooks/useWaveDropsLeaderboard.ts (1)

226-266: Three correctness and lifecycle bugs need fixing: missing auth gate on polling, wrong "newest" detection for non-CREATED_AT sorts, and untracked setTimeout side-effects.

  • Missing connectedProfileHandle gate: Polling query (line 257) has enabled: canPoll && !pausePolling but main infinite query (line 165) gates with !!connectedProfileHandle. Polling will execute API calls even when no user is authenticated.

  • Wrong "newest" detection for RANK/TREND sorts: The checkForNewDrops function (lines 206-224) compares drops[0].created_at against pollingData.drops[0].created_at, but when sort is RANK or TREND (not CREATED_AT), the first item isn't newest by time. This causes incorrect haveNewDrops detection and failed auto-refetch triggers. The polling query must either always use CREATED_AT sort, or compare against the actual newest drop by timestamp (computed across all current drops, not just position 0).

  • Untracked setTimeout side-effects: The setTimeout(refetch(), POLLING_DELAY) in queryFn (lines 247-252) will execute on every polling interval. Multiple scheduled timeouts accumulate if the query refetches before previous timeout fires. They are never cleaned up on unmount (current cleanup at lines 268-274 only removes queries), allowing refetch calls to fire after component unmount.

Proposed fix
-import { useCallback, useEffect, useState, useMemo } from "react";
+import { useCallback, useEffect, useRef, useState, useMemo } from "react";
@@
 export function useWaveDropsLeaderboard({
@@
 }: UseWaveDropsLeaderboardProps) {
@@
   const [canPoll, setCanPoll] = useState(false);
   const isTabVisible = useTabVisibility();
+  const refetchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@
   const canAutoRefetch = isTabVisible && !hasTempDrop;
@@
   const checkForNewDrops = useCallback(
     (pollingData: ApiDropsLeaderboardPage): boolean => {
       if (pollingData.drops.length === 0) return false;
 
       const latestPolledDrop = pollingData.drops[0];
-      const newestExistingDrop = drops[0];
+      const newestExistingCreatedAt = drops.reduce((max, d) => {
+        const t = new Date(d.created_at).getTime();
+        return Number.isFinite(t) ? Math.max(max, t) : max;
+      }, 0);
 
-      if (latestPolledDrop && newestExistingDrop) {
+      if (latestPolledDrop && newestExistingCreatedAt > 0) {
         const polledCreatedAt = new Date(latestPolledDrop.created_at).getTime();
-        const existingCreatedAt = new Date(
-          newestExistingDrop.created_at
-        ).getTime();
-        return polledCreatedAt > existingCreatedAt;
+        return polledCreatedAt > newestExistingCreatedAt;
       }
 
       return true;
     },
     [drops]
   );
@@
   const { data: haveNewDrops = false } = useQuery({
@@
       if (sortDirection) {
         params["sort_direction"] = sortDirection;
       }
@@
-      if (canAutoRefetch && checkForNewDrops(result)) {
-        // Use setTimeout to defer the refetch slightly and avoid React Query batching issues
-        setTimeout(() => {
-          refetch();
-        }, POLLING_DELAY);
+      if (
+        canAutoRefetch &&
+        checkForNewDrops(result) &&
+        !refetchTimeoutRef.current
+      ) {
+        refetchTimeoutRef.current = setTimeout(() => {
+          refetchTimeoutRef.current = null;
+          refetch();
+        }, POLLING_DELAY);
       }
@@
     select: checkForNewDrops,
-    enabled: canPoll && !pausePolling,
+    enabled: canPoll && !!connectedProfileHandle && !pausePolling,
@@
   });
+
+  useEffect(() => {
+    return () => {
+      if (refetchTimeoutRef.current) {
+        clearTimeout(refetchTimeoutRef.current);
+        refetchTimeoutRef.current = null;
+      }
+    };
+  }, []);
🧹 Nitpick comments (2)
hooks/useWaveDropsLeaderboard.ts (1)

105-134: Prefetch effect dependencies/error handling: consider tightening.

This effect uses queryClient, queryKey, sortDirection, getNextPageParam but only depends on [waveId, sort]; also prefetchInfiniteQuery(...) isn’t awaited/caught here (unlike the component).

openapi.yaml (1)

333-371: New /community-metrics/mints endpoint looks reasonable; consider adding pagination constraints/defaults for consistency.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between db5f95c and a4c93ca.

⛔ Files ignored due to path filters (4)
  • generated/models/ApiCommunityMetrics.ts is excluded by !**/generated/**
  • generated/models/ApiMintMetrics.ts is excluded by !**/generated/**
  • generated/models/ApiMintMetricsPage.ts is excluded by !**/generated/**
  • generated/models/ObjectSerializer.ts is excluded by !**/generated/**
📒 Files selected for processing (5)
  • __tests__/components/waves/leaderboard/header/WaveleaderboardSort.test.tsx
  • components/waves/leaderboard/header/WaveleaderboardSort.tsx
  • hooks/useWaveDropsLeaderboard.ts
  • openapi.yaml
  • scripts/worktree/copy.conf
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (8)
scripts/worktree/copy.conf (1)

11-11: Verify that copying production secrets to worktrees is intentional.

Adding .env.production ensures production environment variables are available in worktrees, but this also means production secrets (API keys, credentials) will be copied to each worktree. Confirm this aligns with your security practices—particularly if worktrees are used for local development or testing where production credentials shouldn't be active.

__tests__/components/waves/leaderboard/header/WaveleaderboardSort.test.tsx (1)

1-38: Tests align with new TREND/“🔥 Hot” UI.

components/waves/leaderboard/header/WaveleaderboardSort.tsx (3)

21-30: SORT_DIRECTION_MAP update (TREND: "DESC") looks correct.


104-114: Debounce cancel on unmount is a solid guardrail.


39-103: No issues found. prefetchInfiniteQuery in @tanstack/react-query v5.45.1 (your repo's version) fully supports both initialPageParam and pages options. The usage here is correct: initialPageParam: null is valid, and pages: 1 specifies that exactly one page should be prefetched (which is also the default behavior). No type mismatches or silent no-ops will occur.

hooks/useWaveDropsLeaderboard.ts (2)

23-47: TREND enum + sort-direction mapping are consistent with the UI.


170-224: Derived drops via useMemo is a nice simplification.

openapi.yaml (1)

4279-4330: Adding TREND to wave leaderboard sort enum matches frontend changes.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Signed-off-by: Simo <simo@6529.io>
Signed-off-by: Simo <simo@6529.io>
Copy link
Copy Markdown

@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: 0

Caution

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

⚠️ Outside diff range comments (1)
hooks/useWaveDropsLeaderboard.ts (1)

280-283: Add cleanup for QueryKey.DROPS_LEADERBOARD in the unmount effect.

The hook queries with QueryKey.DROPS_LEADERBOARD (line 82) but only removes QueryKey.DROPS on unmount. The cleanup should also remove the hook's own query key. Consider removing both keys to match the pattern in ReactQueryWrapper.invalidateDrops() (lines 918–925), which invalidates both together, or at minimum ensure DROPS_LEADERBOARD is cleaned up when this component unmounts.

🧹 Nitpick comments (3)
hooks/useWaveDropsLeaderboard.ts (3)

209-228: Unreachable return true on line 225 may cause unintended refetches if reached.

The return true at line 225 appears unreachable: the early return at line 211 ensures pollingData.drops.length > 0, so pollingData.drops[0] will always be defined. If this line is somehow reached due to an unexpected falsy value in the array, returning true would trigger unnecessary refetches.

Consider returning false for safety, or removing the dead code path entirely.

Suggested fix
       if (latestPolledDrop) {
         const polledCreatedAt = new Date(latestPolledDrop.created_at).getTime();
         return polledCreatedAt > newestExistingTimestamp;
       }

-      return true;
+      return false;
     },

247-258: Side effects in queryFn is an anti-pattern and may cause stale closure issues.

Triggering refetch() inside queryFn violates React Query's expected usage pattern where query functions should be pure data-fetching operations. Additionally, canAutoRefetch and checkForNewDrops are captured in the closure at query creation time and may become stale.

Consider moving the refetch logic to a useEffect that reacts to haveNewDrops:

Suggested refactor
  const { data: haveNewDrops = false } = useQuery({
    queryKey: [...queryKey, "polling"],
    queryFn: async () => {
      const params: Record<string, string> = {
        page_size: "1",
        sort: WaveDropsLeaderboardSort.CREATED_AT,
        sort_direction: "DESC",
      };

-      const result = await commonApiFetch<ApiDropsLeaderboardPage>({
+      return await commonApiFetch<ApiDropsLeaderboardPage>({
        endpoint: `waves/${waveId}/leaderboard`,
        params,
      });
-
-      // Trigger refetch directly in the query callback when conditions are met
-      // This replaces the effect-based approach
-      if (canAutoRefetch && checkForNewDrops(result)) {
-        // Clear any existing timeout to prevent accumulation
-        if (pollingTimeoutRef.current) {
-          clearTimeout(pollingTimeoutRef.current);
-        }
-        // Use setTimeout to defer the refetch slightly and avoid React Query batching issues
-        pollingTimeoutRef.current = setTimeout(() => {
-          refetch();
-        }, POLLING_DELAY);
-      }
-
-      return result;
    },
    select: checkForNewDrops,
    // ...rest of options
  });
+
+  // Trigger refetch when new drops are detected
+  useEffect(() => {
+    if (haveNewDrops && canAutoRefetch) {
+      if (pollingTimeoutRef.current) {
+        clearTimeout(pollingTimeoutRef.current);
+      }
+      pollingTimeoutRef.current = setTimeout(() => {
+        refetch();
+      }, POLLING_DELAY);
+    }
+  }, [haveNewDrops, canAutoRefetch, refetch]);

134-134: Consider adding derived values to the dependency array for lint compliance.

queryKey, sortDirection, and getNextPageParam are used inside the effect but not listed in dependencies. While they're derived from waveId and sort, exhaustive-deps lint rules would flag this.

Suggested fix
-  }, [waveId, sort]);
+  }, [waveId, sort, queryKey, sortDirection, getNextPageParam, queryClient]);
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a4c93ca and cde87ce.

📒 Files selected for processing (1)
  • hooks/useWaveDropsLeaderboard.ts
🧰 Additional context used
🧬 Code graph analysis (1)
hooks/useWaveDropsLeaderboard.ts (1)
generated/models/ApiDropsLeaderboardPage.ts (1)
  • ApiDropsLeaderboardPage (18-67)
🔇 Additional comments (8)
hooks/useWaveDropsLeaderboard.ts (8)

3-3: LGTM!

Import additions (useMemo, useRef) align with the new derived state pattern and polling timeout management.


23-29: LGTM!

TREND sort option is properly added to the enum.


38-47: LGTM!

The explicit Record<WaveDropsLeaderboardSort, ...> typing ensures compile-time safety when new sort options are added, and TREND correctly maps to DESC.


171-192: LGTM!

Clean derived state pattern using useMemo. Eliminates the need for synchronizing state with useEffect, and the dependency array correctly includes data and sort.


194-195: LGTM!

Simple derived boolean from data?.pages is cleaner than tracking initialization state separately.


199-204: LGTM!

Smart guard against refetching during optimistic updates by checking for temp drops.


296-296: LGTM!

isFetching || !hasInitialized correctly shows loading state during initial fetch, simplifying the previous approach.


119-121: LGTM!

Consistent typeof pageParam === "number" check correctly handles the number | null type from initialPageParam: null.

Also applies to: 155-157

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@sonarqubecloud
Copy link
Copy Markdown

@simo6529 simo6529 merged commit 09758d9 into main Jan 14, 2026
7 checks passed
@simo6529 simo6529 deleted the trending-to-leaderboard branch January 14, 2026 16:14
This was referenced Jan 15, 2026
This was referenced Feb 14, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Apr 13, 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.

2 participants