From b23f312fc1e95101666bdc4cbef891ba07b14c02 Mon Sep 17 00:00:00 2001 From: Mark Saroufim Date: Sun, 12 Apr 2026 20:39:14 -0700 Subject: [PATCH] Hide time remaining for always-open leaderboards --- frontend/src/lib/date/utils.test.ts | 35 ++++++++++++++- frontend/src/lib/date/utils.ts | 13 ++++++ frontend/src/pages/home/Home.test.tsx | 44 +++++++++++++++++++ frontend/src/pages/home/Home.tsx | 8 +++- .../pages/home/components/LeaderboardTile.tsx | 5 ++- 5 files changed, 100 insertions(+), 5 deletions(-) diff --git a/frontend/src/lib/date/utils.test.ts b/frontend/src/lib/date/utils.test.ts index 773d3cd2..6d7d5feb 100644 --- a/frontend/src/lib/date/utils.test.ts +++ b/frontend/src/lib/date/utils.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { getTimeLeft, toDateUtc } from "./utils"; +import { getTimeLeft, shouldHideTimeRemaining, toDateUtc } from "./utils"; describe("getTimeLeft", () => { beforeEach(() => { @@ -178,3 +178,36 @@ describe("toDateUtc", () => { }); }); }); + +describe("shouldHideTimeRemaining", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("hides countdowns for deadlines more than a year away", () => { + vi.setSystemTime(new Date("2025-03-24T00:00:00.000Z")); + + expect( + shouldHideTimeRemaining("2026-03-25T00:00:01.000Z"), + ).toBe(true); + }); + + it("keeps countdowns for deadlines within a year", () => { + vi.setSystemTime(new Date("2025-03-24T00:00:00.000Z")); + + expect( + shouldHideTimeRemaining("2026-03-24T00:00:00.000Z"), + ).toBe(false); + }); + + it("does not hide past or invalid deadlines", () => { + vi.setSystemTime(new Date("2025-03-24T00:00:00.000Z")); + + expect(shouldHideTimeRemaining("2025-03-23T23:59:59.000Z")).toBe(false); + expect(shouldHideTimeRemaining("gibberish")).toBe(false); + }); +}); diff --git a/frontend/src/lib/date/utils.ts b/frontend/src/lib/date/utils.ts index cd6f97bf..612b6aef 100644 --- a/frontend/src/lib/date/utils.ts +++ b/frontend/src/lib/date/utils.ts @@ -1,5 +1,7 @@ import dayjs from "./dayjs"; +const ALWAYS_OPEN_DEADLINE_THRESHOLD_DAYS = 365; + export const toDateUtc = (raw: string) => { return dayjs(raw).utc().format("YYYY-MM-DD HH:mm"); }; @@ -32,6 +34,17 @@ export const getTimeLeft = (deadline: string): string => { return `${days} ${dayLabel} ${hours} ${hourLabel} remaining`; }; +export const shouldHideTimeRemaining = (deadline: string): boolean => { + const now = dayjs().utc(); + const deadlineDate = dayjs(deadline); + + if (!deadlineDate.isValid() || deadlineDate.isSame(now) || deadlineDate.isBefore(now)) { + return false; + } + + return deadlineDate.diff(now, "day", true) > ALWAYS_OPEN_DEADLINE_THRESHOLD_DAYS; +}; + export const isExpired = ( deadline: string | Date, time: Date = new Date(), diff --git a/frontend/src/pages/home/Home.test.tsx b/frontend/src/pages/home/Home.test.tsx index 789ad156..11283ec5 100644 --- a/frontend/src/pages/home/Home.test.tsx +++ b/frontend/src/pages/home/Home.test.tsx @@ -24,6 +24,7 @@ vi.mock("react-syntax-highlighter/dist/esm/styles/prism", () => ({ vi.mock("../../lib/date/utils", () => ({ getTimeLeft: vi.fn(() => "2 days 5 hours remaining"), isExpired: vi.fn(() => false), + shouldHideTimeRemaining: vi.fn(() => false), })); vi.mock("../../lib/utils/ranking", () => ({ @@ -461,6 +462,49 @@ describe("Home", () => { expect(screen.getByText("2 days 5 hours remaining")).toBeInTheDocument(); }); + it("hides time left for effectively always-open leaderboards", () => { + vi.mocked(dateUtils.shouldHideTimeRemaining).mockReturnValue(true); + + const mockData = { + leaderboards: [ + { + id: 1, + name: "grayscale_v2", + deadline: "2027-12-31T23:59:59Z", + gpu_types: ["T4"], + priority_gpu_type: "T4", + top_users: [ + { + rank: 1, + score: 0.123, + user_name: "alice", + }, + ], + }, + ], + now: "2025-01-01T00:00:00Z", + }; + + const mockHookReturn = { + data: mockData, + loading: false, + hasLoaded: true, + error: null, + errorStatus: null, + call: mockCall, + }; + + (apiHook.fetcherApiCallback as ReturnType).mockReturnValue( + mockHookReturn, + ); + + renderWithProviders(); + + expect( + screen.queryByText("2 days 5 hours remaining"), + ).not.toBeInTheDocument(); + }); + it("formats scores correctly", () => { const mockData = { leaderboards: [ diff --git a/frontend/src/pages/home/Home.tsx b/frontend/src/pages/home/Home.tsx index 923a790b..6e27f6f6 100644 --- a/frontend/src/pages/home/Home.tsx +++ b/frontend/src/pages/home/Home.tsx @@ -23,7 +23,7 @@ import Loading from "../../components/common/loading"; import { ConstrainedContainer } from "../../components/app-layout/ConstrainedContainer"; import MarkdownRenderer from "../../components/markdown-renderer/MarkdownRenderer"; import quickStartMarkdown from "./quick-start.md?raw"; -import { isExpired, getTimeLeft } from "../../lib/date/utils"; +import { isExpired, getTimeLeft, shouldHideTimeRemaining } from "../../lib/date/utils"; import { ColoredSquare } from "../../components/common/ColoredSquare"; import ArrowOutwardIcon from "@mui/icons-material/ArrowOutward"; @@ -104,6 +104,10 @@ export default function Home() { navigate(`/leaderboard/${id}/editor`); }; + const getLeaderboardTimeRemaining = (deadline: string) => { + return shouldHideTimeRemaining(deadline) ? undefined : getTimeLeft(deadline); + }; + return ( @@ -145,7 +149,7 @@ export default function Home() { {lb.name} } - secondary={getTimeLeft(lb.deadline)} + secondary={getLeaderboardTimeRemaining(lb.deadline)} slotProps={{ primary: { fontWeight: 500, diff --git a/frontend/src/pages/home/components/LeaderboardTile.tsx b/frontend/src/pages/home/components/LeaderboardTile.tsx index 485eda91..0478eee1 100644 --- a/frontend/src/pages/home/components/LeaderboardTile.tsx +++ b/frontend/src/pages/home/components/LeaderboardTile.tsx @@ -1,7 +1,7 @@ import { Box, Card, CardContent, Chip, type Theme, Typography } from "@mui/material"; import { Link } from "react-router-dom"; import { getMedalIcon } from "../../../components/common/medal.tsx"; -import { getTimeLeft } from "../../../lib/date/utils.ts"; +import { getTimeLeft, shouldHideTimeRemaining } from "../../../lib/date/utils.ts"; import { formatMicroseconds } from "../../../lib/utils/ranking.ts"; import { ColoredSquare } from "../../../components/common/ColoredSquare.tsx"; @@ -68,6 +68,7 @@ interface LeaderboardTileProps { export default function LeaderboardTile({ leaderboard, expired }: LeaderboardTileProps) { const timeLeft = getTimeLeft(leaderboard.deadline); + const hideTimeRemaining = shouldHideTimeRemaining(leaderboard.deadline); return ( {/* Time Left */} - {!expired && ( + {!expired && !hideTimeRemaining && ( {timeLeft}