Fastwhisper / frontend /src /pages /EntryDetail.tsx
Mbonea's picture
Overhaul journal UI: pending resolve sheet, gold FAB nav, and Home week score.
709db16
Raw
History Blame Contribute Delete
6 kB
/** 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>
);
}