/** Fast Log page: save an entry; high-FSE offers proof brick; pending offers Finish. */ import { useEffect, useMemo, useState } from "preact/hooks"; import { ApiError, createEntry, getDaily, putDaily, type DailyUpsert, type ProofBrickKind, } from "../api"; import { Button } from "../components/Button"; import { Pressable } from "../components/Pressable"; import { ResultChips, type ResultValue } from "../components/ResultChips"; import { Sheet } from "../components/Sheet"; import { usePendingResolve } from "../components/PendingResolve"; import { useToast } from "../components/Toast"; import { isoDate } from "../dates"; import { navigate } from "../router"; const EMOTION_PICKS = [ "calm", "shame", "urge", "anxiety", "anger", "lonely", "tired", "hope", ]; const TAG_SUGGESTS = [ "indoors", "freeze", "movement", "interrupt_walk", "fantasy_walk", "headphones", "proof", ]; const FSE_EMOTIONS = new Set(["shame", "urge", "anxiety", "anger", "lonely"]); const FSE_TAGS = new Set([ "freeze", "indoors", "rerun", "court", "daydream", "fantasy", "fantasy_walk", ]); const AVOIDANCE_PICKS = [ "freeze", "bed", "scroll", "fantasy", "headphones", "court", "rerun", ]; const PROOF_KINDS: { value: ProofBrickKind; label: string }[] = [ { value: "interrupt_walk", label: "Interrupt walk" }, { value: "body_or_room", label: "Body or room" }, { value: "trip_admin", label: "Trip admin" }, { value: "earn", label: "Earn" }, { value: "boundary", label: "Boundary" }, { value: "food", label: "Food" }, { value: "survive", label: "Survive" }, { value: "other", label: "Other" }, ]; function isHighFse(intensity: number, emotions: string[], tagList: string[]): boolean { if (intensity < 7) return false; const emos = emotions.map((e) => e.toLowerCase()); const tags = tagList.map((t) => t.toLowerCase()); return emos.some((e) => FSE_EMOTIONS.has(e)) || tags.some((t) => FSE_TAGS.has(t)); } function isLabelOnlyRemedy(remedy: string): boolean { return /\blabel(?:ing|ling)?\b/i.test(remedy.trim()); } const EMPTY_DAILY_PATCH: Partial = { primary_brick: "none", brick_done: false, corn_sessions: 0, delay_ok: true, daydream: "none", rerun: "clean", court: "closed", stayed_indoors_all_day: false, left_room: false, left_home: false, movement_minutes: 0, interrupt_walk_minutes: 0, fantasy_walk_minutes: 0, headphones_on_walk: false, music_cinematic_on_walk: false, proof_brick_done: false, proof_brick_kind: null, fantasy_minutes_scheduled: 0, fantasy_minutes_unplanned: 0, note: "", }; export function Log({ initialRemedy = "", initialHappened = "", }: { initialRemedy?: string; initialHappened?: string; }) { const toast = useToast(); const { openResolveId } = usePendingResolve(); const [happened, setHappened] = useState(initialHappened); const [emotions, setEmotions] = useState([]); const [intensity, setIntensity] = useState(5); const [result, setResult] = useState("pending"); const [remedy, setRemedy] = useState(initialRemedy); const [activity, setActivity] = useState(""); const [tags, setTags] = useState(""); const [notes, setNotes] = useState(""); const [moreOpen, setMoreOpen] = useState(false); const [saving, setSaving] = useState(false); const [finishPrompt, setFinishPrompt] = useState<{ id: string } | null>(null); const [proofOpen, setProofOpen] = useState(false); const [proofBusy, setProofBusy] = useState(false); const [avoidance, setAvoidance] = useState([]); const [pendingAfterProof, setPendingAfterProof] = useState<{ id: string; pending: boolean; } | null>(null); useEffect(() => { setRemedy(initialRemedy); }, [initialRemedy]); useEffect(() => { if (initialHappened) setHappened(initialHappened); }, [initialHappened]); const canSave = useMemo( () => happened.trim().length > 0 && !saving, [happened, saving], ); const tagList = useMemo( () => tags .split(",") .map((t) => t.trim()) .filter(Boolean), [tags], ); const toggleEmotion = (emotion: string) => { setEmotions((current) => current.includes(emotion) ? current.filter((item) => item !== emotion) : current.length >= 12 ? current : [...current, emotion], ); }; const toggleTag = (tag: string) => { const parts = tagList; const lower = parts.map((t) => t.toLowerCase()); if (lower.includes(tag)) { setTags(parts.filter((t) => t.toLowerCase() !== tag).join(", ")); } else { setTags([...parts, tag].join(", ")); } }; const afterSaveNavigate = (entryId: string, isPending: boolean) => { if (isPending) { setFinishPrompt({ id: entryId }); } else { navigate("/"); } }; const labelOnlyWarn = intensity >= 7 && remedy.trim().length > 0 && isLabelOnlyRemedy(remedy); const toggleAvoidance = (tag: string) => { setAvoidance((current) => current.includes(tag) ? current.filter((item) => item !== tag) : [...current, tag], ); }; const onSave = async () => { if (!canSave) return; setSaving(true); try { const high = isHighFse(intensity, emotions, tagList); const entry = await createEntry({ activity: activity.trim() || "Log", happened: happened.trim(), emotions, intensity, remedy: remedy.trim(), result, tags: tagList, notes: notes.trim(), fse_spike: high || intensity >= 7 ? true : null, avoidance_types: avoidance, walk_type: tagList.includes("interrupt_walk") ? "interrupt" : tagList.includes("fantasy_walk") ? "fantasy" : null, }); toast.show("Saved"); if ( typeof navigator !== "undefined" && "vibrate" in navigator && !window.matchMedia("(prefers-reduced-motion: reduce)").matches ) { navigator.vibrate(10); } const pending = entry.result === "pending"; if (high) { setPendingAfterProof({ id: entry.id, pending }); setProofOpen(true); } else { afterSaveNavigate(entry.id, pending); } } catch (err) { const message = err instanceof ApiError ? err.message : "Could not save entry"; toast.show(message, "error"); } finally { setSaving(false); } }; const closeProof = () => { setProofOpen(false); const next = pendingAfterProof; setPendingAfterProof(null); if (next) afterSaveNavigate(next.id, next.pending); else navigate("/"); }; const saveProof = async (kind: ProofBrickKind) => { setProofBusy(true); const today = isoDate(new Date()); try { let base: DailyUpsert = { ...EMPTY_DAILY_PATCH } as DailyUpsert; try { const row = await getDaily(today); base = { primary_brick: row.primary_brick, brick_done: row.brick_done, corn_sessions: row.corn_sessions, delay_ok: row.delay_ok, daydream: row.daydream, rerun: row.rerun, court: row.court, stayed_indoors_all_day: Boolean(row.stayed_indoors_all_day), left_room: Boolean(row.left_room) || kind === "body_or_room" || kind === "leave_room", left_home: Boolean(row.left_home), movement_minutes: row.movement_minutes ?? 0, interrupt_walk_minutes: row.interrupt_walk_minutes ?? 0, fantasy_walk_minutes: row.fantasy_walk_minutes ?? 0, headphones_on_walk: Boolean(row.headphones_on_walk), music_cinematic_on_walk: Boolean(row.music_cinematic_on_walk), proof_brick_done: true, proof_brick_kind: kind, fantasy_minutes_scheduled: row.fantasy_minutes_scheduled ?? 0, fantasy_minutes_unplanned: row.fantasy_minutes_unplanned ?? 0, note: row.note || "", }; } catch { base = { ...(EMPTY_DAILY_PATCH as DailyUpsert), left_room: kind === "body_or_room" || kind === "leave_room", proof_brick_done: true, proof_brick_kind: kind, }; } if (kind === "interrupt_walk" && !base.interrupt_walk_minutes) { base.interrupt_walk_minutes = 10; base.movement_minutes = (base.interrupt_walk_minutes || 0) + (base.fantasy_walk_minutes || 0); } await putDaily(today, base); toast.show("Proof noted on Daily"); closeProof(); } catch { try { await createEntry({ activity: "Proof brick", happened: `Proof step: ${kind}`, emotions: [], intensity: Math.min(intensity, 5), remedy: kind.replace(/_/g, " "), result: "worked", tags: ["proof", kind], }); toast.show("Proof logged"); closeProof(); } catch (err) { toast.show( err instanceof ApiError ? err.message : "Could not save proof", "error", ); } } finally { setProofBusy(false); } }; return (
navigate("/")} > ‹ Back

Log