Spaces:
Sleeping
Sleeping
| /** Entry detail: snippet, meta, result (auto-open if pending), remedy, save. */ | |
| import { ChevronRight } from "lucide-preact"; | |
| import { useEffect, useState } from "preact/hooks"; | |
| import { | |
| coachEntry, | |
| deleteEntry, | |
| getEntry, | |
| updateEntry, | |
| type CoachResponse, | |
| type Entry, | |
| } from "../api"; | |
| import { Button } from "../components/Button"; | |
| import { CoachResultCard } from "../components/CoachResult"; | |
| import { ResultChips } from "../components/ResultChips"; | |
| import { Sheet } from "../components/Sheet"; | |
| import { usePendingResolve } from "../components/PendingResolve"; | |
| import { useToast } from "../components/Toast"; | |
| import { navigate } from "../router"; | |
| export function EntryDetail({ id }: { id: string }) { | |
| const toast = useToast(); | |
| const { openResolveId, refreshPendingSignal } = usePendingResolve(); | |
| const [entry, setEntry] = useState<Entry | null>(null); | |
| const [draft, setDraft] = useState<Entry | null>(null); | |
| const [confirming, setConfirming] = useState(false); | |
| const [coach, setCoach] = useState<CoachResponse | null>(null); | |
| const [busy, setBusy] = useState(false); | |
| useEffect(() => { | |
| let cancelled = false; | |
| getEntry(id) | |
| .then((data) => { | |
| if (cancelled) return; | |
| setEntry(data); | |
| setDraft(data); | |
| if (data.result === "pending") { | |
| openResolveId(data.id, data); | |
| } | |
| }) | |
| .catch((e) => { | |
| if (!cancelled) toast.show(e.message, "error"); | |
| }); | |
| return () => { | |
| cancelled = true; | |
| }; | |
| }, [id]); | |
| useEffect(() => { | |
| if (!refreshPendingSignal) return; | |
| getEntry(id) | |
| .then((data) => { | |
| setEntry(data); | |
| setDraft(data); | |
| }) | |
| .catch(() => {}); | |
| }, [refreshPendingSignal, id]); | |
| if (!entry || !draft) return <div class="gate-loading">Loading…</div>; | |
| const save = async () => { | |
| setBusy(true); | |
| try { | |
| const updated = await updateEntry(id, { | |
| activity: draft.activity, | |
| happened: draft.happened, | |
| emotions: draft.emotions, | |
| intensity: draft.intensity, | |
| remedy: draft.remedy, | |
| result: draft.result, | |
| tags: draft.tags, | |
| notes: draft.notes, | |
| }); | |
| setEntry(updated); | |
| setDraft(updated); | |
| toast.show("Saved"); | |
| } catch (e) { | |
| toast.show(e instanceof Error ? e.message : "Update failed", "error"); | |
| } finally { | |
| setBusy(false); | |
| } | |
| }; | |
| return ( | |
| <div class="app-shell"> | |
| <header class="top-bar material-bar"> | |
| <Button variant="plain" onClick={() => navigate("/history")}> | |
| ‹ Back | |
| </Button> | |
| <h1 class="page-title">Entry</h1> | |
| <span /> | |
| </header> | |
| <main class="page stack"> | |
| <article class="surface-card detail-card stack"> | |
| <p class="detail-snippet">{draft.happened}</p> | |
| <div class="meta-grid"> | |
| <div> | |
| <span class="field-label">Date</span> | |
| {new Date(entry.ts).toLocaleString()} | |
| </div> | |
| <div> | |
| <span class="field-label">Activity</span> | |
| {draft.activity || "—"} | |
| </div> | |
| <div> | |
| <span class="field-label">Emotions</span> | |
| {draft.emotions.join(", ") || "—"} | |
| </div> | |
| <div> | |
| <span class="field-label">Intensity</span> | |
| {draft.intensity}/10 | |
| </div> | |
| <div> | |
| <span class="field-label">Tags</span> | |
| {draft.tags.join(", ") || "—"} | |
| </div> | |
| </div> | |
| </article> | |
| <section class={`surface-card form-card stack ${draft.result === "pending" ? "pending-focus" : ""}`.trim()}> | |
| <div class="section-head"> | |
| <span class="field-label">Result</span> | |
| {entry.result === "pending" ? ( | |
| <PressableFinish onClick={() => openResolveId(entry.id, entry)} /> | |
| ) : null} | |
| </div> | |
| <ResultChips | |
| value={draft.result} | |
| onChange={(result) => setDraft({ ...draft, result })} | |
| /> | |
| <label class="field-label" for="entry-remedy"> | |
| Remedy | |
| </label> | |
| <input | |
| id="entry-remedy" | |
| class="field-input" | |
| value={draft.remedy} | |
| onInput={(e) => | |
| setDraft({ ...draft, remedy: (e.target as HTMLInputElement).value }) | |
| } | |
| /> | |
| <Button disabled={busy} onClick={save}> | |
| {busy ? "Saving…" : "Save"} | |
| </Button> | |
| </section> | |
| <Button | |
| variant="secondary" | |
| disabled={busy} | |
| onClick={async () => { | |
| setBusy(true); | |
| try { | |
| setCoach(await coachEntry(id)); | |
| } catch (e) { | |
| toast.show(e instanceof Error ? e.message : "Suggestion failed", "error"); | |
| } finally { | |
| setBusy(false); | |
| } | |
| }} | |
| > | |
| Get suggestion | |
| </Button> | |
| {coach ? <CoachResultCard result={coach} /> : null} | |
| <Button variant="destructive" onClick={() => setConfirming(true)}> | |
| Delete entry | |
| </Button> | |
| </main> | |
| <Sheet open={confirming} title="Delete entry?" onClose={() => setConfirming(false)}> | |
| <div class="stack"> | |
| <p>This permanently deletes this entry.</p> | |
| <Button | |
| variant="destructive" | |
| onClick={async () => { | |
| await deleteEntry(id); | |
| toast.show("Entry deleted"); | |
| navigate("/history"); | |
| }} | |
| > | |
| Delete | |
| </Button> | |
| <Button variant="secondary" onClick={() => setConfirming(false)}> | |
| Cancel | |
| </Button> | |
| </div> | |
| </Sheet> | |
| </div> | |
| ); | |
| } | |
| function PressableFinish({ onClick }: { onClick: () => void }) { | |
| return ( | |
| <button type="button" class="linkish" onClick={onClick}> | |
| Finish now <ChevronRight size={16} aria-hidden="true" /> | |
| </button> | |
| ); | |
| } | |