Skip to content

Conversation

@Doezer
Copy link
Owner

@Doezer Doezer commented Feb 10, 2026

This pull request introduces a new statistics feature for the game library, including a dedicated statistics page, reusable stats calculation logic, and UI enhancements. The main changes are the addition of a robust stats utility, integration of the stats page into the app navigation, updates to the dashboard to use the new stats logic, and comprehensive tests to ensure correctness.

Statistics Feature Implementation:

  • Added a new StatsPage (client/src/pages/stats.tsx) that displays detailed library statistics, including total games, average rating, completion rate, metadata health, status breakdown (with pie chart), and quick info for top genre, platform, publisher, unique developers, and average release year.
  • Created a reusable stats calculation utility, calculateLibraryStats, in client/src/lib/stats.ts, which computes all relevant metrics for the library and exposes them via a structured interface.
  • Added comprehensive tests for calculateLibraryStats in client/src/lib/__tests__/stats.test.ts to verify correctness for various scenarios, including empty and incomplete libraries.

App Integration and UI Updates:

  • Integrated the statistics page into the router and sidebar navigation, including lazy loading and a new "Stats" menu item with an appropriate icon. [1] [2] [3] [4]
  • Updated the dashboard (client/src/components/Dashboard.tsx) to use the new calculateLibraryStats utility for statistics, simplifying and consolidating stats logic. Also adjusted the dashboard stats card layout for improved clarity. [1] [2] [3]

Minor Workflow Change:

  • Removed the production tag based on package version from the Docker build tags in .github/workflows/deploy.yml.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @Doezer, 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 significantly enhances the application by adding a robust statistics feature. It provides users with a dedicated page to view detailed insights into their game library, including various metrics like average rating, completion rate, and metadata health, along with breakdowns by status, genre, and platform. The implementation centralizes the statistics calculation logic into a new utility, improving maintainability and consistency across the application, while also updating existing components like the Dashboard to leverage this new functionality.

Highlights

  • New Statistics Page: Introduced a new dedicated 'Stats' page (client/src/pages/stats.tsx) to display comprehensive game library statistics, including total games, average rating, completion rate, metadata health, status breakdown (with a pie chart), and quick info for top genre, platform, publisher, unique developers, and average release year.
  • Reusable Statistics Utility: Developed a reusable calculateLibraryStats utility (client/src/lib/stats.ts) that centralizes the logic for computing all relevant metrics for the game library, ensuring consistent data across the application.
  • Application Integration and UI Updates: Integrated the new statistics page into the application's router and sidebar navigation, including lazy loading and a new 'Stats' menu item. The Dashboard component (client/src/components/Dashboard.tsx) was refactored to utilize the new calculateLibraryStats utility, simplifying its logic and adjusting its stats card layout for improved clarity.
  • Comprehensive Testing: Added extensive unit tests for the calculateLibraryStats utility (client/src/lib/__tests__/stats.test.ts) to verify its correctness across various scenarios, including empty and incomplete libraries.
Changelog
  • client/src/App.tsx
    • Imported the new StatsPage component for lazy loading.
    • Added a new route for /stats to display the StatsPage.
    • Updated the title mapping to show 'Statistics' when on the /stats route.
  • client/src/components/AppSidebar.tsx
    • Imported the PieChart icon from lucide-react.
    • Added a new navigation item for 'Stats' with the PieChart icon, linking to /stats.
  • client/src/components/Dashboard.tsx
    • Imported the calculateLibraryStats utility.
    • Replaced the manual, in-component statistics calculation with a call to calculateLibraryStats.
    • Adjusted the layout of the stats cards on the dashboard to a 1-column (mobile) to 3-column (desktop) grid.
    • Updated the displayed statistics to reflect the output of the new utility, focusing on total games, average rating, and metadata health.
  • client/src/lib/tests/stats.test.ts
    • Added a new test file containing unit tests for the calculateLibraryStats function.
    • Included tests for calculating stats on a mixed library, an empty library, and libraries with missing optional fields, as well as specific metadata health calculations.
  • client/src/lib/stats.ts
    • Added a new utility file containing the calculateLibraryStats function.
    • Defined the LibraryStats interface to type the output of the statistics calculation.
    • Implemented logic to calculate total games, average rating, top genre, top platform, top publisher, unique developers, average release year, metadata health, status breakdown, and completion rate.
  • client/src/pages/stats.tsx
    • Added a new page component, StatsPage, responsible for rendering the library statistics.
    • Fetches game data using useQuery.
    • Utilizes calculateLibraryStats to process the game data.
    • Displays key statistics using StatsCard components.
    • Includes a PieChart to visualize the status breakdown of games.
    • Presents 'Quick Info' cards for top genre, platform, publisher, unique developers, and average release year.
Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/deploy.yml
Activity
  • No human activity has been recorded on this pull request yet.
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.

@codecov
Copy link

codecov bot commented Feb 10, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

Copy link
Contributor

@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 pull request introduces a new statistics page and refactors the existing stats logic into a reusable utility. The changes are well-structured, with the new logic in client/src/lib/stats.ts and comprehensive tests. The new stats page provides a great overview of the user's library. My review includes a few suggestions to improve correctness, performance, and maintainability. I've identified a bug in the average release year calculation, suggested performance improvements for status breakdown, and pointed out ways to make the code more deterministic and cleaner. Overall, this is a great addition.

Comment on lines +68 to +77
const datedGames = games.filter((g) => g.releaseDate);
const avgReleaseYear =
datedGames.length > 0
? Math.round(
datedGames.reduce((acc, g) => {
const year = new Date(g.releaseDate!).getFullYear();
return acc + (isNaN(year) ? 0 : year);
}, 0) / datedGames.length
)
: "N/A";
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The current implementation for calculating avgReleaseYear can produce an incorrect average if any releaseDate string is invalid. When new Date(g.releaseDate!).getFullYear() results in NaN, your code adds 0 to the sum but still includes the game in the datedGames.length divisor, which skews the result.

A more robust approach is to first map all dates to their year, then filter out any invalid NaN values before calculating the average. This ensures the average is only calculated over valid years.

  const years = games
    .map((g) => (g.releaseDate ? new Date(g.releaseDate).getFullYear() : NaN))
    .filter((year) => !isNaN(year));
  const avgReleaseYear =
    years.length > 0
      ? Math.round(years.reduce((acc, year) => acc + year, 0) / years.length)
      : "N/A";

Comment on lines +53 to +55
expect(stats.topGenre?.name).toBe("RPG"); // Action (2), RPG (2) - RPG comes after Action in sort if counts are equal? No, sort order of entries is based on appearance or stable sort. Wait, Action:2, RPG:2. My implementation: sorted[0][0]. RPG:2, Action:2.
// Let's re-verify the sort: .sort((a,b) => b[1]-a[1]). If equal, order is preserved from original entries.
// Action (2), RPG (2).
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

This comment regarding the topGenre calculation appears to be based on an incorrect count of genres in the mock data (Action (2), RPG (2)) and adds confusion about the sorting logic. Based on the provided mockGames, the counts are actually RPG: 3 and Action: 1. Removing this misleading comment would improve the clarity and correctness of the test.

Suggested change
expect(stats.topGenre?.name).toBe("RPG"); // Action (2), RPG (2) - RPG comes after Action in sort if counts are equal? No, sort order of entries is based on appearance or stable sort. Wait, Action:2, RPG:2. My implementation: sorted[0][0]. RPG:2, Action:2.
// Let's re-verify the sort: .sort((a,b) => b[1]-a[1]). If equal, order is preserved from original entries.
// Action (2), RPG (2).
expect(stats.topGenre?.name).toBe("RPG");

if (items.length === 0) return null;
const counts: Record<string, number> = {};
items.forEach((item) => (counts[item] = (counts[item] || 0) + 1));
const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]);
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The sort function (a, b) => b[1] - a[1] is not stable. If two items have the same count, their returned order is not guaranteed across JavaScript engines, which can lead to non-deterministic results for "Top" items (e.g., Top Genre). To ensure consistent and predictable results, you can add a secondary sort criterion, such as sorting by name alphabetically.

Suggested change
const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]);
const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));

Comment on lines +86 to +91
const statusBreakdown = {
wanted: games.filter((g) => g.status === "wanted").length,
owned: games.filter((g) => g.status === "owned").length,
completed: games.filter((g) => g.status === "completed").length,
downloading: games.filter((g) => g.status === "downloading").length,
};
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The statusBreakdown is calculated by filtering the games array four separate times. For large game libraries, this can be inefficient as it requires multiple passes over the data. A more performant approach would be to use a single loop (e.g., with reduce or forEach) to iterate over the games array only once and build the statusBreakdown object.

} from "recharts";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";

const COLORS = ["#ef4444", "#3b82f6", "#10b981", "#8b5cf6"]; // wanted, owned, completed, downloading
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The COLORS constant is declared but never used in this component. The pie chart uses the chartColors object for its color scheme. This unused constant should be removed to avoid dead code.

Comment on lines +39 to +48
queryFn: async () => {
const token = localStorage.getItem("token");
const headers: Record<string, string> = {};
if (token) {
headers["Authorization"] = `Bearer ${token}`;
}
const response = await fetch("/api/games?includeHidden=true", { headers });
if (!response.ok) throw new Error("Failed to fetch games");
return response.json();
},
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The queryFn for fetching games duplicates logic for handling authentication tokens, which is also present in other components like Dashboard.tsx. To improve maintainability and adhere to the DRY (Don't Repeat Yourself) principle, this data fetching logic should be centralized. Consider creating a shared API client function that handles auth headers and can be reused across all useQuery calls that require authentication.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant