Skip to content

Conversation

@hyochan
Copy link
Owner

@hyochan hyochan commented Nov 28, 2025

Summary by CodeRabbit

  • New Features

    • Leaderboards: tier-based filtering, top-tier users carousel, and infinite scrolling
    • Date period picker to select/reset month for 1-year stats
    • Monthly contribution line charts and radar visualizations on stats pages
  • UI/UX Updates

    • "Recent List" renamed to "Leaderboards" with updated labels and hints
    • Improved responsive layouts, header/link behavior, and horizontal scrolling for lists
    • Loading states and clearer interactive controls
  • Documentation

    • Added project coding and Tailwind styling guidelines

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

@vercel
Copy link

vercel bot commented Nov 28, 2025

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

Project Deployment Preview Comments Updated (UTC)
github-stats Ready Ready Preview Comment Nov 28, 2025 5:04pm

@hyochan hyochan added the 💎 feature New features label Nov 28, 2025
@coderabbitai
Copy link

coderabbitai bot commented Nov 28, 2025

Warning

Rate limit exceeded

@hyochan has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 18 minutes and 49 seconds before requesting another review.

⌛ 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 4c6db0e and 2881646.

📒 Files selected for processing (2)
  • app/[lang]/leaderboards/GithubUserList.tsx (1 hunks)
  • app/[lang]/stats/[login]/SearchTextInput.tsx (4 hunks)

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds a leaderboards feature with tier filtering and top-tier carousel, introduces monthly contribution fetching and charts for stats with an endDate selector, adds tier-related API endpoints, extends GitHub service types and date-range parameters, adjusts layouts/styles, and updates localization and documentation.

Changes

Cohort / File(s) Summary
Documentation & Dependency
CLAUDE.md, package.json
Adds project guidelines document and adds recharts (^3.5.1) dependency.
Leaderboards UI
app/[lang]/leaderboards/*, app/[lang]/leaderboards/page.tsx, app/[lang]/leaderboards/GithubUserList.tsx, app/[lang]/leaderboards/TopTierUsers.tsx, app/[lang]/leaderboards/apiRoutes.ts
New leaderboards page, GithubUserList with tier filtering and infinite scroll, TopTierUsers carousel, and exported API route constants (API_USERS_BY_TIER, API_TOP_TIER_USERS).
Recent-List Enhancement
app/[lang]/recent-list/GithubUserList.tsx
Adds tier filter UI and logic to existing recent-list component, toggling between recent data and tier-specific results.
Stats - UI Components
app/[lang]/stats/[login]/MonthPicker.tsx, app/[lang]/stats/[login]/Scouter/StatsDetails/StatsChart.tsx, app/[lang]/stats/[login]/Scouter/StatsDetails/StatsRadar.tsx
Adds client MonthPicker, interactive LineChart (StatsChart), and RadarChart (StatsRadar) components for monthly and plugin stats visualization.
Stats - Integration & Props
app/[lang]/stats/[login]/page.tsx, app/[lang]/stats/[login]/SearchTextInput.tsx, app/[lang]/stats/[login]/Scouter/index.tsx, app/[lang]/stats/[login]/Scouter/StatsHeader.tsx, app/[lang]/stats/[login]/Scouter/StatsDetails/index.tsx, app/[lang]/stats/[login]/Scouter/StatsDetails/SectionDooboo.tsx, app/[lang]/stats/[login]/Scouter/StatsDetails/SectionPeople.tsx, app/[lang]/stats/[login]/Scouter/Container.tsx
Threads endDate through page → Scouter → StatsDetails → SectionDooboo; fetches monthly contributions in parallel; refactors SearchTextInput to use router navigation and adds layout width constraints.
API Endpoints
pages/api/users-by-tier.ts, pages/api/top-tier-users.ts, pages/api/github-stats.ts, pages/api/github-stats-advanced.ts, pages/api/github-trophies.ts
Adds /api/users-by-tier and /api/top-tier-users endpoints; extends existing stats endpoints to accept and forward startDate parameter to service layer.
Server GitHub Service
server/services/githubService.ts
Adds monthly contribution fetching, MonthlyContribution type and getMonthlyContribution export; extends getGithubUser, getDoobooStats, and upsertGithubStats to accept optional startDate.
Server Types & Plugins
server/plugins/types/UserGraph.ts, server/plugins/stats/fire.ts, server/plugins/stats/gold.ts
Adds ContributionCalendar types and augments ContributionsCollection; refactors repo/PR mappings for null-safety and owner extraction.
Layout, Header & Footer
app/[lang]/layout.tsx, app/[lang]/(common)/Header/index.tsx, app/[lang]/(home)/SectionFooter.tsx
Adds min-w-0/w-full and overflow-x-hidden classes to constrain layout; updates Header active-path detection and renames navigation path to leaderboards; updates a social link target.
Styling
app/[lang]/styles.module.css, styles/output.css, styles/root.css
Adds .horizontalScroll and scrollbar rules, expands utility CSS with responsive/dark-mode classes, and adds spin keyframes.
Localization
locales/en.json, locales/ko.json
Renames recentListleaderboards, adds tier and stats period keys (selectPeriod, periodDescription, resetToDefault, search), and updates search hint text.
Minor UI Tweaks
app/[lang]/stats/page.tsx, app/[lang]/stats/[login]/Scouter/StatsHeader.tsx, app/[lang]/stats/[login]/Scouter/StatsDetails/index.tsx
Small text and layout class adjustments (width constraints, class splitting).

Sequence Diagrams

sequenceDiagram
    actor User
    participant Page as Stats Page\n[login]/page.tsx
    participant GithubService as GithubService
    participant GitHub as GitHub GraphQL
    participant Scouter as Scouter Component
    participant MonthPicker as MonthPicker
    participant StatsChart as StatsChart

    User->>Page: Open stats (optional endDate)
    Page->>GithubService: getDoobooStats(login, startDate)
    Page->>GithubService: getMonthlyContribution(login, startDate)
    par Parallel GitHub queries
        GithubService->>GitHub: Query aggregated stats
        GithubService->>GitHub: Query monthly contributions
    end
    GitHub-->>GithubService: Responses
    GithubService-->>Page: Stats + MonthlyContribution[]
    Page->>Scouter: Render with statsWithMonthly & endDate
    Scouter->>StatsChart: supply monthlyContributions
    Scouter->>MonthPicker: supply value (endDate)
    User->>MonthPicker: Select month → onChangeAction
    MonthPicker->>Scouter: onChangeAction(newEndDate)
    Scouter->>Page: push endDate to URL (router)
    StatsChart-->>User: Render interactive chart
Loading
sequenceDiagram
    actor User
    participant Page as Leaderboards Page
    participant TopTier as TopTierUsers
    participant List as GithubUserList
    participant API_Top as /api/top-tier-users
    participant API_Tier as /api/users-by-tier
    participant Supabase as Supabase

    User->>Page: Visit leaderboards
    Page->>TopTier: mount
    TopTier->>API_Top: GET
    API_Top->>Supabase: query top 5 plugin users
    Supabase-->>API_Top: results
    API_Top-->>TopTier: PluginUser[]
    TopTier-->>User: show carousel

    Page->>List: render recent list (initial data)
    User->>List: click tier button (e.g., Gold)
    List->>API_Tier: GET ?tier=Gold
    API_Tier->>Supabase: query users within tier score range
    Supabase-->>API_Tier: filtered users
    API_Tier-->>List: PluginUser[]
    List-->>User: display tier users
    User->>List: click "All"
    List-->>User: reset to recent data
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Focus review on:
    • server/services/githubService.ts (date-window logic, monthly aggregation, new types, and caching behavior)
    • pages/api/users-by-tier.ts and pages/api/top-tier-users.ts (tiers scoring logic, query limits, error cases)
    • UI prop threading for endDate (page → Scouter → StatsDetails → SectionDooboo → MonthPicker)
    • Leaderboards client components (tier fetch, infinite scroll, deduplication)
    • Null-safety changes in server plugins (owner extraction/filtering)

Poem

🐰
I dug through data, month by month and tier by tier,
I painted charts that wiggle and badges that cheer,
A carrot for top users, a hop for each date,
Now leaderboards sparkle — come celebrate! 🥕✨

Pre-merge checks and finishing touches

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: stats and leaderboard renewal' clearly and concisely describes the main objective of the changeset, which involves substantial updates to the stats and leaderboard features.

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.

@gemini-code-assist
Copy link

Summary of Changes

Hello @hyochan, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request delivers a significant upgrade to the application's user statistics and leaderboard functionalities. It introduces advanced data visualization tools and flexible filtering options, allowing users to explore performance metrics with greater detail and interactivity. The changes are supported by robust backend enhancements for data retrieval and a refined user interface that adapts seamlessly to various devices.

Highlights

  • Enhanced User Statistics Page: The user statistics page now includes a monthly contribution line chart and a radar chart for overall stats, providing a more comprehensive visual overview of a user's activity and performance. Users can also filter stats by a specific month.
  • Revamped Leaderboard with Tier Filtering: The leaderboard has been updated to include a list of top-ranked users and introduces dynamic tier-based filtering, allowing users to view and compare performance within specific tiers.
  • Dynamic Data Fetching and Caching: Backend services and API endpoints have been modified to support fetching monthly contribution data and filtering user statistics by a specified date range. A conditional caching mechanism ensures that only default, non-filtered data is persisted to the database.
  • Improved UI/UX and Responsiveness: Various UI components have been refined for better layout and responsiveness across different screen sizes. This includes updates to headers, footers, and the introduction of horizontal scrolling for certain elements. Loading states and spinners have been added for a smoother user experience.
  • New Code Guidelines Document: A new markdown file, CLAUDE.md, has been added to document code guidelines, including development commands, project structure, styling conventions (Tailwind color system, typography, dark mode), and component best practices.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This is an impressive pull request that significantly revamps the stats and leaderboard functionality. The introduction of interactive charts with recharts, tier-based filtering on the leaderboard, and a date picker for stats are all excellent feature additions that greatly enhance the user experience. The backend changes, including new API endpoints and updates to the GitHub data fetching logic to support date ranges and monthly contributions, are well-implemented. I particularly appreciate the thoughtful approach to caching, where you only update the database for default data fetches, preventing filtered views from overwriting the main cache. My feedback is focused on improving code consistency by adhering to the project's own styling guidelines, enhancing type safety, and removing some minor code duplications and magic numbers. Overall, this is a high-quality contribution.

#### Brand/Accent Colors
```tsx
// Primary brand color (blue)
className="bg-brand" // #4190EB

Choose a reason for hiding this comment

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

medium

It's great that you've added this coding guidelines file! To improve clarity, you could add a comment explaining the purpose of the bg-brand color, similar to how you've done for other colors like "Primary buttons" or "Status colors". This helps developers understand when to use it.

Suggested change
className="bg-brand" // #4190EB
// Primary brand color (blue) - used for primary actions and highlights
className="bg-brand" // #4190EB

Comment on lines 179 to 181
stargazers {
totalCount
}

Choose a reason for hiding this comment

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

medium

Removing the owner field from this GraphQL query is a good optimization since it doesn't appear to be used. This helps to reduce the payload size from the GitHub API.

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: 4

Caution

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

⚠️ Outside diff range comments (4)
app/[lang]/(home)/SectionFooter.tsx (1)

35-39: Remove stray quote in Tailwind class string

'md:p-0"' includes an extra " at the end, so the class becomes md:p-0" and Tailwind likely won’t apply the intended md:p-0 utility. Drop the trailing quote inside the string.

-          'flex flex-col justify-center items-center',
-          'md:p-0"',
+          'flex flex-col justify-center items-center',
+          'md:p-0',
app/[lang]/recent-list/GithubUserList.tsx (1)

144-144: Reference comparison in includes() will not filter duplicates correctly.

data.includes(el) performs reference equality, not value comparison. Since users from the API are new objects, they will never match existing objects in data, causing duplicates when properties match.

-      const filteredUsers = users.filter((el) => !data.includes(el));
+      const filteredUsers = users.filter(
+        (el) => !data.some((existing) => existing.githubId === el.githubId),
+      );
pages/api/github-trophies.ts (1)

25-68: Wrap case block in braces to scope the variable declaration.

The static analysis correctly identifies that const startDate declared inside a case without block scope can be accidentally accessed from other switch clauses if they're added later.

   switch (method) {
-    case 'GET':
-      const startDate = <string>req.query.startDate;
+    case 'GET': {
+      const startDate = Array.isArray(req.query.startDate) 
+        ? req.query.startDate[0] 
+        : <string>req.query.startDate;
       assert(login, common.badRequest);

       try {
         const stats = await getDoobooStats({
           login: login.toLocaleLowerCase(),
           lang: locale,
           startDate,
         });

         if (!stats) {
           res.status(404).json({message: common.notFound});

           return;
         }

         track('github-trophies', undefined, {
           language: locale,
           user_id: login,
           extra: {login, lang: locale},
         });

         const trophies = stats.pluginTrophies.filter(
           (el) => el.score > LOWEST_TROPHIES_SCORE,
         );

         if (trophies.length === 0) {
           res.status(404).json({message: common.notFound});

           return;
         }

         const trophySvg = renderGithubTrophies(trophies);
         uploadTrophiesSvg(login, trophySvg);

         res.setHeader('Content-Type', 'image/svg+xml');
         res.send(trophySvg);
       } catch (err) {
         res.status(500).send(err);
       }

       break;
+    }
     default:
       res.status(404).json({message: common.notFound});
   }
pages/api/github-stats-advanced.ts (1)

23-55: Wrap case block in braces to scope the variable declaration.

Same issue as in github-trophies.ts - the const startDate declaration inside a case without block scope can be accessed from other switch clauses.

   switch (method) {
-    case 'GET':
-      const startDate = <string>req.query.startDate;
+    case 'GET': {
+      const startDate = Array.isArray(req.query.startDate)
+        ? req.query.startDate[0]
+        : <string>req.query.startDate;
       assert(login, common.badRequest);

       try {
         const stats = await getDoobooStats({
           login: login.toLocaleLowerCase(),
           lang: locale,
           startDate,
         });

         if (!stats) {
           res.status(404).json({message: common.notFound});

           return;
         }

         const {file} = await generateGithubSVG(login, stats, true);

         track('github-stats-advanced', undefined, {
           language: locale,
           user_id: login,
           extra: {login, lang: locale},
         });

         res.setHeader('Content-Type', 'image/svg+xml');
         res.send(file);
       } catch (err) {
         res.status(500).send(err);
       }

       break;
+    }
     default:
       res.status(404).json({message: common.notFound});
   }
🧹 Nitpick comments (17)
styles/root.css (1)

5-12: Consider using Tailwind's built-in animate-spin utility.

Tailwind CSS already provides an animate-spin utility that creates the same rotation animation. Unless you need custom timing or behavior, the built-in utility is preferable for consistency and maintainability.

If the default Tailwind animation suffices, you can remove this custom keyframes definition and use animate-spin directly in your components. If you need custom timing, consider extending Tailwind's animation configuration in tailwind.config.js instead:

-@keyframes spin {
-  from {
-    transform: rotate(0deg);
-  }
-  to {
-    transform: rotate(360deg);
-  }
-}

Then in tailwind.config.js:

module.exports = {
  theme: {
    extend: {
      animation: {
        'spin-slow': 'spin 3s linear infinite',
      }
    }
  }
}
app/[lang]/recent-list/GithubUserList.tsx (1)

50-71: Consider handling non-OK HTTP responses.

The fetch call doesn't check response.ok before parsing JSON. A 4xx/5xx response would still attempt to parse and could lead to confusing behavior if result.users is undefined.

  const handleTierSelect = useCallback(async (tier: Tier | null) => {
    setSelectedTier(tier);

    if (!tier) {
      setTierData([]);
      return;
    }

    setIsLoadingTier(true);
    try {
      const response = await fetch(`/api/users-by-tier?tier=${tier}`);
+      if (!response.ok) {
+        throw new Error(`HTTP error: ${response.status}`);
+      }
      const result = await response.json();
      if (result.users) {
        setTierData(result.users);
      }
    } catch (error) {
      console.error('Failed to fetch tier users:', error);
      setTierData([]);
    } finally {
      setIsLoadingTier(false);
    }
  }, []);
pages/api/top-tier-users.ts (1)

37-42: Consider handling potential Supabase query errors.

The userPlugins query doesn't check for errors. While it gracefully falls back to an empty array, logging errors would help with debugging production issues.

-  const {data: userPlugins}: {data: UserPluginRow[] | null} = await supabase
+  const {data: userPlugins, error}: {data: UserPluginRow[] | null; error: unknown} = await supabase
     .from('user_plugins')
     .select('*')
     .match({plugin_id: 'dooboo-github'})
     .order('score', {ascending: false})
     .limit(5);
+
+  if (error) {
+    console.error('Failed to fetch user plugins:', error);
+  }
pages/api/users-by-tier.ts (2)

24-29: Handle potential array query parameter.

When tier is passed multiple times (e.g., ?tier=Gold&tier=Silver), query.tier becomes an array. Casting directly to string would result in unexpected behavior.

-  const tier = query.tier as string;
+  const tier = Array.isArray(query.tier) ? query.tier[0] : query.tier;

   if (!tier) {
     res.status(400).send({message: 'Tier is required'});
     return;
   }

64-71: Consider adding error handling for the database query.

The query result doesn't check for errors. If the database call fails, userPlugins could be null due to an error, but the code would silently return an empty array.

-  const {data: userPlugins}: {data: UserPluginRow[] | null} = await supabase
+  const {data: userPlugins, error}: {data: UserPluginRow[] | null; error: any} = await supabase
     .from('user_plugins')
     .select('*')
     .match({plugin_id: 'dooboo-github'})
     .gte('score', minScore)
     .lte('score', maxScore)
     .order('score', {ascending: false})
     .limit(50);
+
+  if (error) {
+    res.status(500).send({message: 'Failed to fetch users'});
+    return;
+  }
server/services/githubService.ts (1)

275-327: Consider adding a delay between batches for rate limiting.

The batching approach (size 3) helps with rate limiting, but there's no delay between batches. GitHub's GraphQL API has rate limits, and rapid sequential requests might still hit them.

   // Fetch stats for each month in parallel (batch of 3 to avoid rate limiting)
   const batchSize = 3;
   for (let i = 0; i < months.length; i += batchSize) {
     const batch = months.slice(i, i + batchSize);
     const results = await Promise.all(
       batch.map(async (month) => {
         try {
           const {commits, pullRequests, reviews} =
             await fetchGithubMonthlyContributions(login, month);

           return {
             month,
             commits,
             pullRequests,
             reviews,
             total: commits + pullRequests + reviews,
           };
         } catch {
           return {month, commits: 0, pullRequests: 0, reviews: 0, total: 0};
         }
       }),
     );
     monthlyContributions.push(...results);
+    
+    // Add small delay between batches to respect rate limits
+    if (i + batchSize < months.length) {
+      await new Promise((resolve) => setTimeout(resolve, 100));
+    }
   }
pages/api/github-stats.ts (2)

39-46: Consider validating startDate format.

The startDate parameter is extracted from the query but not validated for the expected YYYY-MM format. While downstream services may handle invalid formats, adding validation at the API boundary would provide clearer error messages and prevent unnecessary processing.

Consider adding format validation:

 const startDate = req.query.startDate as string | undefined;
+if (startDate && !/^\d{4}-\d{2}$/.test(startDate)) {
+  res.status(400).json({message: 'Invalid startDate format. Expected YYYY-MM'});
+  return;
+}
 assert(loginParam, common.badRequest);

71-78: Consider validating startDate format.

Similar to the GET endpoint, the POST endpoint should validate the startDate format before passing it to downstream services.

Apply the same validation pattern:

 const startDateBody = req.body.startDate as string | undefined;
+if (startDateBody && !/^\d{4}-\d{2}$/.test(startDateBody)) {
+  res.status(400).json({message: 'Invalid startDate format. Expected YYYY-MM'});
+  return;
+}
 assert(loginBody, common.badRequest);
app/[lang]/stats/[login]/Scouter/StatsDetails/StatsRadar.tsx (1)

39-57: Consider responsive dimensions for the radar chart.

The chart uses fixed dimensions (280×250). While this may work for the current layout, consider making the chart responsive to container width for better adaptability across screen sizes.

If responsiveness is desired, you could use ResponsiveContainer from recharts:

 return (
   <div className="mt-6">
-    <RadarChart
-      width={280}
-      height={250}
+    <ResponsiveContainer width="100%" height={250}>
+      <RadarChart
-      data={radarData}
-      cx="50%"
-      cy="50%"
-      outerRadius="70%"
-    >
+        data={radarData}
+        cx="50%"
+        cy="50%"
+        outerRadius="70%"
+      >
-      <PolarGrid stroke="#444" />
-      <PolarAngleAxis dataKey="stat" tick={{fill: '#888', fontSize: 12}} />
-      <Radar
-        name="Score"
-        dataKey="value"
-        stroke="#3b82f6"
-        fill="#3b82f6"
-        fillOpacity={0.4}
-        strokeWidth={2}
-      />
-    </RadarChart>
+        <PolarGrid stroke="#444" />
+        <PolarAngleAxis dataKey="stat" tick={{fill: '#888', fontSize: 12}} />
+        <Radar
+          name="Score"
+          dataKey="value"
+          stroke="#3b82f6"
+          fill="#3b82f6"
+          fillOpacity={0.4}
+          strokeWidth={2}
+        />
+      </RadarChart>
+    </ResponsiveContainer>
   </div>
 );

And add the import:

-import {RadarChart, PolarGrid, PolarAngleAxis, Radar} from 'recharts';
+import {RadarChart, PolarGrid, PolarAngleAxis, Radar, ResponsiveContainer} from 'recharts';
app/[lang]/recent-list/TopTierUsers.tsx (2)

22-38: Consider showing error state to users.

The error is logged to console but users see only a loading skeleton that never completes. Consider showing an error message or gracefully degrading to an empty state.

Add error state:

 const [topTierUsers, setTopTierUsers] = useState<UserListItem[]>([]);
 const [isLoading, setIsLoading] = useState(true);
+const [error, setError] = useState(false);

 useEffect(() => {
   const fetchTopUsers = async () => {
     try {
       const response = await fetch('/api/top-tier-users');
       const data = await response.json();
       if (data.users) {
         setTopTierUsers(data.users);
       }
     } catch (error) {
       console.error('Failed to fetch top tier users:', error);
+      setError(true);
     } finally {
       setIsLoading(false);
     }
   };

   fetchTopUsers();
 }, []);

+if (error) {
+  return <div />;
+}
+
 if (isLoading) {

100-100: Type cast may be unsafe.

The tierName is cast to Tier without validation. If the API returns an unexpected tier name, getTierSvg will fall back to Iron tier, but the type system won't catch this mismatch.

Consider validating the tier or making getTierSvg more defensive:

-src={getTierSvg(user.tierName as Tier)}
+src={getTierSvg(user.tierName as Tier) || '/assets/tier_iron.svg'}

Or add runtime validation if stricter type safety is desired.

app/[lang]/stats/[login]/page.tsx (1)

47-47: Parameter naming inconsistency: endDate passed as startDate.

The variable endDate is passed to getDoobooStats as the startDate parameter. Based on the context, it appears endDate represents the end of a time range, but it's being used as a filter point. This naming mismatch could cause confusion.

Consider adding a clarifying comment or renaming for consistency:

 const [stats, monthlyContributions] = await Promise.all([
-  getDoobooStats({login, lang, startDate: endDate}),
+  getDoobooStats({login, lang, startDate: endDate}), // endDate filters stats up to this month
   getMonthlyContribution({login, startDate}),
 ]);
app/[lang]/stats/[login]/MonthPicker.tsx (3)

84-102: Duplicate month names array.

The monthNames array duplicates MONTHS constant defined at lines 23-27. Reuse the existing constant for consistency and maintainability.

  const formatDisplay = () => {
    if (!value) {
      return t.selectPeriod;
    }
    const [year, month] = value.split('-');
-   const monthNames = [
-     'Jan',
-     'Feb',
-     'Mar',
-     'Apr',
-     'May',
-     'Jun',
-     'Jul',
-     'Aug',
-     'Sep',
-     'Oct',
-     'Nov',
-     'Dec',
-   ];
-   const monthName = monthNames[parseInt(month) - 1];
+   const monthName = MONTHS[parseInt(month) - 1];
    // Show year only if it's not the current year
    if (parseInt(year) === currentYear) {
      return monthName;
    }
    return `${monthName} ${year}`;
  };

36-42: viewYear doesn't sync when value prop changes externally.

The viewYear state is initialized from value but never updated if value changes after mount (e.g., parent resets it). Consider adding an effect to sync viewYear when value changes significantly.

+ useEffect(() => {
+   if (value) {
+     setViewYear(parseInt(value.split('-')[0]));
+   }
+ }, [value]);

164-172: Consider adding accessibility attributes for the dropdown.

The dropdown lacks aria-expanded, aria-haspopup, and keyboard navigation support (Escape to close). This could impact accessibility compliance.

      <button
        type="button"
        onClick={() => setIsOpen(!isOpen)}
+       aria-expanded={isOpen}
+       aria-haspopup="listbox"
        className={clsx(
app/[lang]/stats/[login]/Scouter/StatsDetails/StatsChart.tsx (2)

83-101: Consider extracting month formatting to a shared utility.

The formatMonth function duplicates the same month names array found in MonthPicker.tsx. Consider extracting this to a shared utility for consistency and reusability.

Create a shared utility:

// src/utils/dateUtils.ts
export const MONTH_NAMES = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] as const;

export function formatMonth(month: string): string {
  const [, monthNum] = month.split('-');
  return MONTH_NAMES[parseInt(monthNum, 10) - 1];
}

173-208: Conditional rendering of <Line> components may cause legend inconsistencies.

When lines are hidden via isLineVisible(), the corresponding legend entries also disappear, which might confuse users about the toggle behavior. Consider keeping all lines rendered but using strokeOpacity={0} for hidden lines to preserve legend entries.

-           {isLineVisible('commits') && (
-             <Line
-               ...
-               strokeOpacity={getLineOpacity('commits')}
-             />
-           )}
+           <Line
+             ...
+             strokeOpacity={isLineVisible('commits') ? getLineOpacity('commits') : 0}
+             dot={isLineVisible('commits') ? {fill: '#22c55e', strokeWidth: 2, r: 3} : false}
+           />

This preserves the legend for all series while visually hiding inactive ones.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 269a0ef and 9bb4349.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (32)
  • CLAUDE.md (1 hunks)
  • app/[lang]/(common)/Header/index.tsx (1 hunks)
  • app/[lang]/(home)/SectionFooter.tsx (1 hunks)
  • app/[lang]/layout.tsx (1 hunks)
  • app/[lang]/recent-list/GithubUserList.tsx (4 hunks)
  • app/[lang]/recent-list/TopTierUsers.tsx (1 hunks)
  • app/[lang]/recent-list/page.tsx (3 hunks)
  • app/[lang]/stats/Container.tsx (1 hunks)
  • app/[lang]/stats/[login]/MonthPicker.tsx (1 hunks)
  • app/[lang]/stats/[login]/Scouter/StatsDetails/SectionDooboo.tsx (5 hunks)
  • app/[lang]/stats/[login]/Scouter/StatsDetails/SectionPeople.tsx (2 hunks)
  • app/[lang]/stats/[login]/Scouter/StatsDetails/StatsChart.tsx (1 hunks)
  • app/[lang]/stats/[login]/Scouter/StatsDetails/StatsRadar.tsx (1 hunks)
  • app/[lang]/stats/[login]/Scouter/StatsDetails/index.tsx (2 hunks)
  • app/[lang]/stats/[login]/Scouter/StatsHeader.tsx (1 hunks)
  • app/[lang]/stats/[login]/Scouter/index.tsx (3 hunks)
  • app/[lang]/stats/[login]/SearchTextInput.tsx (4 hunks)
  • app/[lang]/stats/[login]/page.tsx (3 hunks)
  • app/[lang]/stats/page.tsx (1 hunks)
  • app/[lang]/styles.module.css (2 hunks)
  • locales/en.json (2 hunks)
  • locales/ko.json (2 hunks)
  • package.json (1 hunks)
  • pages/api/github-stats-advanced.ts (1 hunks)
  • pages/api/github-stats.ts (2 hunks)
  • pages/api/github-trophies.ts (1 hunks)
  • pages/api/top-tier-users.ts (1 hunks)
  • pages/api/users-by-tier.ts (1 hunks)
  • server/plugins/types/UserGraph.ts (1 hunks)
  • server/services/githubService.ts (12 hunks)
  • styles/output.css (37 hunks)
  • styles/root.css (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (17)
app/[lang]/stats/[login]/Scouter/StatsDetails/SectionPeople.tsx (1)
app/[lang]/stats/[login]/Scouter/StatsDetails/StatsRadar.tsx (1)
  • StatsRadar (15-60)
app/[lang]/stats/[login]/Scouter/StatsDetails/StatsRadar.tsx (2)
server/plugins/index.ts (1)
  • PluginStats (57-64)
src/localization.ts (1)
  • Translates (11-11)
app/[lang]/recent-list/TopTierUsers.tsx (3)
src/fetches/recentList.ts (1)
  • UserListItem (5-12)
src/utils/functions.ts (1)
  • getTierSvg (99-115)
app/[lang]/recent-list/TierRowItem.tsx (1)
  • Tier (6-14)
app/[lang]/stats/[login]/Scouter/StatsDetails/StatsChart.tsx (1)
server/services/githubService.ts (1)
  • MonthlyContribution (365-371)
app/[lang]/stats/[login]/MonthPicker.tsx (1)
src/localization.ts (1)
  • Translates (11-11)
app/[lang]/stats/[login]/SearchTextInput.tsx (1)
src/hooks/useSearchHistory.ts (1)
  • useSearchHistory (26-65)
app/[lang]/stats/[login]/Scouter/index.tsx (3)
server/services/githubService.ts (2)
  • DoobooStatsResponse (373-388)
  • MonthlyContribution (365-371)
src/fetches/github.ts (1)
  • StatsInfo (41-48)
app/[lang]/stats/[login]/Scouter/StatsDetails/index.tsx (1)
  • StatsDetails (19-45)
pages/api/github-stats-advanced.ts (1)
server/services/githubService.ts (1)
  • getDoobooStats (631-809)
pages/api/github-trophies.ts (2)
src/utils/assert.ts (1)
  • assert (7-14)
server/services/githubService.ts (1)
  • getDoobooStats (631-809)
pages/api/users-by-tier.ts (4)
pages/api/top-tier-users.ts (1)
  • handler (11-62)
src/utils/functions.ts (1)
  • PluginUser (43-58)
server/supabaseClient.ts (1)
  • getSupabaseClient (9-17)
src/types/types.ts (1)
  • UserPluginRow (10-10)
app/[lang]/recent-list/page.tsx (2)
app/[lang]/recent-list/TopTierUsers.tsx (1)
  • TopTierUsers (18-114)
app/[lang]/(common)/GreatFrontEnd.tsx (1)
  • GreatFrontEnd (16-43)
server/services/githubService.ts (2)
server/plugins/types/UserGraph.ts (1)
  • UserGraph (1-31)
src/types/supabase.ts (1)
  • Json (1-7)
pages/api/github-stats.ts (2)
src/utils/assert.ts (1)
  • assert (7-14)
server/services/githubService.ts (1)
  • getDoobooStats (631-809)
app/[lang]/stats/[login]/Scouter/StatsDetails/SectionDooboo.tsx (3)
app/[lang]/stats/[login]/Scouter/index.tsx (2)
  • statNames (18-25)
  • StatsWithMonthly (27-29)
app/[lang]/stats/[login]/MonthPicker.tsx (1)
  • MonthPicker (29-265)
app/[lang]/stats/[login]/Scouter/StatsDetails/StatsChart.tsx (1)
  • StatsChart (24-214)
app/[lang]/stats/[login]/Scouter/StatsDetails/index.tsx (2)
app/[lang]/stats/[login]/Scouter/index.tsx (2)
  • ScouterProps (31-35)
  • StatName (37-37)
app/[lang]/stats/[login]/Scouter/StatsDetails/SectionDooboo.tsx (1)
  • SectionDooboo (190-206)
app/[lang]/stats/[login]/page.tsx (4)
src/i18n.ts (1)
  • Locale (6-6)
src/localization.ts (1)
  • getTranslates (17-23)
server/services/githubService.ts (2)
  • getDoobooStats (631-809)
  • getMonthlyContribution (812-832)
app/[lang]/stats/[login]/Scouter/index.tsx (1)
  • Scouter (39-59)
app/[lang]/recent-list/GithubUserList.tsx (3)
app/[lang]/recent-list/TierRowItem.tsx (1)
  • Tier (6-14)
src/fetches/recentList.ts (1)
  • UserListItem (5-12)
src/utils/functions.ts (1)
  • getTierSvg (99-115)
🪛 Biome (2.1.2)
pages/api/github-stats-advanced.ts

[error] 25-25: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

pages/api/github-trophies.ts

[error] 27-27: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

styles/output.css

[error] 1508-1508: Duplicate properties can lead to unexpected behavior and may override previous declarations unintentionally.

min-width is already defined here.

Remove or rename the duplicate property to ensure consistent styling.

(lint/suspicious/noDuplicateProperties)

🪛 GitHub Actions: CI
app/[lang]/stats/[login]/SearchTextInput.tsx

[error] 37-37: ESLint: Calling setState synchronously within an effect. Avoid calling setState directly within an effect.

app/[lang]/stats/[login]/Scouter/StatsDetails/SectionDooboo.tsx

[error] 32-32: ESLint: Calling setState synchronously within an effect. Avoid calling setState directly within an effect.

🔇 Additional comments (44)
app/[lang]/(home)/SectionFooter.tsx (1)

84-86: Designer link update to GitHub profile looks good

Pointing the “hyochan” designer credit to the GitHub profile is correct and consistent with the project’s main CTA link.

server/plugins/types/UserGraph.ts (1)

84-105: Contribution calendar typing looks sound; confirm subset matches your data shape

The new contributionCalendar field and the ContributionCalendar / ContributionWeek / ContributionDay interfaces are consistent and structurally sane: they’re backward‑compatible (optional field on ContributionsCollection) and minimal but sufficient for typical calendar and chart use.

The only thing to double‑check is that this subset (totalContributions, weeks[].firstDay, weeks[].contributionDays[].{contributionCount,date}) matches exactly what your GitHub data layer (or API client) returns and what downstream chart components expect, especially around string vs Date handling and the possibility of contributionCalendar being undefined.

app/[lang]/stats/[login]/SearchTextInput.tsx (4)

40-44: Verify the need for router.refresh() after router.push().

Calling router.refresh() immediately after router.push() may be redundant—navigating to a new route should already fetch fresh server component data. If you're trying to invalidate the router cache, consider whether router.push() alone suffices, or use router.replace() with cache invalidation options.


103-113: Ensure @keyframes spin is defined in global styles.

The inline spinner relies on animation: 'spin 1s linear infinite', but @keyframes spin must be defined in your global CSS for this animation to work. Verify this keyframe exists, or consider using Tailwind's built-in animate-spin class instead:

- <div
-   style={{
-     width: '20px',
-     height: '20px',
-     border: '2px solid #BEBEBE',
-     borderTopColor: '#4190EB',
-     borderRadius: '50%',
-     animation: 'spin 1s linear infinite',
-   }}
- />
+ <div
+   className="w-5 h-5 border-2 border-[#BEBEBE] border-t-[#4190EB] rounded-full animate-spin"
+ />

This approach uses Tailwind's animate-spin utility which includes the necessary keyframes.


29-33: LGTM on language extraction.

The pattern correctly extracts the language segment from the pathname with a sensible fallback to 'en'. This aligns with Next.js i18n routing conventions.


92-101: Good accessible button implementation.

The native button with proper type="submit", disabled state handling, and visual feedback is well-implemented.

app/[lang]/stats/page.tsx (1)

22-22: LGTM!

Removing the trailing period from the hint text improves the UI polish and follows common UI conventions for placeholder/hint text.

app/[lang]/layout.tsx (1)

33-36: LGTM! Proper flexbox overflow prevention.

The addition of min-w-0 on flex containers is a well-known flexbox pattern to prevent flex items from overflowing. By default, flex items have min-width: auto, which can cause content to overflow its container. Setting min-w-0 allows the flex item to shrink below its content size. The overflow-x-hidden prevents unwanted horizontal scrolling, and w-full ensures proper width distribution.

These changes align with similar layout constraint updates across other components in this PR.

app/[lang]/stats/[login]/Scouter/StatsHeader.tsx (1)

113-114: LGTM! Consistent width constraint pattern.

The addition of explicit width constraints (w-full min-w-0) aligns with the broader layout improvements across this PR. Splitting the class strings improves readability while maintaining all existing styles.

app/[lang]/(common)/Header/index.tsx (1)

311-319: LGTM! Consistent layout constraint additions.

The width constraints added to the header follow the same pattern applied across other layout components in this PR, ensuring consistent overflow prevention and proper width handling throughout the application.

app/[lang]/styles.module.css (1)

6-49: LGTM! Well-designed horizontal scroll utility.

The .horizontalScroll class is well-implemented with:

  • Cross-browser scrollbar support (Firefox via scrollbar-width, WebKit via pseudo-elements)
  • Subtle, professional scrollbar styling with transparency and rounded corners
  • Mobile-responsive behavior that hides the scrollbar on smaller screens for a cleaner UX

This complements the layout constraint updates and supports the new horizontal UI elements introduced in this PR.

app/[lang]/stats/Container.tsx (1)

23-24: LGTM! Comprehensive width constraint implementation.

The explicit width constraints (flex-1 w-full max-w-full min-w-0) provide robust layout control:

  • flex-1: Takes available space in flex container
  • w-full: Ensures 100% width usage
  • max-w-full: Prevents exceeding container width
  • min-w-0: Allows shrinking below content size

This follows the same pattern applied consistently throughout the PR's layout improvements.

package.json (1)

47-47: recharts@3.5.1 is valid and has no known direct security vulnerabilities.

Version 3.5.1 is the latest official release of recharts and is not marked as deprecated. The official recharts package has no CVE or GHSA entries. Standard practice: run npm audit to check transitive dependencies (e.g., d3-color for known ReDoS issues), and ensure typo-squat packages like "recharts-smart" are not accidentally installed.

locales/en.json (2)

87-96: LGTM!

The new localization keys for the leaderboard feature (all, topUsers, topRanked) and the updated strings (title, noRecentList) are well-structured and consistent with the existing naming conventions.


111-115: LGTM!

The new stats-related localization keys (selectPeriod, periodDescription, resetToDefault, search) are appropriate for the month-picker and search functionality.

CLAUDE.md (1)

1-98: LGTM! Excellent addition of project guidelines.

This documentation provides clear and actionable guidance for development, covering styling patterns, dark mode support, and component conventions. The examples are practical and align well with Tailwind best practices.

locales/ko.json (2)

96-105: LGTM!

The Korean translations for the leaderboard section are consistent with the English counterparts and maintain proper structure.


120-124: LGTM!

The Korean translations for the new stats keys are appropriate and match the en.json structure.

app/[lang]/recent-list/GithubUserList.tsx (1)

151-207: LGTM! Well-structured tier filter UI.

The tier filter implementation has good UX considerations: proper loading states with disabled buttons and opacity changes, responsive design for mobile, and accessible aria-labels. The use of useCallback for handleTierSelect is appropriate given its use in button handlers.

pages/api/top-tier-users.ts (1)

11-61: LGTM! Well-structured API handler.

The handler has proper method validation, sequential data fetching with appropriate null checks, and clean data transformation. The tier calculation using getTierName is correctly applied.

pages/api/users-by-tier.ts (1)

58-61: Off-by-one edge case for max score boundary.

When maxScore is calculated as sortedTiers[tierIndex + 1].score - 1, users exactly at the boundary score of the next tier are correctly excluded. However, for the top tier, the hardcoded 100 as max might not align if scores can exceed 100.

Please verify that scores are guaranteed to be ≤ 100, or adjust the max score logic for the top tier to use a value like Number.MAX_SAFE_INTEGER or query the actual max score from the database.

styles/output.css (1)

1478-1509: This is auto-generated CSS - no action needed.

The static analysis warning about duplicate min-width properties appears to be a false positive. These are distinct Tailwind utility classes (.min-w-0 and .min-w-fit) with different selectors, not duplicate properties within the same rule. Since this file is auto-generated by Tailwind CSS, manual modifications should be avoided.

server/services/githubService.ts (4)

51-66: LGTM! Robust date handling with UTC.

The date parsing correctly uses UTC to avoid timezone issues, and the fallback to 12 months ago is well-implemented. Starting from the 1st of the month at noon UTC is a good practice to avoid edge cases.


535-599: Good caching strategy - skipping DB writes for custom date queries.

The logic to skip database updates when a custom startDate is provided is a sensible optimization. This prevents cached default data from being overwritten by date-specific queries, maintaining data integrity for the default view.


811-831: Public API correctly handles errors and returns empty array.

The getMonthlyContribution export properly logs errors with context and returns an empty array as a safe fallback, allowing callers to handle missing data gracefully.


390-416: I'll help you verify the review comment about the generateMonths function. Let me investigate the codebase to understand the context and validate the concerns raised.
<function_calls>


#!/bin/bash

First, find the file and examine the context around the function

cat -n server/services/githubService.ts | sed -n '385,420p'


</function_calls>
<function_calls>


#!/bin/bash

Search for where generateMonths is called and how startDate is passed

rg "generateMonths" --type ts -B5 -A5


</function_calls>
<function_calls>


#!/bin/bash

Look for API endpoints and validation logic related to startDate

rg "startDate" --type ts -B3 -A3 | head -80


</function_calls>
<function_calls>


#!/bin/bash

Search for validation functions or middleware that might validate startDate

rg "(validate|valid|check)" --type ts server/services/githubService.ts -B2 -A2


</function_calls>

app/[lang]/stats/[login]/Scouter/StatsDetails/SectionPeople.tsx (1)

7-7: LGTM! Radar chart integration is clean.

The StatsRadar component is properly imported and integrated after the scores block, with correct props passed through.

Also applies to: 62-63

app/[lang]/stats/[login]/Scouter/StatsDetails/StatsRadar.tsx (1)

18-27: Good hydration mismatch prevention.

The requestAnimationFrame pattern correctly defers rendering until after the client-side hydration completes, preventing React hydration mismatches.

app/[lang]/stats/[login]/Scouter/StatsDetails/index.tsx (2)

23-26: LGTM! Clean endDate propagation to Dooboo section.

The endDate prop is correctly destructured and passed only to the SectionDooboo component, which aligns with the monthly contribution feature being specific to the Dooboo stats view.


36-41: Good responsive layout constraints.

The addition of min-w-0 enables proper flex shrinking, and overflow-x-hidden prevents unwanted horizontal scrolling. These classes work together to ensure the stats details adapt properly to container constraints.

app/[lang]/stats/[login]/Scouter/index.tsx (2)

27-35: LGTM! Clean type extension for monthly contributions.

The StatsWithMonthly type extends DoobooStatsResponse with optional monthly contributions, and ScouterProps is updated accordingly. The optional nature of both monthlyContributions and endDate allows for backward compatibility.


39-56: Good prop destructuring and propagation.

The endDate is cleanly destructured from props and passed through to StatsDetails. The addition of min-w-0 ensures proper flex behavior in constrained layouts.

app/[lang]/recent-list/TopTierUsers.tsx (1)

81-83: Good security attributes on external links.

The use of target="_blank" with rel="noopener noreferrer" correctly prevents security vulnerabilities when opening external GitHub profile links.

app/[lang]/stats/[login]/page.tsx (3)

46-49: Good use of parallel fetching.

Using Promise.all to fetch stats and monthly contributions concurrently reduces total loading time compared to sequential fetches.


32-43: Date calculation logic is correct.

All test cases pass with expected results. The month - 11 calculation properly computes 12 months before the end date, with JavaScript's Date constructor correctly handling the negative month values to wrap into the previous year. No issues found.


17-17: The use of force-dynamic is appropriate and should not be changed.

The page fetches user-specific, frequently-changing GitHub statistics (commits, PRs, reviews, contributions). These are not static data—they update as the user makes contributions. The getDoobooStats function already implements intelligent caching with a 3-hour background refresh strategy for cached data, and getMonthlyContribution fetches current data from the GitHub GraphQL API.

Using Incremental Static Regeneration (ISR) would be inappropriate here since:

  1. Data is per-user and changes daily
  2. Each GitHub user should see their current stats
  3. ISR caches by URL, which would serve stale data across requests

The force-dynamic setting correctly ensures each request fetches fresh data while the underlying Supabase/GitHub layers manage their own caching strategies.

app/[lang]/recent-list/page.tsx (2)

56-78: Good responsive layout for horizontal scroll region.

The nested structure with horizontalScroll style and responsive breakpoints ensures the TopTierUsers and GreatFrontEnd components display properly across screen sizes. The use of min-w-fit on the inner container prevents premature wrapping, and the mobile layout switches to a column layout appropriately.


43-44: Layout alignment changed to items-start.

The change from items-center justify-between to items-start aligns items to the top. Ensure this is the intended visual change, especially for the H1 title alignment relative to the new TopTierUsers component.

Verify the visual alignment is correct in the UI, particularly on different screen sizes where the H1 and TopTierUsers might have different heights.

app/[lang]/stats/[login]/Scouter/StatsDetails/SectionDooboo.tsx (2)

35-47: router.refresh() after router.push() may be redundant.

Calling router.refresh() immediately after router.push() can cause unnecessary re-fetching. Next.js App Router's push() with a new URL typically triggers a navigation that re-fetches server components. The refresh call may be redundant or cause a double data fetch.

Verify if the refresh is actually needed for your use case. If the stats data is fetched server-side based on the URL query param, router.push() alone should suffice:

  const handleEndDateChange = useCallback(
    (newDate: string | undefined) => {
      if (!pathname) return;
      setIsLoading(true);
      if (newDate) {
        router.push(`${pathname}?endDate=${newDate}`);
      } else {
        router.push(pathname);
      }
-     router.refresh();
    },
    [pathname, router],
  );

190-205: LGTM!

The component structure cleanly passes endDate through props and properly spreads remaining props to child components. The layout with flex and width constraints looks appropriate.

app/[lang]/stats/[login]/Scouter/StatsDetails/StatsChart.tsx (3)

35-41: Good hydration fix using requestAnimationFrame.

Using requestAnimationFrame to defer setIsClient(true) avoids React 19's strict mode warnings about synchronous state updates in effects. This is a clean pattern for client-only rendering guards.


43-48: Acceptable use of any for Recharts event handlers.

The eslint-disable comments for @typescript-eslint/no-explicit-any are reasonable here since Recharts' event handler types are not well-typed. The code properly validates data?.dataKey before use, which is defensive.

Also applies to: 54-67


137-138: Chart width could grow unbounded with many months.

The width calculation Math.max(640, chartData.length * 60) grows linearly with data length. For 12+ months of data, this works fine, but verify the expected data range. The horizontal scroll container handles overflow gracefully.

app/[lang]/stats/[login]/MonthPicker.tsx (1)

151-162: I'll help you verify whether the spin keyframe animation is properly defined in this codebase. Let me search for its definition and understand the CSS setup.
<function_calls>

#!/bin/bash

Search for @Keyframes spin definition in CSS/SCSS files

echo "=== Searching for @Keyframes spin ==="
rg -n '@Keyframes\s+spin' --type css --type scss

echo ""
echo "=== Searching for spin animation references ==="
rg -n 'spin' --type css --type scss | head -20

echo ""
echo "=== Checking for Tailwind config ==="
fd -n "tailwind|tailwind.config" -e js -e ts -e json


</function_calls>
<function_calls>

#!/bin/bash

Check the MonthPicker.tsx file and surrounding files

echo "=== Checking MonthPicker.tsx imports ==="
fd -p "MonthPicker.tsx" | xargs head -30

echo ""
echo "=== Looking for global stylesheets ==="
fd -e css -e scss | grep -E "globals|global|styles|app" | head -20

echo ""
echo "=== Checking for CSS-in-JS or styled components ==="
rg -n "styled|css`|keyframes" --type tsx --type ts | grep -i spin | head -10


</function_calls>

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: 8

♻️ Duplicate comments (6)
app/[lang]/leaderboard/GithubUserList.tsx (2)

60-60: Hardcoded API endpoint should be centralized.

This duplicates a previous review comment. The API endpoint URL should be defined in a centralized constants file for maintainability.


229-232: Use HTTPS and add security parameters for window.open.

This duplicates a previous review comment. The GitHub URL should use HTTPS instead of HTTP. Additionally, consider adding security parameters to prevent tabnabbing attacks.

Apply this diff:

          onClickRow={(user) => {
            const login = user.login;
-            window.open('http://github.com/' + login);
+            window.open('https://github.com/' + login, '_blank', 'noopener,noreferrer');
          }}
app/[lang]/leaderboard/TopTierUsers.tsx (1)

25-25: API endpoint URL is hardcoded.

The endpoint /api/top-tier-users is hardcoded, which makes it harder to maintain and update. Consider extracting API routes to a shared constants file.

app/[lang]/stats/[login]/Scouter/StatsDetails/StatsChart.tsx (1)

43-47: Replace any with typed event payload.

Past review already flagged the weak type safety from using any. Both handlers receive legend event payloads from recharts with a known shape.

Apply this diff to type both handlers:

-  const handleLegendMouseEnter = useCallback((data: any) => {
+  const handleLegendMouseEnter = useCallback((data: { dataKey?: string }) => {
     if (data?.dataKey) {
       setHoveredLine(String(data.dataKey));
     }
   }, []);

   const handleLegendMouseLeave = useCallback(() => {
     setHoveredLine(null);
   }, []);

-  const handleLegendClick = useCallback((data: any) => {
+  const handleLegendClick = useCallback((data: { dataKey?: string }) => {
     if (data?.dataKey) {
       const key = data.dataKey as LineKey;
       setActiveLines((prev) => {

Also applies to: 53-65

app/[lang]/stats/[login]/Scouter/StatsDetails/SectionDooboo.tsx (1)

30-35: Fix ESLint error blocking the pipeline.

Past review flagged this synchronous setState call within an effect, and the pipeline is failing. Adding eslint-disable suppresses the warning but doesn't fix the architectural issue.

Apply the solution from the past review — remove this effect and reset loading when data arrives:

-  // Reset loading when endDate changes (data loaded)
-  useEffect(() => {
-    // Safe reset after data refresh
-    // eslint-disable-next-line react-hooks/set-state-in-effect
-    setIsLoading(false);
-  }, [endDate, stats]);

Then, derive isLoading from props or reset it in the parent component when stats updates. Alternatively, implement the ref-based approach suggested in the past review to track expected endDate.

app/[lang]/stats/[login]/SearchTextInput.tsx (1)

71-81: Spinner/button UX is solid, but replace inline styles with Tailwind (or a shared Spinner) and add an accessible label.

  • The button/disabled logic and inline spinner feedback are good, but the spinner is implemented via inline style={...}; project guidelines prefer Tailwind utilities or a shared component. You could match the prior suggestion and swap to something like:
-          {isLoading ? (
-            <div
-              style={{
-                width: '20px',
-                height: '20px',
-                border: '2px solid #BEBEBE',
-                borderTopColor: '#4190EB',
-                borderRadius: '50%',
-                animation: 'spin 1s linear infinite',
-              }}
-            />
-          ) : (
-            <SearchIcon size={20} className="text-basic" />
-          )}
+          {isLoading ? (
+            <div
+              className="h-5 w-5 animate-spin rounded-full border-2 border-gray-300 border-t-brand dark:border-gray-600 dark:border-t-brand"
+            />
+          ) : (
+            <SearchIcon size={20} className="text-basic" />
+          )}
  • For accessibility, consider giving the submit button an explicit label so icon-only content is announced correctly, e.g. aria-label={t.search} or title={t.search}.

Also applies to: 94-119

🧹 Nitpick comments (5)
app/[lang]/leaderboard/GithubUserList.tsx (4)

44-48: Cursor initialization is safe but consider adding a comment.

The nested optional chaining protects against undefined access, but the logic is non-obvious. A brief comment explaining the cursor initialization would improve maintainability.


66-67: Provide user-facing error feedback.

Currently, errors are only logged to the console. Consider displaying an error message or toast notification to inform users when tier data fails to load.


103-103: Remove redundant height class.

The className contains both h-[37px] and h-[50px]. The latter overrides the former, making h-[37px] redundant.

Apply this diff:

-        cellClassName: 'h-[37px] text-start w-3/12 h-[50px] py-[8px]',
+        cellClassName: 'text-start w-3/12 h-[50px] py-[8px]',

114-114: Remove redundant height class.

Same issue as Line 103 - h-[37px] is overridden by h-[50px].

Apply this diff:

-        cellClassName: 'h-[37px] text-start w-3/12 h-[50px] py-[8px]',
+        cellClassName: 'text-start w-3/12 h-[50px] py-[8px]',
app/[lang]/leaderboard/TopTierUsers.tsx (1)

22-38: Add cleanup with AbortController.

The useEffect should include an AbortController to cancel the fetch if the component unmounts before the request completes, preventing potential memory leaks and state updates on unmounted components.

Apply this diff:

   useEffect(() => {
+    const controller = new AbortController();
+    
     const fetchTopUsers = async () => {
       try {
-        const response = await fetch('/api/top-tier-users');
+        const response = await fetch('/api/top-tier-users', {
+          signal: controller.signal,
+        });
         const data = await response.json();
         if (data.users) {
           setTopTierUsers(data.users);
         }
       } catch (error) {
+        if (error instanceof Error && error.name === 'AbortError') {
+          return;
+        }
         console.error('Failed to fetch top tier users:', error);
       } finally {
         setIsLoading(false);
       }
     };
 
     fetchTopUsers();
+    
+    return () => {
+      controller.abort();
+    };
   }, []);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9bb4349 and 5edff7c.

📒 Files selected for processing (10)
  • app/[lang]/(common)/Header/index.tsx (6 hunks)
  • app/[lang]/leaderboard/GithubUserList.tsx (1 hunks)
  • app/[lang]/leaderboard/TopTierUsers.tsx (1 hunks)
  • app/[lang]/leaderboard/page.tsx (3 hunks)
  • app/[lang]/stats/[login]/Scouter/StatsDetails/SectionDooboo.tsx (5 hunks)
  • app/[lang]/stats/[login]/Scouter/StatsDetails/StatsChart.tsx (1 hunks)
  • app/[lang]/stats/[login]/SearchTextInput.tsx (4 hunks)
  • locales/en.json (4 hunks)
  • locales/ko.json (3 hunks)
  • styles/output.css (38 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
app/[lang]/stats/[login]/SearchTextInput.tsx (1)
src/hooks/useSearchHistory.ts (1)
  • useSearchHistory (26-65)
app/[lang]/stats/[login]/Scouter/StatsDetails/StatsChart.tsx (1)
server/services/githubService.ts (1)
  • MonthlyContribution (365-371)
app/[lang]/leaderboard/TopTierUsers.tsx (3)
src/fetches/recentList.ts (1)
  • UserListItem (5-12)
src/utils/functions.ts (1)
  • getTierSvg (99-115)
app/[lang]/leaderboard/TierRowItem.tsx (1)
  • Tier (6-14)
app/[lang]/leaderboard/page.tsx (2)
app/[lang]/leaderboard/TopTierUsers.tsx (1)
  • TopTierUsers (18-131)
app/[lang]/(common)/GreatFrontEnd.tsx (1)
  • GreatFrontEnd (16-43)
app/[lang]/stats/[login]/Scouter/StatsDetails/SectionDooboo.tsx (3)
app/[lang]/stats/[login]/Scouter/index.tsx (2)
  • statNames (18-25)
  • StatsWithMonthly (27-29)
app/[lang]/stats/[login]/MonthPicker.tsx (1)
  • MonthPicker (29-265)
app/[lang]/stats/[login]/Scouter/StatsDetails/StatsChart.tsx (1)
  • StatsChart (24-212)
🪛 Biome (2.1.2)
styles/output.css

[error] 1508-1508: Duplicate properties can lead to unexpected behavior and may override previous declarations unintentionally.

min-width is already defined here.

Remove or rename the duplicate property to ensure consistent styling.

(lint/suspicious/noDuplicateProperties)

🔇 Additional comments (18)
locales/ko.json (2)

96-105: Korean translations for leaderboard and tier filtering look good.

The new keys (all, topUsers, topRanked) and updated labels properly reflect the leaderboard feature and tier-filtering capabilities. Translations maintain consistency with the English locale structure and align with the PR's feature set.


106-124: Stats section translations support monthly tracking feature.

The new keys (selectPeriod, periodDescription, resetToDefault, search, score) are appropriately translated and align with the monthly contribution visualization goals. The updated searchUserHint is clear and contextually accurate.

locales/en.json (3)

87-95: Leaderboard and tier filter labels are clear and consistent.

The rebranding from "Recent List" to "Leaderboard" (line 88) and the new tier-filter keys (all, topUsers, topRanked) effectively communicate the filtering capabilities. The updated "No users found" message (line 90) is more accurate than the previous "No recent list."


111-111: Inconsistent capitalization for stats.score.

In the recentList section (line 92), "score" is capitalized as "Score", but in the stats section (line 111), it's lowercase "score". This inconsistency may cause unexpected formatting if both keys are used in similar UI contexts (e.g., column headers, badges).

Verify whether the lowercase form is intentional for stats display or should be capitalized to match recentList.

Consider updating line 111 to maintain consistency:

-    "score": "score",
+    "score": "Score",

97-115: Stats section translations properly support monthly tracking and period selection.

The new keys (selectPeriod, periodDescription, resetToDefault, search) are well-phrased and clearly communicate the UI interactions for monthly stats navigation. The updated searchUserHint (line 99) provides better context than the previous message.

styles/output.css (1)

1506-1509: Static analysis flag is a false positive — vendor prefixes are intentional.

Line 1508 flags a duplicate min-width property, but this is standard CSS practice: the -moz- vendor prefix and the standard property coexist for cross-browser fallback support. No action needed.

app/[lang]/leaderboard/GithubUserList.tsx (4)

1-31: LGTM! Clean imports and tier ordering.

The component structure and tier hierarchy are well-defined.


73-74: LGTM! Clean data source switching.

The conditional logic correctly selects between tier-filtered and unfiltered data.


151-207: LGTM! Well-structured tier filter UI.

The tier selection buttons have appropriate loading states, responsive styling, and accessibility attributes.


209-242: Well-designed data table container.

The table wrapper has appropriate styling, loading states, and conditional scroll handling. The scroll handler is correctly disabled when a tier is selected.

app/[lang]/leaderboard/page.tsx (2)

43-44: Layout restructuring looks good.

The shift from justify-between to gap-4 with items-start alignment properly supports the new horizontal scroll container while maintaining responsive behavior for mobile viewports.


56-78: Horizontal scroll container structure is functional.

The three-level nesting (scroll container → nowrap flex → components) correctly implements horizontal scrolling with fixed-width children. The responsive visibility controls are appropriate for the layout constraints.

app/[lang]/(common)/Header/index.tsx (3)

49-54: Path matching logic is functional.

The normalizePath and isActivePath functions correctly handle trailing slashes and provide both exact and prefix matching. The startsWith approach works well for the current navigation structure (/stats/${login}, /leaderboard).


265-265: Navigation path updates are correct.

The route rename from /recent-list to /leaderboard aligns with the PR objectives, and removing the trailing slash from /stats/ ensures consistency with the new normalizePath utility.

Also applies to: 271-271, 275-275


325-325: Layout constraint classes are appropriate.

Adding w-full min-w-0 to the header and min-w-0 to the inner container correctly prevents flex overflow issues and enables proper text truncation behavior.

Also applies to: 333-333

app/[lang]/leaderboard/TopTierUsers.tsx (3)

1-16: Imports and type definitions are clean.

The component correctly imports necessary dependencies and defines a clear props interface.


40-62: Loading skeleton provides good UX.

The shimmer effect with consistent styling and dark mode support creates a polished loading experience.


68-130: Main render logic is well-implemented.

The horizontal scroll container, user tiles, and interactive states are correctly structured. The component properly handles Next.js Image optimization, external links with security attributes, focus states, and responsive design.

cursor,
});

const filteredUsers = users.filter((el) => !data.includes(el));
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Object comparison by reference won't deduplicate users.

The data.includes(el) check compares objects by reference, not by value. This means duplicate users (with the same data but different object instances) will not be filtered out, leading to duplicate entries in the list.

Apply this diff to compare by unique identifier (login):

-      const filteredUsers = users.filter((el) => !data.includes(el));
+      const filteredUsers = users.filter((el) => !data.some(existing => existing.login === el.login));
📝 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 filteredUsers = users.filter((el) => !data.includes(el));
const filteredUsers = users.filter((el) => !data.some(existing => existing.login === el.login));
🤖 Prompt for AI Agents
In app/[lang]/leaderboard/GithubUserList.tsx around line 144, the filter uses
data.includes(el) which compares object references and fails to deduplicate
users with identical content; change the logic to compare by unique identifier
(login) — build a Set of existing logins from data (e.g. new Set(data.map(d =>
d.login))) and filter users by checking if the user's login is in that Set (or
use data.some(d => d.login === el.login)) so duplicates are removed by value
rather than reference.

const filteredUsers = users.filter((el) => !data.includes(el));

setData([...data, ...filteredUsers]);
setCursor(new Date(filteredUsers?.[filteredUsers.length - 1]?.createdAt));
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

Guard against empty filteredUsers array.

If filteredUsers is empty after filtering, accessing filteredUsers[filteredUsers.length - 1] will be undefined, resulting in new Date(undefined) which creates an invalid date and breaks pagination.

Apply this diff to add a guard:

-      setData([...data, ...filteredUsers]);
-      setCursor(new Date(filteredUsers?.[filteredUsers.length - 1]?.createdAt));
+      if (filteredUsers.length > 0) {
+        setData([...data, ...filteredUsers]);
+        setCursor(new Date(filteredUsers[filteredUsers.length - 1].createdAt));
+      }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/[lang]/leaderboard/GithubUserList.tsx around line 147, the code
unconditionally does setCursor(new Date(filteredUsers?.[filteredUsers.length -
1]?.createdAt)); which will call new Date(undefined) when filteredUsers is
empty; change it to first check that filteredUsers exists and has length > 0 and
only then call setCursor with the last item's createdAt, otherwise setCursor to
null/undefined (or skip the call) to avoid creating an invalid Date and breaking
pagination.

Comment on lines 37 to 49
const handleEndDateChange = useCallback(
(newDate: string | undefined) => {
if (!pathname) return;
setIsLoading(true);
if (newDate) {
router.push(`${pathname}?endDate=${newDate}`);
} else {
router.push(pathname);
}
router.refresh();
},
[pathname, router],
);
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

Loading state management is fragile and may cause stuck spinners.

The pattern of setting isLoading(true) here and resetting it in a useEffect dependent on endDate/stats creates a race condition. If the parent component doesn't propagate the new endDate back down (e.g., fetch fails, navigation interrupted), isLoading remains true indefinitely, leaving the UI in a perpetual loading state.

Lift the loading state to the parent component that fetches the data, or use a data-fetching library (like React Query) that tracks loading/error/success states. The child component should receive isLoading as a prop from the source of truth.

Alternatively, if you must manage it here, track the expected endDate in a ref and only reset loading when the incoming stats actually corresponds to that endDate:

const expectedEndDateRef = useRef<string | undefined>(endDate);

const handleEndDateChange = useCallback(
  (newDate: string | undefined) => {
    if (!pathname) return;
    expectedEndDateRef.current = newDate;
    setIsLoading(true);
    // ... push & refresh
  },
  [pathname, router],
);

useEffect(() => {
  if (expectedEndDateRef.current === endDate) {
    setIsLoading(false);
  }
}, [endDate]);
🤖 Prompt for AI Agents
In app/[lang]/stats/[login]/Scouter/StatsDetails/SectionDooboo.tsx around lines
37 to 49, the component sets isLoading(true) locally when changing endDate which
can lead to a stuck spinner if the parent does not propagate the updated data;
either lift isLoading to the parent (pass isLoading as a prop and let the parent
set/clear it based on actual fetch success/failure or use a data-fetching lib
like React Query to manage loading/error states), or if you must keep local
state, track the expected endDate in a ref before router.push and only clear
isLoading when incoming props/stats match that expected endDate (update
expectedEndDateRef before navigation and compare it in an effect that clears
loading), and remove any reliance on a generic endDate/stats effect that could
leave loading true on failed/aborted navigations.

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 (1)
server/plugins/stats/gold.ts (1)

166-287: Fix GOLD language counts and collaborated‑repo detail source

There are a few logic issues that skew the GOLD stats:

  1. langsMyCount is computed from uniqueContribLangs instead of uniqueMyLangs, so “My Languages” count and the GOLD score use contributed‑repo language data twice.
  2. The MY_LANGUAGES stat’s totalCount uses langsContribCount instead of langsMyCount.
  3. The STARS_IN_COLLABORATED_REPOS details are built from contribRepoPRs instead of collaboratedRepos, so the list does not match the collaboratedReposStarCount metric.

A minimal fix:

-  const langsContribCount = uniqueContribLangs.filter(
-    (el) => el.count > 3,
-  ).length;
-  const langsMyCount = uniqueContribLangs.filter((el) => el.count > 3).length;
+  const langsContribCount = uniqueContribLangs.filter(
+    (el) => el.count > 3,
+  ).length;
+  const langsMyCount = uniqueMyLangs.filter((el) => el.count > 3).length;
@@
      {
        key: 'STARS_IN_COLLABORATED_REPOS',
@@
-        totalCount: collaboratedReposStarCount,
-        details: JSON.stringify(
-          contribRepoPRs
+        totalCount: collaboratedReposStarCount,
+        details: JSON.stringify(
+          collaboratedRepos
             .filter((el) => !!el)
             .map((el) => {
@@
      {
        key: 'MY_LANGUAGES',
@@
-        totalCount: langsContribCount,
-        details: JSON.stringify(uniqueMyLangs),
+        totalCount: langsMyCount,
+        details: JSON.stringify(uniqueMyLangs),
      },

This aligns counts and detail lists with the intended datasets and ensures the GOLD score reflects the correct language mix.

♻️ Duplicate comments (4)
app/[lang]/leaderboard/GithubUserList.tsx (2)

127-149: Fix infinite scroll deduplication and cursor handling

The current infinite scroll logic still has two issues:

  • users.filter((el) => !data.includes(el)) compares object references, so duplicate users with the same login will not be removed and can appear multiple times.
  • When filteredUsers is empty, filteredUsers?.[filteredUsers.length - 1]?.createdAt is undefined, so new Date(undefined) yields an invalid Date. That invalid cursor will still be truthy, so subsequent fetches may be called with an invalid cursor value.

Consider instead deduping by a stable key (e.g., login) and guarding against the empty case:

       const {users} = await fetchRecentList({
         pluginId: 'dooboo-github',
         take: 20,
         cursor,
       });

-      const filteredUsers = users.filter((el) => !data.includes(el));
-
-      setData([...data, ...filteredUsers]);
-      setCursor(new Date(filteredUsers?.[filteredUsers.length - 1]?.createdAt));
+      const existingLogins = new Set(data.map((u) => u.login));
+      const filteredUsers = users.filter(
+        (u) => !existingLogins.has(u.login),
+      );
+
+      if (filteredUsers.length === 0) {
+        return;
+      }
+
+      setData([...data, ...filteredUsers]);
+      setCursor(
+        new Date(filteredUsers[filteredUsers.length - 1].createdAt),
+      );

This avoids duplicates by value and ensures the cursor is only advanced when new items exist.


51-72: Still missing response status check for tier fetch

handleTierSelect calls response.json() without verifying response.ok, so 4xx/5xx responses (or HTML error pages) could throw or yield unexpected shapes.

Adding a simple status check before response.json() would make this more robust:

    setIsLoadingTier(true);
    try {
      const response = await fetch(`${API_USERS_BY_TIER}?tier=${tier}`);
+     if (!response.ok) {
+       throw new Error(`Failed to fetch tier users: ${response.status}`);
+     }
      const result = await response.json();
      if (result.users) {
        setTierData(result.users);
      }
app/[lang]/leaderboard/TopTierUsers.tsx (1)

65-67: Improve empty state handling.

Returning an empty <div /> is neither semantic nor accessible. Consider returning null instead.

app/[lang]/stats/[login]/Scouter/StatsDetails/StatsChart.tsx (1)

82-100: Add validation for month format.

The function assumes month is always in valid YYYY-MM format. If the backend returns malformed data, parseInt(monthNum, 10) - 1 could produce an invalid index (e.g., -1, NaN, or 12), returning undefined.

🧹 Nitpick comments (8)
server/plugins/stats/fire.ts (1)

16-45: FIRE repo mapping null‑safety is improved; consider also guarding null edges for consistency

The new owner extraction + return null + type‑guard .filter pattern ensures you don’t generate broken owner/name URLs or objects with missing owners. That’s a nice robustness win.

For consistency with other plugin mappers (e.g., gold/trophies using if (!el) return null; before accessing el.node), you might also add an early if (!el) return null; here, though GraphQL edges are typically non‑null.

server/services/githubService.ts (3)

275-327: Monthly contributions helper is sound; consider surfacing partial failures

The monthly contributions query correctly bounds each month with UTC from / to values and returns zero counts when the GitHub request fails, which is safe.

If you need to debug per‑month failures later, you might want to optionally log or tag which month failed (even at a lower log level) rather than silently returning zeros in the catch path of getMonthlyContributions. For now, the behavior is acceptable.


454-599: Avoid overwriting cached stats for custom startDate – behavior looks correct

The if (!startDate) guard around all Supabase writes in upsertGithubStats ensures that ad‑hoc, date‑bounded stats requests don’t mutate the default cached dataset. Passing startDate only into getGithubUser while keeping DB writes behind the guard is a clean separation of concerns.

No functional issues spotted here.


811-832: getMonthlyContribution wrapper is fine; consider reusing validation

The public API wrapper lowercases login, delegates to getMonthlyContributions, and returns an empty array on error with useful logging — that’s a reasonable contract.

If you adopt stronger startDate validation in getGithubUser, you might want to reuse the same validation (or a shared helper) here to ensure both entry points interpret startDate identically.

pages/api/top-tier-users.ts (1)

1-61: Top-tier users API is well-structured; consider sharing the tier type

The handler correctly:

  • Restricts to GET, 404s when the plugin is missing.
  • Pulls the top 5 user_plugins by score for dooboo-github.
  • Derives tierName via getTierName and filters out rows with github_id === null.
  • Adds Cache-Control: s-maxage=3600, stale-while-revalidate.

One small clean‑up: you could reuse the same TierDef type used in pages/api/users-by-tier.ts instead of re‑declaring {tier: string; score: number} here, to keep tier JSON typing in one place.

app/[lang]/leaderboard/TopTierUsers.tsx (1)

104-110: Unsafe type assertion for tierName.

The user.tierName as Tier cast assumes the API always returns valid tier names. If the API returns an unexpected value, getTierSvg will default to Iron tier silently, which may hide data issues.

Consider adding runtime validation or using a type guard:

const isValidTier = (tier: string): tier is Tier => 
  ['Iron', 'Bronze', 'Silver', 'Gold', 'Platinum', 'Diamond', 'Master', 'Challenger'].includes(tier);

// Then in render:
src={getTierSvg(isValidTier(user.tierName) ? user.tierName : 'Iron')}
app/[lang]/stats/[login]/Scouter/StatsDetails/StatsChart.tsx (1)

136-140: Consider ResponsiveContainer for better chart responsiveness.

The current implementation uses a fixed width calculation. Recharts provides ResponsiveContainer which handles resizing automatically and may provide a smoother experience.

+import { ResponsiveContainer } from 'recharts';

-<LineChart
-  width={Math.max(640, chartData.length * 60)}
-  height={300}
-  ...
->
+<ResponsiveContainer width="100%" height={300}>
+  <LineChart data={chartData} margin={{top: 10, right: 30, left: 10, bottom: 20}}>
+    ...
+  </LineChart>
+</ResponsiveContainer>
app/[lang]/stats/[login]/Scouter/StatsDetails/SectionDooboo.tsx (1)

50-54: Consider using router.replace instead of router.push for filter changes.

Using router.push adds entries to the browser history for each date change. For filter/query param changes, router.replace is often preferred to avoid polluting the history stack.

     if (newDate) {
-      router.push(`${pathname}?endDate=${newDate}`);
+      router.replace(`${pathname}?endDate=${newDate}`);
     } else {
-      router.push(pathname);
+      router.replace(pathname);
     }
     router.refresh();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5edff7c and 8baaef7.

📒 Files selected for processing (12)
  • app/[lang]/leaderboard/GithubUserList.tsx (1 hunks)
  • app/[lang]/leaderboard/TopTierUsers.tsx (1 hunks)
  • app/[lang]/leaderboard/apiRoutes.ts (1 hunks)
  • app/[lang]/stats/[login]/MonthPicker.tsx (1 hunks)
  • app/[lang]/stats/[login]/Scouter/StatsDetails/SectionDooboo.tsx (5 hunks)
  • app/[lang]/stats/[login]/Scouter/StatsDetails/StatsChart.tsx (1 hunks)
  • app/[lang]/stats/[login]/SearchTextInput.tsx (4 hunks)
  • pages/api/top-tier-users.ts (1 hunks)
  • server/plugins/stats/fire.ts (1 hunks)
  • server/plugins/stats/gold.ts (1 hunks)
  • server/services/githubService.ts (13 hunks)
  • styles/output.css (42 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/[lang]/stats/[login]/MonthPicker.tsx
🧰 Additional context used
🧬 Code graph analysis (5)
app/[lang]/leaderboard/TopTierUsers.tsx (4)
src/fetches/recentList.ts (1)
  • UserListItem (5-12)
app/[lang]/leaderboard/apiRoutes.ts (1)
  • API_TOP_TIER_USERS (2-2)
src/utils/functions.ts (1)
  • getTierSvg (99-115)
app/[lang]/leaderboard/TierRowItem.tsx (1)
  • Tier (6-14)
app/[lang]/stats/[login]/SearchTextInput.tsx (1)
src/hooks/useSearchHistory.ts (1)
  • useSearchHistory (26-65)
app/[lang]/stats/[login]/Scouter/StatsDetails/StatsChart.tsx (1)
server/services/githubService.ts (1)
  • MonthlyContribution (365-371)
pages/api/top-tier-users.ts (4)
src/utils/functions.ts (1)
  • PluginUser (43-58)
pages/api/users-by-tier.ts (1)
  • handler (13-89)
server/supabaseClient.ts (1)
  • getSupabaseClient (9-17)
src/types/types.ts (1)
  • UserPluginRow (10-10)
server/services/githubService.ts (4)
server/plugins/types/UserGraph.ts (1)
  • UserGraph (1-31)
server/plugins/index.ts (1)
  • getGithubStatus (66-93)
server/plugins/topLanguages.ts (1)
  • getTopLanguages (9-72)
server/plugins/trophies.ts (1)
  • getTrophies (140-282)
🪛 Biome (2.1.2)
styles/output.css

[error] 1508-1508: Duplicate properties can lead to unexpected behavior and may override previous declarations unintentionally.

min-width is already defined here.

Remove or rename the duplicate property to ensure consistent styling.

(lint/suspicious/noDuplicateProperties)

🔇 Additional comments (10)
app/[lang]/leaderboard/apiRoutes.ts (1)

1-2: Centralized leaderboard API route constants look good

Defining API_USERS_BY_TIER and API_TOP_TIER_USERS here removes hard‑coded paths in components and keeps routes consistent.

styles/output.css (1)

1506-1509: Duplicate min-width in .min-w-fit is intentional, not a bug

The two min-width declarations in .min-w-fit (-moz-fit-content then fit-content) are the usual vendor‑prefixed + standard pattern generated by Tailwind. Functionally this is correct; the second line just overrides the first where supported.

If Biome is flagging this as noDuplicateProperties, it’s safe to suppress that rule for generated CSS rather than changing the output.

server/services/githubService.ts (2)

390-452: Monthly window generation & batching look reasonable

generateMonths and getMonthlyContributions correctly:

  • Start from the given startDate month (UTC) and advance up to 12 months.
  • Exclude future months relative to current UTC year/month.
  • Limit concurrency to batches of 3 GitHub GraphQL calls.

That’s a good balance between correctness and rate‑limit friendliness. No changes needed here.


631-799: Cache‑skip semantics for getDoobooStats with startDate are consistent

getDoobooStats now:

  • Skips the cached user_plugins + stats fast‑path when a custom startDate is provided.
  • Still uses the cached path (with background upsertGithubStats) when !startDate and cached stats exist.
  • Propagates startDate through to upsertGithubStats in the miss path.

This correctly prevents custom date queries from polluting the default cache while keeping the existing ISR‑like behavior intact. Looks good.

app/[lang]/stats/[login]/Scouter/StatsDetails/StatsChart.tsx (2)

36-42: Good use of requestAnimationFrame for hydration safety.

The deferred setIsClient(true) via requestAnimationFrame correctly avoids React 19 hydration warnings. The cleanup function properly cancels the frame on unmount.


44-66: Legend interaction handlers are well-typed.

Using LegendPayload from recharts provides proper type safety. The click behavior that resets to all lines when clicking the last active line is a good UX pattern.

app/[lang]/stats/[login]/Scouter/StatsDetails/SectionDooboo.tsx (3)

31-42: Improved loading state management with ref pattern.

The pendingEndDateRef approach addresses the previous ESLint warning and race condition concerns. The requestAnimationFrame wrapper defers the state update appropriately.

However, the condition pendingEndDateRef.current === undefined on line 35 will reset loading on initial mount even if no navigation occurred. This may be intentional for the initial load, but verify this is the desired behavior.


139-149: StatsChart and MonthPicker integration looks correct.

The components receive appropriate props: MonthPicker gets the translation object, current value, change handler, and loading state. StatsChart receives the monthly contributions data and loading state for visual feedback.


194-198: Type definition updated correctly.

The SectionProps type now properly includes endDate?: string and uses StatsWithMonthly which includes the monthly contributions data needed by StatsChart.

app/[lang]/leaderboard/TopTierUsers.tsx (1)

94-103: GitHub avatar domain is already properly configured.

The next.config.js file includes the correct images.remotePatterns configuration for avatars.githubusercontent.com with the https protocol and /u/** pathname pattern, which matches GitHub avatar URLs. No configuration changes are needed.

Comment on lines 23 to 39
useEffect(() => {
const fetchTopUsers = async () => {
try {
const response = await fetch(API_TOP_TIER_USERS);
const data = await response.json();
if (data.users) {
setTopTierUsers(data.users);
}
} catch (error) {
console.error('Failed to fetch top tier users:', error);
} finally {
setIsLoading(false);
}
};

fetchTopUsers();
}, []);
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

Missing response status check before parsing JSON.

The fetch doesn't verify response.ok before calling response.json(). If the API returns a non-2xx status (e.g., 404, 500), parsing may fail or return unexpected data.

Apply this diff to add proper response handling:

   useEffect(() => {
     const fetchTopUsers = async () => {
       try {
         const response = await fetch(API_TOP_TIER_USERS);
+        if (!response.ok) {
+          throw new Error(`HTTP error: ${response.status}`);
+        }
         const data = await response.json();
         if (data.users) {
           setTopTierUsers(data.users);
         }
       } catch (error) {
         console.error('Failed to fetch top tier users:', error);
       } finally {
         setIsLoading(false);
       }
     };

     fetchTopUsers();
   }, []);
📝 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
useEffect(() => {
const fetchTopUsers = async () => {
try {
const response = await fetch(API_TOP_TIER_USERS);
const data = await response.json();
if (data.users) {
setTopTierUsers(data.users);
}
} catch (error) {
console.error('Failed to fetch top tier users:', error);
} finally {
setIsLoading(false);
}
};
fetchTopUsers();
}, []);
useEffect(() => {
const fetchTopUsers = async () => {
try {
const response = await fetch(API_TOP_TIER_USERS);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
if (data.users) {
setTopTierUsers(data.users);
}
} catch (error) {
console.error('Failed to fetch top tier users:', error);
} finally {
setIsLoading(false);
}
};
fetchTopUsers();
}, []);
🤖 Prompt for AI Agents
In app/[lang]/leaderboard/TopTierUsers.tsx around lines 23 to 39, the fetch
handler parses response.json() without checking response.ok; update
fetchTopUsers to first check response.ok after awaiting fetch, and if not ok
read response.text() (or response.json() guarded in try) and throw or log a
descriptive error including status and body so we don't attempt to use
unexpected data; only call setTopTierUsers with data.users when the response is
ok and parsed successfully, keep the existing catch/finally to handle and
surface errors and ensure setIsLoading(false) still runs.

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

♻️ Duplicate comments (5)
app/[lang]/leaderboards/TopTierUsers.tsx (1)

68-70: Empty state could return null for clarity.

Returning an empty fragment <></> works, but returning null is more explicit and semantically clearer for "render nothing" scenarios.

   if (topTierUsers.length === 0) {
-    return <></>;
+    return null;
   }
app/[lang]/stats/[login]/Scouter/StatsDetails/StatsChart.tsx (2)

82-104: Month format validation looks good.

The validation for invalid month indices (NaN, out of range) with a fallback to the original string addresses the previous review concern.


44-48: Type safety improved for legend handlers.

Using LegendPayload instead of any addresses the previous type safety concern from the past review.

app/[lang]/stats/[login]/Scouter/StatsDetails/SectionDooboo.tsx (2)

31-42: Loading state management improved with ref pattern.

The ref-based approach addresses the previous ESLint warning and race condition concerns. Tracking pendingEndDateRef and only clearing loading when the incoming endDate matches (or on initial mount) is a sound solution.


44-56: Navigation handler looks correct.

The callback properly sets the pending ref before navigation and triggers loading state. The dependency array correctly includes pathname and router.

🧹 Nitpick comments (6)
server/services/githubService.ts (1)

402-428: Consider validating startDate in generateMonths.

The generateMonths function assumes startDate is a valid YYYY-MM string. If called with a malformed input, year and month will be NaN, leading to unexpected behavior in the loop. Although getGithubUser validates its startDate, generateMonths is called through a different path via getMonthlyContributions.

Consider adding validation consistent with getGithubUser:

 const generateMonths = (startDate: string): string[] => {
   const months: string[] = [];
   const [year, month] = startDate.split('-').map(Number);
+
+  // Validate startDate format
+  if (!Number.isFinite(year) || !Number.isFinite(month) || month < 1 || month > 12) {
+    // Fallback to current month minus 12
+    const now = new Date();
+    const fallbackYear = now.getUTCFullYear();
+    const fallbackMonth = now.getUTCMonth(); // 0-indexed, so this is 12 months ago
+    return generateMonths(`${fallbackYear}-${String(fallbackMonth + 1).padStart(2, '0')}`);
+  }
 
   // Get current year and month in UTC
   const now = new Date();
app/[lang]/stats/[login]/Scouter/StatsDetails/StatsChart.tsx (1)

119-128: Consider edge case when all lines are inactive.

If activeLines becomes empty (which shouldn't happen with current logic, but defensively), maxValue would be -Infinity from Math.max() on an empty spread. The || 10 fallback on yAxisMax partially handles this, but Math.ceil(-Infinity * 1.1) returns -Infinity, not a falsy value.

   const maxValue = Math.max(
+    0,
     ...chartData.flatMap((d) => {
       const values: number[] = [];
       if (activeLines.has('commits')) values.push(d.commits);
       if (activeLines.has('pullRequests')) values.push(d.pullRequests);
       if (activeLines.has('reviews')) values.push(d.reviews);
       return values.length > 0 ? values : [0];
     }),
   );
app/[lang]/leaderboards/GithubUserList.tsx (2)

110-110: Unsafe type cast for tierName.

Casting tierName as Tier without validation could cause issues if the API returns an unexpected tier value. Consider adding a type guard or fallback.

-        cell: ({tierName}) => <TierRowItem tier={tierName as Tier} />,
+        cell: ({tierName}) => (
+          <TierRowItem tier={TIER_ORDER.includes(tierName as Tier) ? (tierName as Tier) : 'Iron'} />
+        ),

234-237: Consider using Next.js Link for navigation.

Opening GitHub profiles via window.open works, but using a standard anchor tag or Next.js Link would be more accessible and allow users to right-click for context menu options.

app/[lang]/stats/[login]/SearchTextInput.tsx (2)

42-50: Consider removing requestAnimationFrame for simpler loading state reset.

The requestAnimationFrame wrapper defers the check by one frame but doesn't provide a clear benefit here. The effect already runs after the component renders with the new initialValue, so the check can be synchronous:

- useEffect(() => {
-   const frame = requestAnimationFrame(() => {
-     if (pendingLoginRef.current && initialValue === pendingLoginRef.current) {
-       pendingLoginRef.current = null;
-       setIsLoading(false);
-     }
-   });
-   return () => cancelAnimationFrame(frame);
- }, [initialValue]);
+ useEffect(() => {
+   if (pendingLoginRef.current && initialValue === pendingLoginRef.current) {
+     pendingLoginRef.current = null;
+     setIsLoading(false);
+   }
+ }, [initialValue]);

This reduces complexity without changing behavior.


52-61: Consider adding a timeout to reset loading state.

If navigation fails or is interrupted, isLoading could remain true indefinitely, leaving the button disabled. A timeout provides a safety net:

 const navigateTo = (loginValue: string) => {
   if (loginValue === initialValue) {
     return;
   }

   pendingLoginRef.current = loginValue;
   setIsLoading(true);
   router.push(`/${lang}/stats/${loginValue}`);
+  
+  // Safety timeout in case navigation fails
+  setTimeout(() => {
+    if (pendingLoginRef.current === loginValue) {
+      pendingLoginRef.current = null;
+      setIsLoading(false);
+    }
+  }, 5000);
 };

While Next.js navigation rarely fails, this ensures better UX resilience.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8baaef7 and 7fea141.

📒 Files selected for processing (13)
  • CLAUDE.md (1 hunks)
  • app/[lang]/(common)/Header/index.tsx (5 hunks)
  • app/[lang]/leaderboards/GithubUserList.tsx (1 hunks)
  • app/[lang]/leaderboards/TopTierUsers.tsx (1 hunks)
  • app/[lang]/leaderboards/apiRoutes.ts (1 hunks)
  • app/[lang]/leaderboards/page.tsx (3 hunks)
  • app/[lang]/stats/[login]/Scouter/StatsDetails/SectionDooboo.tsx (5 hunks)
  • app/[lang]/stats/[login]/Scouter/StatsDetails/StatsChart.tsx (1 hunks)
  • app/[lang]/stats/[login]/SearchTextInput.tsx (4 hunks)
  • locales/en.json (4 hunks)
  • locales/ko.json (3 hunks)
  • server/services/githubService.ts (13 hunks)
  • styles/output.css (41 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/[lang]/(common)/Header/index.tsx
🧰 Additional context used
🧬 Code graph analysis (5)
app/[lang]/leaderboards/page.tsx (3)
src/localization.ts (1)
  • getTranslates (17-23)
app/[lang]/leaderboards/TopTierUsers.tsx (1)
  • TopTierUsers (19-135)
app/[lang]/leaderboards/GithubUserList.tsx (1)
  • GithubUserList (39-249)
app/[lang]/stats/[login]/SearchTextInput.tsx (1)
src/hooks/useSearchHistory.ts (1)
  • useSearchHistory (26-65)
app/[lang]/stats/[login]/Scouter/StatsDetails/SectionDooboo.tsx (3)
app/[lang]/stats/[login]/Scouter/index.tsx (2)
  • statNames (18-25)
  • StatsWithMonthly (27-29)
app/[lang]/stats/[login]/MonthPicker.tsx (1)
  • MonthPicker (29-226)
app/[lang]/stats/[login]/Scouter/StatsDetails/StatsChart.tsx (1)
  • StatsChart (25-217)
app/[lang]/stats/[login]/Scouter/StatsDetails/StatsChart.tsx (1)
server/services/githubService.ts (1)
  • MonthlyContribution (377-383)
server/services/githubService.ts (2)
server/plugins/types/UserGraph.ts (1)
  • UserGraph (1-31)
server/plugins/types/AuthorCommits.ts (1)
  • AuthorCommits (1-5)
🪛 Biome (2.1.2)
styles/output.css

[error] 1508-1508: Duplicate properties can lead to unexpected behavior and may override previous declarations unintentionally.

min-width is already defined here.

Remove or rename the duplicate property to ensure consistent styling.

(lint/suspicious/noDuplicateProperties)

🔇 Additional comments (21)
CLAUDE.md (1)

1-98: LGTM! Comprehensive coding guidelines documentation.

This new documentation file provides clear conventions for the project including development commands, project structure, and Tailwind-based styling guidelines. The dark mode patterns, color system documentation, and component guidelines will help maintain consistency across the codebase.

styles/output.css (1)

1506-1509: Auto-generated Tailwind CSS - no action needed.

The static analysis hint about duplicate min-width properties is a false positive. The -moz-fit-content followed by fit-content is standard cross-browser CSS generated by Tailwind for vendor prefix support. This is expected behavior in generated CSS output.

server/services/githubService.ts (4)

49-78: Good implementation of startDate validation.

The validation logic properly handles malformed startDate inputs by checking for finite values and valid month ranges (1-12), with a sensible fallback to 12 months ago. This addresses the previous review feedback.


438-461: Good batching strategy with per-item error handling.

The batching approach (3 concurrent requests at a time) with individual error handling that falls back to zero values is robust. This prevents a single failed month from breaking the entire request.


547-611: Good cache protection for custom date queries.

The conditional gating (if (!startDate)) prevents custom date range queries from overwriting the cached default data in the database. This maintains cache integrity while still allowing flexible date-based queries.


822-844: Clean public API with proper error handling.

The getMonthlyContribution function properly normalizes the login, delegates to the internal implementation, and handles errors gracefully by logging context and returning an empty array.

app/[lang]/leaderboards/apiRoutes.ts (1)

1-2: LGTM! Clean API route constants.

Centralizing API route constants promotes consistency and makes refactoring easier. These are appropriately consumed by the leaderboard components.

app/[lang]/leaderboards/page.tsx (1)

38-79: Well-structured responsive layout.

The updated layout properly integrates the TopTierUsers component with horizontal scrolling and responsive breakpoints. The flex layout with min-w-0 and flex-1 ensures proper overflow handling.

app/[lang]/leaderboards/TopTierUsers.tsx (3)

23-42: Good data fetching pattern with error handling.

The useEffect properly handles the fetch lifecycle with loading state, error handling, and cleanup via finally. The error is logged for debugging while the UI gracefully handles the failure.


44-65: Nice loading skeleton implementation.

The skeleton loading state with pulse animation provides good UX feedback while data is being fetched. The structure mirrors the final rendered content, preventing layout shift.


82-131: LGTM! Well-designed user tile cards.

The user tiles have appropriate accessibility attributes (target="_blank", rel="noopener noreferrer"), good hover interactions with scale transitions, and proper truncation for long usernames. The use of Next.js Image component ensures optimized image loading.

app/[lang]/stats/[login]/Scouter/StatsDetails/StatsChart.tsx (3)

1-24: LGTM! Well-structured imports and type definitions.

The component is properly marked as client-side, imports are organized, and the Props type clearly defines the expected data shape with optional properties.


36-42: Good hydration mismatch prevention.

Using requestAnimationFrame to defer setIsClient(true) is a solid approach to avoid React 19 hydration warnings while ensuring the chart only renders client-side.


130-216: Clean chart implementation with good UX.

The loading state visual feedback (opacity + blur), responsive width calculation, and interactive legend behavior are well implemented.

app/[lang]/leaderboards/GithubUserList.tsx (1)

77-125: Good use of useMemo for column definitions.

The column definitions are properly memoized with appropriate dependencies, and the table structure is well-organized.

locales/ko.json (2)

96-105: Localization updates look consistent.

The Korean translations for the new leaderboards section are well-structured and align with the English counterparts.


120-124: Stats localization additions look good.

The new stats-related Korean translations (selectPeriod, periodDescription, resetToDefault, search) are properly added and provide clear user-facing text.

app/[lang]/stats/[login]/Scouter/StatsDetails/SectionDooboo.tsx (2)

138-148: Good integration of MonthPicker and StatsChart.

The MonthPicker is positioned between scores and chart with proper props, and StatsChart receives the monthly contributions data with loading state.


199-215: Clean component composition with endDate prop forwarding.

The destructuring pattern separating endDate from other props and forwarding it explicitly to SectionHeader is clear and maintainable.

locales/en.json (2)

87-96: Leaderboards localization section looks good.

The new keys (all, topUsers, topRanked) and updated text (noRecentList → "No users found") are well-structured.


111-115: Stats localization additions are complete.

The new period selection and search-related strings are properly added and provide clear guidance for users.

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: 1

♻️ Duplicate comments (1)
app/[lang]/stats/[login]/SearchTextInput.tsx (1)

112-116: aria-label doesn't differentiate loading and idle states.

Both branches of the ternary use t.search, so screen-reader users won't know the button is searching. Update to provide distinct feedback:

-          aria-label={
-            isLoading
-              ? t.search || 'Search'
-              : t.search || 'Search'
-          }
+          aria-label={
+            isLoading
+              ? 'Searching...'
+              : t.search || 'Search'
+          }

If translations.searching or a similar key exists in the localization files, prefer that over the hardcoded string.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7fea141 and 4c6db0e.

📒 Files selected for processing (2)
  • app/[lang]/leaderboards/GithubUserList.tsx (1 hunks)
  • app/[lang]/stats/[login]/SearchTextInput.tsx (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/[lang]/stats/[login]/SearchTextInput.tsx (1)
src/hooks/useSearchHistory.ts (1)
  • useSearchHistory (26-65)
🔇 Additional comments (6)
app/[lang]/leaderboards/GithubUserList.tsx (2)

51-75: Past issue resolved: error handling implemented correctly.

The response status check and error handling have been properly implemented as suggested in the previous review. The code now verifies response.ok before parsing JSON and handles errors gracefully.


130-167: Past issue resolved: error handling and state management improved.

The scroll handler now properly wraps the async fetch in try-catch and uses a functional state updater to avoid stale closure issues. The cursor logic is also correctly managed.

app/[lang]/stats/[login]/SearchTextInput.tsx (4)

33-34: Language extraction assumes Next.js route structure.

The extraction pathname?.split('/')[1] correctly handles the app/[lang]/stats/[login]/ structure. The fallback to 'en' is appropriate for undefined pathnames. Given Next.js routing guarantees, the [lang] segment should always be present, making this implementation safe.


37-50: LGTM! State synchronization and loading reset are well-implemented.

Both effects correctly address the previous review feedback:

  • Lines 37-39 keep the input synchronized with route changes
  • Lines 42-50 use requestAnimationFrame to defer the loading state reset, avoiding ESLint violations while ensuring the reset happens only when navigation completes

The ref-based pattern properly tracks pending navigation and prevents race conditions.


52-61: LGTM! Navigation logic is correct and handles edge cases.

The guard at lines 54-56 correctly prevents the loading state from getting stuck when searching for the same user. The language-aware path construction and ref-based tracking ensure smooth navigation between stats pages.


125-129: LGTM! Spinner implementation follows project conventions.

The spinner correctly uses Tailwind's animate-spin utility with semantic color classes (border-gray-300, border-t-brand) and proper dark-mode variants. This aligns with the project's styling approach seen elsewhere in the codebase.

@hyochan hyochan merged commit a3c4e04 into main Nov 28, 2025
4 checks passed
@hyochan hyochan deleted the feat/stats-list-renewal branch November 28, 2025 17:04
@lulunac27a
Copy link

lulunac27a commented Dec 3, 2025

Add support for numbered rank like ranked nth out of total number of users. Also, the scores for each metric is different when an end date has been selected because it calculates the data from 12 months before the specific end date to the specific end date, not from the last 12 months. These scores should not be different when an end date has been selected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💎 feature New features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants