Skip to content

Commit b23f312

Browse files
committed
Hide time remaining for always-open leaderboards
1 parent cd945d5 commit b23f312

5 files changed

Lines changed: 100 additions & 5 deletions

File tree

frontend/src/lib/date/utils.test.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2-
import { getTimeLeft, toDateUtc } from "./utils";
2+
import { getTimeLeft, shouldHideTimeRemaining, toDateUtc } from "./utils";
33

44
describe("getTimeLeft", () => {
55
beforeEach(() => {
@@ -178,3 +178,36 @@ describe("toDateUtc", () => {
178178
});
179179
});
180180
});
181+
182+
describe("shouldHideTimeRemaining", () => {
183+
beforeEach(() => {
184+
vi.useFakeTimers();
185+
});
186+
187+
afterEach(() => {
188+
vi.useRealTimers();
189+
});
190+
191+
it("hides countdowns for deadlines more than a year away", () => {
192+
vi.setSystemTime(new Date("2025-03-24T00:00:00.000Z"));
193+
194+
expect(
195+
shouldHideTimeRemaining("2026-03-25T00:00:01.000Z"),
196+
).toBe(true);
197+
});
198+
199+
it("keeps countdowns for deadlines within a year", () => {
200+
vi.setSystemTime(new Date("2025-03-24T00:00:00.000Z"));
201+
202+
expect(
203+
shouldHideTimeRemaining("2026-03-24T00:00:00.000Z"),
204+
).toBe(false);
205+
});
206+
207+
it("does not hide past or invalid deadlines", () => {
208+
vi.setSystemTime(new Date("2025-03-24T00:00:00.000Z"));
209+
210+
expect(shouldHideTimeRemaining("2025-03-23T23:59:59.000Z")).toBe(false);
211+
expect(shouldHideTimeRemaining("gibberish")).toBe(false);
212+
});
213+
});

frontend/src/lib/date/utils.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import dayjs from "./dayjs";
22

3+
const ALWAYS_OPEN_DEADLINE_THRESHOLD_DAYS = 365;
4+
35
export const toDateUtc = (raw: string) => {
46
return dayjs(raw).utc().format("YYYY-MM-DD HH:mm");
57
};
@@ -32,6 +34,17 @@ export const getTimeLeft = (deadline: string): string => {
3234
return `${days} ${dayLabel} ${hours} ${hourLabel} remaining`;
3335
};
3436

37+
export const shouldHideTimeRemaining = (deadline: string): boolean => {
38+
const now = dayjs().utc();
39+
const deadlineDate = dayjs(deadline);
40+
41+
if (!deadlineDate.isValid() || deadlineDate.isSame(now) || deadlineDate.isBefore(now)) {
42+
return false;
43+
}
44+
45+
return deadlineDate.diff(now, "day", true) > ALWAYS_OPEN_DEADLINE_THRESHOLD_DAYS;
46+
};
47+
3548
export const isExpired = (
3649
deadline: string | Date,
3750
time: Date = new Date(),

frontend/src/pages/home/Home.test.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ vi.mock("react-syntax-highlighter/dist/esm/styles/prism", () => ({
2424
vi.mock("../../lib/date/utils", () => ({
2525
getTimeLeft: vi.fn(() => "2 days 5 hours remaining"),
2626
isExpired: vi.fn(() => false),
27+
shouldHideTimeRemaining: vi.fn(() => false),
2728
}));
2829

2930
vi.mock("../../lib/utils/ranking", () => ({
@@ -461,6 +462,49 @@ describe("Home", () => {
461462
expect(screen.getByText("2 days 5 hours remaining")).toBeInTheDocument();
462463
});
463464

465+
it("hides time left for effectively always-open leaderboards", () => {
466+
vi.mocked(dateUtils.shouldHideTimeRemaining).mockReturnValue(true);
467+
468+
const mockData = {
469+
leaderboards: [
470+
{
471+
id: 1,
472+
name: "grayscale_v2",
473+
deadline: "2027-12-31T23:59:59Z",
474+
gpu_types: ["T4"],
475+
priority_gpu_type: "T4",
476+
top_users: [
477+
{
478+
rank: 1,
479+
score: 0.123,
480+
user_name: "alice",
481+
},
482+
],
483+
},
484+
],
485+
now: "2025-01-01T00:00:00Z",
486+
};
487+
488+
const mockHookReturn = {
489+
data: mockData,
490+
loading: false,
491+
hasLoaded: true,
492+
error: null,
493+
errorStatus: null,
494+
call: mockCall,
495+
};
496+
497+
(apiHook.fetcherApiCallback as ReturnType<typeof vi.fn>).mockReturnValue(
498+
mockHookReturn,
499+
);
500+
501+
renderWithProviders(<Home />);
502+
503+
expect(
504+
screen.queryByText("2 days 5 hours remaining"),
505+
).not.toBeInTheDocument();
506+
});
507+
464508
it("formats scores correctly", () => {
465509
const mockData = {
466510
leaderboards: [

frontend/src/pages/home/Home.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import Loading from "../../components/common/loading";
2323
import { ConstrainedContainer } from "../../components/app-layout/ConstrainedContainer";
2424
import MarkdownRenderer from "../../components/markdown-renderer/MarkdownRenderer";
2525
import quickStartMarkdown from "./quick-start.md?raw";
26-
import { isExpired, getTimeLeft } from "../../lib/date/utils";
26+
import { isExpired, getTimeLeft, shouldHideTimeRemaining } from "../../lib/date/utils";
2727
import { ColoredSquare } from "../../components/common/ColoredSquare";
2828
import ArrowOutwardIcon from "@mui/icons-material/ArrowOutward";
2929

@@ -104,6 +104,10 @@ export default function Home() {
104104
navigate(`/leaderboard/${id}/editor`);
105105
};
106106

107+
const getLeaderboardTimeRemaining = (deadline: string) => {
108+
return shouldHideTimeRemaining(deadline) ? undefined : getTimeLeft(deadline);
109+
};
110+
107111
return (
108112
<ConstrainedContainer>
109113
<Box>
@@ -145,7 +149,7 @@ export default function Home() {
145149
{lb.name}
146150
</Box>
147151
}
148-
secondary={getTimeLeft(lb.deadline)}
152+
secondary={getLeaderboardTimeRemaining(lb.deadline)}
149153
slotProps={{
150154
primary: {
151155
fontWeight: 500,

frontend/src/pages/home/components/LeaderboardTile.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Box, Card, CardContent, Chip, type Theme, Typography } from "@mui/material";
22
import { Link } from "react-router-dom";
33
import { getMedalIcon } from "../../../components/common/medal.tsx";
4-
import { getTimeLeft } from "../../../lib/date/utils.ts";
4+
import { getTimeLeft, shouldHideTimeRemaining } from "../../../lib/date/utils.ts";
55
import { formatMicroseconds } from "../../../lib/utils/ranking.ts";
66
import { ColoredSquare } from "../../../components/common/ColoredSquare.tsx";
77

@@ -68,6 +68,7 @@ interface LeaderboardTileProps {
6868

6969
export default function LeaderboardTile({ leaderboard, expired }: LeaderboardTileProps) {
7070
const timeLeft = getTimeLeft(leaderboard.deadline);
71+
const hideTimeRemaining = shouldHideTimeRemaining(leaderboard.deadline);
7172

7273
return (
7374
<Card
@@ -103,7 +104,7 @@ export default function LeaderboardTile({ leaderboard, expired }: LeaderboardTil
103104
</Box>
104105

105106
{/* Time Left */}
106-
{!expired && (
107+
{!expired && !hideTimeRemaining && (
107108
<Typography variant="body1" sx={{ color: "text.secondary" }}>
108109
{timeLeft}
109110
</Typography>

0 commit comments

Comments
 (0)