diff --git a/client/__tests__/CalendarPage.test.tsx b/client/__tests__/CalendarPage.test.tsx index 5869f298..51600eab 100644 --- a/client/__tests__/CalendarPage.test.tsx +++ b/client/__tests__/CalendarPage.test.tsx @@ -144,6 +144,29 @@ describe("CalendarPage", () => { }); }); + it("greys out past months and past releases in the year view", async () => { + mockGamesFetch([ + { ...baseGame, id: "game-5", title: "April Game", releaseDate: "2026-04-10" }, + { ...baseGame, id: "game-6", title: "Early May Game", releaseDate: "2026-05-03" }, + { ...baseGame, id: "game-7", title: "June Game", releaseDate: "2026-06-02" }, + { ...baseGame, id: "game-8", title: "Datetime Game", releaseDate: "2026-05-10T12:00:00Z" }, + ]); + + renderPage(); + + const aprilGame = await screen.findByText("April Game"); + const aprilCard = screen.getByRole("heading", { name: "April" }).closest(".bg-card"); + const juneCard = screen.getByRole("heading", { name: "June" }).closest(".bg-card"); + + expect(aprilCard).toHaveClass("opacity-50"); + expect(juneCard).not.toHaveClass("opacity-50"); + // Inside a past month the card is already dimmed, so its badges stay as-is. + expect(aprilGame.closest("button")).not.toHaveClass("opacity-50"); + expect(screen.getByText("Early May Game").closest("button")).toHaveClass("opacity-50"); + expect(screen.getByText("June Game").closest("button")).not.toHaveClass("opacity-50"); + expect(screen.getByText("Datetime Game").closest("button")).toHaveClass("opacity-50"); + }); + it("switches to week view and opens the download dialog from a visible game", async () => { mockGamesFetch([{ ...baseGame, id: "game-4", title: "Week Hero", releaseDate: "2026-05-29" }]); diff --git a/client/src/pages/calendar.tsx b/client/src/pages/calendar.tsx index 8407aad0..e408cb32 100644 --- a/client/src/pages/calendar.tsx +++ b/client/src/pages/calendar.tsx @@ -32,6 +32,12 @@ export function formatDate(date: Date): string { return `${y}-${m}-${d}`; } +// Release dates are usually date-only ("YYYY-MM-DD"), but a claimed download can carry a full +// ISO datetime. Map those to the viewer's local calendar day before comparing with today. +function toLocalDateKey(releaseDate: string): string { + return /^\d{4}-\d{2}-\d{2}$/.test(releaseDate) ? releaseDate : formatDate(new Date(releaseDate)); +} + function getMonthName(month: number): string { return new Date(2000, month, 1).toLocaleDateString(undefined, { month: "long" }); } @@ -262,6 +268,7 @@ export default function CalendarPage() { currentDate={currentDate} gamesByDate={gamesByDate} onGameClick={handleGameClick} + today={today} /> )} {viewMode === "month" && ( @@ -305,13 +312,17 @@ function YearView({ currentDate, gamesByDate, onGameClick, + today, }: { currentDate: Date; gamesByDate: GamesByDate; onGameClick: (game: Game) => void; + today: Date; }) { const year = currentDate.getFullYear(); const months = Array.from({ length: 12 }, (_, i) => i); + const todayKey = formatDate(today); + const currentMonthPrefix = todayKey.slice(0, 8); // Pre-calculate entries once outside the render loop const allGamesEntries = useMemo(() => Object.entries(gamesByDate), [gamesByDate]); @@ -332,8 +343,16 @@ function YearView({ } } + const isPastMonth = monthPrefix < currentMonthPrefix; + return ( -
+

{getMonthName(month)}

{gameCount > 0 && ( @@ -353,7 +372,12 @@ function YearView({ })}
{games.map((game) => ( - onGameClick(game)} /> + onGameClick(game)} + /> ))}
))