Files
minabot/src/components/CheckInEditor.tsx

62 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { FormMessage } from "./design-system/WorkspacePanel";
import { useEffect, useRef, useState } from "react";
import { habitRequest, formatTrackingDate, type TodayHabit } from "../lib/dashboard";
import { Button, Checkbox } from "./design-system/primitives";
import { Field } from "./design-system/Field";
export function CheckInEditor({ habitId, date, disabled, onSaved, onExpired, onBusyChange }: {
habitId: string; date: string; disabled?: boolean; onSaved: () => void; onExpired?: () => void; onBusyChange?: (busy: boolean) => void;
}) {
const [day, setDay] = useState<TodayHabit | null>(null);
const [count, setCount] = useState("");
const [error, setError] = useState("");
const [notice, setNotice] = useState("");
const [busy, setBusy] = useState(false);
const [attempt, setAttempt] = useState(0);
const saving = useRef(false);
const expired = useRef(onExpired); expired.current = onExpired;
useEffect(() => {
const controller = new AbortController();
setDay(null); setError("");
habitRequest<TodayHabit>(`/habits/${habitId}/days/${date}`, { signal: controller.signal })
.then(value => { if (!controller.signal.aborted) { setDay(value); setCount(String(value.value)); } })
.catch(error => { if (!controller.signal.aborted) {
if (error.message.includes("session has expired")) expired.current?.();
setError(error.message);
} });
return () => controller.abort();
}, [habitId, date, attempt]);
async function save(body: { count: number } | { done: boolean }, taskId?: string) {
if (saving.current || disabled) return;
saving.current = true; setBusy(true); onBusyChange?.(true); setError(""); setNotice("");
try {
const result = await habitRequest<TodayHabit>(`/habits/${habitId}/days/${date}/${taskId ? `tasks/${taskId}` : "progress"}`, {
method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body),
});
setDay(result); setCount(String(result.value)); setNotice(`Saved check-in for ${date}.`); onSaved();
} catch (error) {
const message = error instanceof Error ? error.message : "Could not save. Try again.";
setError(message); if (message.includes("session has expired")) expired.current?.();
} finally { saving.current = false; setBusy(false); onBusyChange?.(false); }
}
return <div className="ds-form-stack ds-check-in-controls" aria-label={`Check-in for ${date}`}>
<header className="ds-check-in-heading"><p className="type-ui-heading">{formatTrackingDate(date)}</p></header>
<FormMessage error>{error}</FormMessage>
{!day ? error ? <Button onClick={() => setAttempt(value => value + 1)}>Retry check-in</Button> : <p role="status">Loading check-in…</p>
: day.future ? <p>Future check-ins cannot be edited.</p>
: !day.due ? <p>Nothing was scheduled for this date.</p>
: <>
<p className="ds-supporting-copy">{day.timezone} · Using this day’s saved target and schedule.</p>
{day.method === "count" ? <form className="ds-form-stack" onSubmit={event => { event.preventDefault(); void save({ count: Number(count) }); }}>
<Field label={`Count (${day.unit}, target ${day.target})`}>
{id => <input id={id} type="number" min={0} max={1000000000} step={1} required value={count} disabled={busy || disabled} onChange={event => setCount(event.target.value)} />}
</Field>
{day.carriedFrom && <p className="type-small">Includes carryover from {day.carriedFrom}. Saving sets this day's total.</p>}
<div className="ds-action-row"><Button type="submit" disabled={busy || disabled}>{busy ? "Saving…" : "Save check-in"}</Button></div>
</form> : day.method === "manual" ? <Checkbox label="Completed on this date" checked={day.complete} disabled={busy || disabled} onChange={event => void save({ done: event.target.checked })} />
: day.tasks.map(task => <Checkbox key={task.taskId} label={task.name} checked={task.done} disabled={busy || disabled} onChange={event => void save({ done: event.target.checked }, task.taskId)} />)}
</>}
<FormMessage>{notice}</FormMessage>
</div>;
}