import {
dailySummary,
msUntilReset,
type DailySummary,
} from "../../shared/daily.ts";
import type { LaneId } from "../../shared/custom.ts";
import type { Region } from "../../shared/schema.ts";
import { formatRemaining } from "../../shared/time.ts";
/**
* The checklist for an event you have to come back to every day.
*
* A single "done" tick is the wrong shape for these: the job is not finished
* or unfinished, it is finished *today*, and yesterday's is gone whatever you
* do now. So this shows the whole run as a strip of days — what you got, what
* you missed, and how many chances are left — with today's tick as the one
* prominent control.
*
* Past days stay clickable on purpose. People log in and tick up later, and a
* checklist that cannot be corrected stops being trusted after the first
* mistake.
*/
export function DailyChecklist({
startsMs,
endsMs,
region,
game,
now,
logged,
onToggleDay,
}: {
startsMs: number;
endsMs: number | null;
region: Region;
/** Whose reset clock the days are counted on — not every game shares one. */
game: LaneId;
now: number;
logged: string[];
onToggleDay: (day: string) => void;
}) {
const summary = dailySummary({ startsMs, endsMs, region, game, now, logged });
const { days, today, doneToday, todayInWindow } = summary;
const started = now >= startsMs;
return (
Daily checklist
resets in {formatRemaining(msUntilReset(now, region, game))}
{days === null ? (
The source hasn't announced an end date, so how many days are left is
unknown. Your ticks are still counted — {summary.logged} so far.
) : (
<>
{days.map((day) => (
onToggleDay(day)}
/>
))}
{caption(summary, todayInWindow)}
>
)}
);
}
/**
* One day. Future days are dimmed but not disabled-looking, past misses read as
* empty rather than as an error — a missed daily is information, not a telling
* off.
*/
function DayPip({
day,
today,
done,
onToggle,
}: {
day: string;
today: string;
done: boolean;
onToggle: () => void;
}) {
const isToday = day === today;
const isFuture = day > today;
// Rendered in UTC on purpose. A day key is a game-day, not an instant, and
// formatting it in the reader's own zone shifts it a day backwards for
// everyone west of UTC — so the pip would read "12" while the label a screen
// reader announces said "Aug 11".
const label = new Date(`${day}T00:00:00Z`).toLocaleDateString(undefined, {
day: "numeric",
month: "short",
timeZone: "UTC",
});
return (
);
}
function caption(summary: DailySummary, todayInWindow: boolean): string {
const { days, logged, remaining, missed } = summary;
if (days === null || remaining === null) return `${logged} days ticked off.`;
const parts = [`${logged} of ${days.length} days`];
if (remaining > 0) {
parts.push(
todayInWindow
? `${remaining} left including today`
: `${remaining} to come`,
);
} else {
parts.push("the run is over");
}
if (missed !== null && missed > 0) parts.push(`${missed} missed`);
return `${parts.join(" · ")}.`;
}