import { useState } from "react"; import type { LaneId } from "../../shared/custom.ts"; import { TaskSchema } from "../../shared/task.ts"; import { useGameMeta } from "../state/gameMeta.tsx"; import type { TaskDraft } from "../state/useTasks.ts"; import { parseAmount } from "./AmountEditor.tsx"; const WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]; /** The form's fields as typed, before they are a task. */ export interface TaskFields { game: string; title: string; unit: "week" | "month"; weekday: number; /** As typed, so the field can be emptied and retyped; read through `parseMonthDay`. */ monthDay: string | number; kind: "days" | "amount"; required: string; target: string; amountUnit: string; } /** A typed day of the month, clamped to 1–31; null while empty or unreadable. */ export function parseMonthDay(raw: string): number | null { const n = Number.parseInt(raw, 10); return Number.isFinite(n) ? Math.min(31, Math.max(1, n)) : null; } /** * The task these fields describe, or null while they do not describe one the * store would accept. Checked against `TaskSchema` itself, so the form can * never offer Save on something the store would then refuse. */ export function taskDraftFrom(f: TaskFields): TaskDraft | null { const monthDay = parseMonthDay(String(f.monthDay)); if (f.unit === "month" && monthDay === null) return null; const period = f.unit === "week" ? ({ unit: "week", startsOn: f.weekday } as const) : ({ unit: "month", startsOn: monthDay ?? 1 } as const); const unit = f.amountUnit.trim(); const target = parseAmount(f.target); const measure = f.kind === "days" ? ({ kind: "days", required: Number.parseInt(f.required, 10) } as const) : target === null ? null : ({ kind: "amount", target, ...(unit === "" ? {} : { unit }) } as const); if (measure === null) return null; const draft: TaskDraft = { game: f.game, title: f.title.trim(), period, measure }; const check = TaskSchema.safeParse({ ...draft, id: "mytask:check00000", at: "", updatedAt: "" }); return check.success ? draft : null; } function pill(on: boolean): string { return `rounded-full border px-3 py-1.5 text-xs font-medium transition-colors duration-150 ${ on ? "border-near/70 bg-near/15 text-near" : "border-hairline text-faint hover:text-muted" }`; } const field = "rounded-lg border border-hairline bg-ground px-2 py-1 text-sm text-ink focus:border-faint focus:outline-none"; /** * Add or change a weekly or monthly task. * * Every answer has a default a reader can accept unread — weekly, from Monday, * ticked off once — because most of these games roll their week on Monday at * reset and most chores are done-or-not. The number is the one thing with no * sensible default, so choosing "Reach a number" asks for it. */ export function TaskForm({ lanes, initial, onSave, onCancel, }: { lanes: LaneId[]; initial?: TaskDraft | undefined; onSave: (draft: TaskDraft) => void; onCancel: () => void; }) { const gameMeta = useGameMeta(); const [f, setF] = useState(() => ({ game: initial?.game ?? lanes[0] ?? "", title: initial?.title ?? "", unit: initial?.period.unit ?? "week", weekday: initial?.period.unit === "week" ? initial.period.startsOn : 1, monthDay: String(initial?.period.unit === "month" ? initial.period.startsOn : 1), kind: initial?.measure.kind ?? "days", required: initial?.measure.kind === "days" ? String(initial.measure.required) : "1", target: initial?.measure.kind === "amount" ? String(initial.measure.target) : "", amountUnit: initial?.measure.kind === "amount" ? (initial.measure.unit ?? "") : "", })); const set = (patch: Partial) => setF((prev) => ({ ...prev, ...patch })); const draft = taskDraftFrom(f); return (
{ e.preventDefault(); if (draft !== null) onSave(draft); }} >

How often

{f.unit === "week" ? ( ) : ( )}

It rolls over at the game's daily reset on that day, on your server.

Done when

{f.kind === "days" ? ( <> set({ required: e.target.value })} className={`tnum w-14 ${field}`} /> {f.required === "1" ? "day" : "days"} this {f.unit} ) : ( <> set({ target: e.target.value })} placeholder="2100" className={`tnum w-20 ${field}`} /> set({ amountUnit: e.target.value })} placeholder="points" className={`w-24 ${field}`} /> this {f.unit} )}
); }