Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion frontend/src/lib/date/utils.test.ts
Original file line number Diff line number Diff line change
@@ -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(() => {
Expand Down Expand Up @@ -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);
});
});
13 changes: 13 additions & 0 deletions frontend/src/lib/date/utils.ts
Original file line number Diff line number Diff line change
@@ -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");
};
Expand Down Expand Up @@ -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(),
Expand Down
44 changes: 44 additions & 0 deletions frontend/src/pages/home/Home.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => ({
Expand Down Expand Up @@ -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<typeof vi.fn>).mockReturnValue(
mockHookReturn,
);

renderWithProviders(<Home />);

expect(
screen.queryByText("2 days 5 hours remaining"),
).not.toBeInTheDocument();
});

it("formats scores correctly", () => {
const mockData = {
leaderboards: [
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/pages/home/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -104,6 +104,10 @@ export default function Home() {
navigate(`/leaderboard/${id}/editor`);
};

const getLeaderboardTimeRemaining = (deadline: string) => {
return shouldHideTimeRemaining(deadline) ? undefined : getTimeLeft(deadline);
};

return (
<ConstrainedContainer>
<Box>
Expand Down Expand Up @@ -145,7 +149,7 @@ export default function Home() {
{lb.name}
</Box>
}
secondary={getTimeLeft(lb.deadline)}
secondary={getLeaderboardTimeRemaining(lb.deadline)}
slotProps={{
primary: {
fontWeight: 500,
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/pages/home/components/LeaderboardTile.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -68,6 +68,7 @@ interface LeaderboardTileProps {

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

return (
<Card
Expand Down Expand Up @@ -103,7 +104,7 @@ export default function LeaderboardTile({ leaderboard, expired }: LeaderboardTil
</Box>

{/* Time Left */}
{!expired && (
{!expired && !hideTimeRemaining && (
<Typography variant="body1" sx={{ color: "text.secondary" }}>
{timeLeft}
</Typography>
Expand Down
Loading