import { useEffect, useState } from "react"; import { clampRequired, type Goal } from "../../shared/goal.ts"; const WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]; const MODES = [ { id: "off", label: "Off" }, { id: "week", label: "Each week" }, { id: "event", label: "Across the event" }, ] as const; type Mode = (typeof MODES)[number]["id"]; /** * The goal a mode press asks for: a Goal, undefined to clear it, or null for * "nothing to do". Pressing the mode already shown is null — including Off * when no goal is *readable*, which may still be one stored by a newer * version, and a press that looks like a no-op must not delete it. */ export function chooseGoal( id: Mode, goal: Goal | undefined, defaultWeekStartsOn: number, maxDays: number | null, ): Goal | undefined | null { if (id === (goal?.period ?? "off")) return null; if (id === "off") return undefined; const required = goal?.measure === "days" ? goal.required : 5; if (id === "week") { return { period: "week", measure: "days", required: clampRequired("week", required, null), weekStartsOn: goal?.period === "week" ? goal.weekStartsOn : defaultWeekStartsOn, }; } return { period: "event", measure: "days", required: clampRequired("event", required, maxDays) }; } /** A typed count, clamped; null while the field is empty or unreadable — mid-edit, not an answer. */ export function parseDraft( raw: string, period: Goal["period"], maxDays: number | null, ): number | null { const n = Number.parseInt(raw, 10); return Number.isFinite(n) ? clampRequired(period, n, maxDays) : null; } /** * How many check-ins the event actually wants. * * Some events ask for five days out of seven, not every day. The reader says * so here — nothing is read off the title — and the checklist and the dailies * strip count against it. Meeting it never marks the event done. * * The same pill style as the status and effort controls below it, because it * is the same kind of answer: something only the reader knows. */ export function GoalControl({ goal, defaultWeekStartsOn, maxDays, onChange, }: { goal: Goal | undefined; /** The weekday of the event's first game-day: its weeks most likely start there. */ defaultWeekStartsOn: number; /** Claimable days in the run, or null when the end is unannounced. */ maxDays: number | null; onChange: (goal: Goal | undefined) => void; }) { const mode = goal?.period ?? "off"; const choose = (id: Mode) => { const next = chooseGoal(id, goal, defaultWeekStartsOn, maxDays); if (next !== null) onChange(next); }; return (
Check-in goal