Files
minabot/src/components/ArchivedHabits.tsx

86 lines
6.5 KiB
TypeScript

import { useEffect, useRef, useState } from "react";
import { habitRequest, scheduleLabel, type TodayHabit } from "../lib/dashboard";
import { Button } from "./design-system/primitives";
import { Archive, ChevronDown, RotateCcw, Trash2 } from "lucide-react";
import { FormMessage, WorkspacePanel } from "./design-system/WorkspacePanel";
import { Modal } from "./design-system/Modal";
import { HabitHistory } from "./HabitHistory";
export function ArchivedHabits({ date, revision, onChanged, onExpired, onClose }: {
date: string; revision: number; onChanged: () => void; onExpired: () => void; onClose?: () => void;
}) {
const [habits, setHabits] = useState<TodayHabit[]>([]);
const [deleting, setDeleting] = useState<TodayHabit | null>(null);
const [deleteError, setDeleteError] = useState("");
const archiveFocus = useRef<HTMLDivElement>(null);
const [selected, setSelected] = useState<string>();
const [error, setError] = useState("");
const [notice, setNotice] = useState("");
const [loading, setLoading] = useState(true);
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(); setLoading(true); setError("");
habitRequest<{ habits: TodayHabit[] }>(`/days/${date}`, { signal: controller.signal })
.then(data => { if (!controller.signal.aborted) setHabits(data.habits.filter(habit => habit.requirements?.archived)); })
.catch(error => { if (!controller.signal.aborted) { setError(error.message); if (error.message.includes("session has expired")) expired.current(); } })
.finally(() => { if (!controller.signal.aborted) setLoading(false); });
return () => controller.abort();
}, [date, revision, attempt]);
async function restore(habit: TodayHabit) {
if (saving.current) return;
saving.current = true; setBusy(true); setError(""); setNotice("");
try {
await habitRequest(`/habits/${habit.habitId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ archived: false }) });
setHabits(current => current.filter(item => item.habitId !== habit.habitId));
setNotice(`${habit.name} restored. Check-ins resume today.`); onChanged();
} catch (error) {
const message = error instanceof Error ? error.message : "Could not restore. Try again.";
setError(message); if (message.includes("session has expired")) expired.current();
} finally { saving.current = false; setBusy(false); }
}
async function permanentlyDelete(habit: TodayHabit) {
if (saving.current) return;
saving.current = true; setBusy(true); setDeleteError(""); setNotice("");
try {
await habitRequest(`/habits/${habit.habitId}/permanent`, { method: "DELETE" });
setHabits(current => current.filter(item => item.habitId !== habit.habitId));
setSelected(current => current === habit.habitId ? undefined : current);
setDeleting(null);
setNotice(`${habit.name} permanently deleted.`);
onChanged();
} catch (error) {
const message = error instanceof Error ? error.message : "Could not delete. Try again.";
setDeleteError(message); if (message.includes("session has expired")) expired.current();
} finally { saving.current = false; setBusy(false); }
}
return <WorkspacePanel id="archived-habits" title="Archived habits" onClose={onClose} busy={busy} closeLabel="Close archive">
<div ref={archiveFocus} tabIndex={-1} aria-label="Archive status"><FormMessage>{notice}</FormMessage></div>
<FormMessage error>{error}</FormMessage>
{error && <Button variant="secondary" onClick={() => setAttempt(value => value + 1)}>Retry archive</Button>}
{loading ? <p className="ds-supporting-copy" role="status">Loading archive…</p> : !habits.length && <div className="ds-empty-panel"><Archive size={24} aria-hidden="true" /><div><p className="type-ui-heading">No archived habits.</p></div></div>}
<div className="ds-record-list">
{habits.map(habit => <article key={habit.habitId} className="ds-record" aria-labelledby={`archive-title-${habit.habitId}`}>
<header className="ds-record-heading">
<div className="ds-record-copy"><h3 id={`archive-title-${habit.habitId}`}>{habit.name ?? "Habit"}</h3><p>{scheduleLabel(habit.requirements?.schedule)} · Archived</p></div>
<div className="ds-action-row">
<Button variant="text" onClick={() => setSelected(selected === habit.habitId ? undefined : habit.habitId)} aria-expanded={selected === habit.habitId} aria-controls={selected === habit.habitId ? `archive-history-${habit.habitId}` : undefined}>{selected === habit.habitId ? "Hide history" : "View history"}<ChevronDown className="ds-disclosure-icon" size={16} aria-hidden="true" /></Button>
<Button variant="secondary" disabled={busy || loading} aria-label={`Restore ${habit.name}`} onClick={() => void restore(habit)}><RotateCcw size={16} aria-hidden="true" />Restore</Button>
<Button variant="text" disabled={busy || loading} aria-label={`Delete permanently ${habit.name}`} aria-haspopup="dialog" onClick={() => { setDeleteError(""); setDeleting(habit); }}><Trash2 size={16} aria-hidden="true" />Delete permanently</Button>
</div>
</header>
{selected === habit.habitId && <div className="ds-record-detail" id={`archive-history-${habit.habitId}`}><HabitHistory calendarOnly habit={habit} date={date} revision={habit.calendarRevision ?? revision + attempt} disabled={busy || loading} onExpired={onExpired} onHistorySaved={() => setAttempt(value => value + 1)} /></div>}
</article>)}
</div>
{deleting && <Modal id="delete-archived-habit" size="compact" eyebrow="PERMANENT DELETION" title="Delete this habit?" description={`“${deleting.name ?? "Habit"}” and all its check-ins, tasks, and progress history will be permanently deleted. It will be removed from combined charts; charts with no remaining habits will also be deleted. This cannot be undone.`} busy={busy} initialFocus="[data-cancel-delete]" returnFocus={() => archiveFocus.current} onClose={() => setDeleting(null)}>
<FormMessage error>{deleteError}</FormMessage>
<div className="ds-action-row">
<Button variant="secondary" data-cancel-delete disabled={busy} onClick={() => setDeleting(null)}>Keep habit</Button>
<Button disabled={busy} onClick={() => void permanentlyDelete(deleting)}>{busy ? "Deleting…" : "Delete permanently"}</Button>
</div>
</Modal>}
</WorkspacePanel>;
}