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
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,39 @@ import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { TimelineToggleHeader } from "@/components/waves/leaderboard/time/TimelineToggleHeader";
import { SeizeSettingsProvider } from "@/contexts/SeizeSettingsContext";
import type { ApiWaveDecisionPause } from "@/generated/models/ApiWaveDecisionPause";

jest.mock("@/helpers/waves/time.utils", () => ({
calculateTimeLeft: jest.fn(() => ({ days: 0, hours: 1, minutes: 2, seconds: 3 })),
}));


jest.mock("@/hooks/useWave", () => ({
useWave: jest.fn(() => null),
calculateTimeLeft: jest.fn(() => ({
days: 0,
hours: 1,
minutes: 2,
seconds: 3,
})),
}));

jest.mock("@/services/6529api", () => ({
fetchUrl: jest.fn(() => Promise.resolve({
memes_wave_id: "test-wave-id"
})),
fetchUrl: jest.fn(() =>
Promise.resolve({
memes_wave_id: "test-wave-id",
})
),
}));

describe("TimelineToggleHeader", () => {
afterEach(() => {
jest.restoreAllMocks();
});

it("renders Decision Timeline when next decision time provided", () => {
const setIsOpen = jest.fn();
render(
<SeizeSettingsProvider>
<TimelineToggleHeader isOpen={false} setIsOpen={setIsOpen} nextDecisionTime={Date.now()+1000} />
<TimelineToggleHeader
isOpen={false}
setIsOpen={setIsOpen}
nextDecisionTime={Date.now() + 1000}
/>
</SeizeSettingsProvider>
);
expect(screen.getByText("Decision Timeline")).toBeInTheDocument();
Expand All @@ -34,4 +45,60 @@ describe("TimelineToggleHeader", () => {
fireEvent.click(toggle);
expect(setIsOpen).toHaveBeenCalledWith(true);
});

it("uses the actual next decision date in the paused banner", () => {
const nextDecisionTime = Date.UTC(2026, 3, 10, 12, 0, 0);
const currentPause: ApiWaveDecisionPause = {
start_time: Date.UTC(2026, 3, 8, 12, 0, 0),
end_time: Date.UTC(2026, 3, 9, 12, 0, 0),
};
const formatSpy = jest
.spyOn(Date.prototype, "toLocaleDateString")
.mockImplementation(function mockToLocaleDateString() {
return `formatted:${this.getTime()}`;
});

render(
<SeizeSettingsProvider>
<TimelineToggleHeader
isOpen={false}
setIsOpen={jest.fn()}
nextDecisionTime={nextDecisionTime}
isPaused
currentPause={currentPause}
/>
</SeizeSettingsProvider>
);

expect(
screen.getByText(`Next decision after formatted:${nextDecisionTime}`)
).toBeInTheDocument();
expect(
screen.queryByText(
`Next decision after formatted:${currentPause.end_time}`
)
).not.toBeInTheDocument();
expect(formatSpy).toHaveBeenCalled();
});

it("shows no decision scheduled when paused without a next decision time", () => {
const currentPause: ApiWaveDecisionPause = {
start_time: Date.UTC(2026, 3, 8, 12, 0, 0),
end_time: Date.UTC(2026, 3, 9, 12, 0, 0),
};

render(
<SeizeSettingsProvider>
<TimelineToggleHeader
isOpen={false}
setIsOpen={jest.fn()}
nextDecisionTime={null}
isPaused
currentPause={currentPause}
/>
</SeizeSettingsProvider>
);

expect(screen.getByText("No decision scheduled")).toBeInTheDocument();
});
});
69 changes: 29 additions & 40 deletions components/waves/leaderboard/time/TimelineToggleHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,13 @@ import type { FC } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faChevronDown } from "@fortawesome/free-solid-svg-icons";
import type { ApiWaveDecisionPause } from "@/generated/models/ApiWaveDecisionPause";
import { useWave } from "@/hooks/useWave";
import type { ApiWave } from "@/generated/models/ApiWave";

interface TimelineToggleHeaderProps {
readonly isOpen: boolean;
readonly setIsOpen: (isOpen: boolean) => void;
readonly nextDecisionTime: number | null;
readonly isPaused?: boolean | undefined;
readonly currentPause?: ApiWaveDecisionPause | null | undefined;
readonly wave?: ApiWave | undefined;
}

/**
Expand All @@ -25,46 +22,35 @@ export const TimelineToggleHeader: FC<TimelineToggleHeaderProps> = ({
nextDecisionTime,
isPaused = false,
currentPause,
wave,
}) => {
const waveData = useWave(wave);
const hasNextDecision = !!nextDecisionTime;
const hasNextDecision = typeof nextDecisionTime === "number";

// Extract the status display logic
const getStatusDisplay = () => {
if (isPaused && currentPause) {
return (
Comment thread
simo6529 marked this conversation as resolved.
<span className="tw-inline-flex tw-items-center tw-gap-x-2 sm:tw-gap-x-2.5 tw-px-3 tw-py-1.5 tw-rounded-md tw-text-xs tw-bg-gradient-to-r tw-from-iron-800/90 tw-to-iron-700/70 group-hover:tw-from-iron-700/60 group-hover:tw-to-iron-600/40 tw-border tw-border-iron-600/40 group-hover:tw-border-iron-600/30 tw-border-solid tw-shadow-sm group-hover:tw-shadow-none tw-transition-all tw-duration-300 tw-ease-out">
<span className="tw-font-bold tw-bg-gradient-to-r tw-from-amber-200 tw-via-amber-100 tw-to-amber-200/90 tw-bg-clip-text tw-text-transparent tw-whitespace-nowrap">
Congrats to last SZN winners!
<span className="tw-inline-flex tw-items-center tw-gap-x-2 tw-rounded-md tw-border tw-border-solid tw-border-iron-600/40 tw-bg-gradient-to-r tw-from-iron-800/90 tw-to-iron-700/70 tw-px-3 tw-py-1.5 tw-text-xs tw-shadow-sm tw-transition-all tw-duration-300 tw-ease-out group-hover:tw-border-iron-600/30 group-hover:tw-from-iron-700/60 group-hover:tw-to-iron-600/40 group-hover:tw-shadow-none sm:tw-gap-x-2.5">
<span className="tw-whitespace-nowrap tw-bg-gradient-to-r tw-from-amber-200 tw-via-amber-100 tw-to-amber-200/90 tw-bg-clip-text tw-font-bold tw-text-transparent">
Decisions paused
</span>
<span className="tw-text-iron-500">•</span>
<span className="tw-text-iron-300 tw-font-medium tw-whitespace-nowrap">
Next SZN starts:{" "}
{(() => {
const mintingDate =
wave && waveData ? waveData.pauses.calculateMintingDate(nextDecisionTime) : null;
return mintingDate
? new Date(mintingDate).toLocaleDateString(undefined, {
<span className="tw-whitespace-nowrap tw-font-medium tw-text-iron-300">
{hasNextDecision
? `Next decision after ${new Date(
nextDecisionTime
).toLocaleDateString(undefined, {
month: "short",
day: "numeric",
})
: new Date(currentPause.end_time).toLocaleDateString(
undefined,
{
month: "short",
day: "numeric",
}
);
})()}
})}`
: "No decision scheduled"}
</span>
</span>
);
}

if (nextDecisionTime) {
if (hasNextDecision) {
return (
<span className="tw-text-iron-300 tw-font-medium tw-text-xs">
<span className="tw-text-xs tw-font-medium tw-text-iron-300">
{new Date(nextDecisionTime).toLocaleDateString(undefined, {
month: "short",
day: "numeric",
Expand All @@ -76,34 +62,37 @@ export const TimelineToggleHeader: FC<TimelineToggleHeaderProps> = ({
}

return (
<span className="tw-text-iron-400 tw-text-xs">No upcoming events</span>
<span className="tw-text-xs tw-text-iron-400">No upcoming events</span>
);
};

return (
<div
className="tw-@container tw-px-3 tw-py-1.5 tw-bg-iron-800/95 tw-rounded-t-lg tw-border tw-border-solid tw-border-iron-700/50 tw-cursor-pointer desktop-hover:hover:tw-bg-iron-700/80 tw-transition-all tw-duration-300 tw-ease-out tw-group tw-shadow-sm"
onClick={() => setIsOpen(!isOpen)}>
<div className="tw-flex tw-items-center tw-gap-2 tw-w-full">

className="tw-group tw-cursor-pointer tw-rounded-t-lg tw-border tw-border-solid tw-border-iron-700/50 tw-bg-iron-800/95 tw-px-3 tw-py-1.5 tw-shadow-sm tw-transition-all tw-duration-300 tw-ease-out tw-@container desktop-hover:hover:tw-bg-iron-700/80"
onClick={() => setIsOpen(!isOpen)}
>
<div className="tw-flex tw-w-full tw-items-center tw-gap-2">
<span
className={`tw-text-xs tw-font-semibold tw-whitespace-nowrap tw-flex-shrink-0 ${hasNextDecision ? "tw-text-iron-100" : "tw-text-iron-400"
}`}>
className={`tw-flex-shrink-0 tw-whitespace-nowrap tw-text-xs tw-font-semibold ${
hasNextDecision ? "tw-text-iron-100" : "tw-text-iron-400"
}`}
>
{hasNextDecision ? "Decision Timeline" : "Announcement history"}
</span>


<div className="tw-flex-1 tw-text-xs tw-font-medium tw-flex tw-justify-end tw-items-center">
<div className="tw-flex tw-flex-1 tw-items-center tw-justify-end tw-text-xs tw-font-medium">
{getStatusDisplay()}
</div>

<button
className="tw-w-6 tw-h-6 tw-flex tw-items-center tw-justify-center tw-bg-iron-700/50 tw-rounded-md tw-border tw-border-solid tw-border-iron-600/40 desktop-hover:hover:tw-bg-iron-600/60 desktop-hover:hover:tw-border-iron-500/50 tw-transition-all tw-duration-300 tw-ease-out tw-flex-shrink-0"
aria-label={isOpen ? "Collapse" : "Expand"}>
className="tw-flex tw-h-6 tw-w-6 tw-flex-shrink-0 tw-items-center tw-justify-center tw-rounded-md tw-border tw-border-solid tw-border-iron-600/40 tw-bg-iron-700/50 tw-transition-all tw-duration-300 tw-ease-out desktop-hover:hover:tw-border-iron-500/50 desktop-hover:hover:tw-bg-iron-600/60"
aria-label={isOpen ? "Collapse" : "Expand"}
>
<FontAwesomeIcon
icon={faChevronDown}
className={`tw-w-4 tw-h-4 tw-text-iron-200 desktop-hover:group-hover:tw-text-iron-100 tw-flex-shrink-0 ${isOpen ? "tw-rotate-180" : ""
} tw-transition-all tw-duration-300 tw-ease-in-out`}
className={`tw-h-4 tw-w-4 tw-flex-shrink-0 tw-text-iron-200 desktop-hover:group-hover:tw-text-iron-100 ${
isOpen ? "tw-rotate-180" : ""
} tw-transition-all tw-duration-300 tw-ease-in-out`}
/>
</button>
</div>
Expand Down
Loading