Spaces:
Sleeping
Sleeping
| /** Home “Move now” checklist → Daily + optional Log prefill. */ | |
| import { useState } from "preact/hooks"; | |
| import { | |
| ApiError, | |
| getDaily, | |
| putDaily, | |
| type DailyUpsert, | |
| } from "../api"; | |
| import { isoDate } from "../dates"; | |
| import { navigate } from "../router"; | |
| import { Button } from "./Button"; | |
| import { Pressable } from "./Pressable"; | |
| import { Sheet } from "./Sheet"; | |
| import { useToast } from "./Toast"; | |
| const EMPTY: DailyUpsert = { | |
| 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: "", | |
| }; | |
| type Props = { | |
| open: boolean; | |
| onClose: () => void; | |
| indoorsStreak?: number; | |
| }; | |
| export function MoveNowSheet({ open, onClose, indoorsStreak = 0 }: Props) { | |
| const toast = useToast(); | |
| const [leftBed, setLeftBed] = useState(false); | |
| const [walletHome, setWalletHome] = useState(true); | |
| const [walkKind, setWalkKind] = useState<"interrupt" | "fantasy">("interrupt"); | |
| const [minutes, setMinutes] = useState(20); | |
| const [busy, setBusy] = useState(false); | |
| const save = async (alsoLog: boolean) => { | |
| setBusy(true); | |
| const today = isoDate(new Date()); | |
| try { | |
| let base: DailyUpsert = { ...EMPTY }; | |
| 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: false, | |
| left_room: leftBed || Boolean(row.left_room), | |
| left_home: Boolean(row.left_home) || minutes >= 15, | |
| 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: | |
| walkKind === "interrupt" ? false : Boolean(row.headphones_on_walk), | |
| music_cinematic_on_walk: | |
| walkKind === "interrupt" ? false : Boolean(row.music_cinematic_on_walk), | |
| proof_brick_done: true, | |
| proof_brick_kind: "interrupt_walk", | |
| fantasy_minutes_scheduled: row.fantasy_minutes_scheduled ?? 0, | |
| fantasy_minutes_unplanned: row.fantasy_minutes_unplanned ?? 0, | |
| note: row.note || "", | |
| }; | |
| } catch { | |
| base = { | |
| ...EMPTY, | |
| left_room: leftBed, | |
| left_home: minutes >= 15, | |
| proof_brick_done: true, | |
| proof_brick_kind: "interrupt_walk", | |
| headphones_on_walk: walkKind !== "interrupt", | |
| }; | |
| } | |
| if (walkKind === "interrupt") { | |
| base.interrupt_walk_minutes = Math.max( | |
| base.interrupt_walk_minutes || 0, | |
| minutes, | |
| ); | |
| base.headphones_on_walk = false; | |
| base.music_cinematic_on_walk = false; | |
| } else { | |
| base.fantasy_walk_minutes = Math.max( | |
| base.fantasy_walk_minutes || 0, | |
| minutes, | |
| ); | |
| } | |
| base.movement_minutes = | |
| (base.interrupt_walk_minutes || 0) + (base.fantasy_walk_minutes || 0); | |
| base.stayed_indoors_all_day = false; | |
| await putDaily(today, base); | |
| toast.show("Movement saved on Daily"); | |
| onClose(); | |
| if (alsoLog) { | |
| const happened = encodeURIComponent( | |
| walkKind === "interrupt" | |
| ? `Interrupt walk ${minutes} min — wallet home, first 15 no headphones` | |
| : `Music / fantasy walk ${minutes} min — leisure, not medicine`, | |
| ); | |
| navigate(`/log?happened=${happened}&remedy=${encodeURIComponent("Interrupt walk")}`); | |
| } | |
| } catch (err) { | |
| toast.show( | |
| err instanceof ApiError ? err.message : "Could not save movement", | |
| "error", | |
| ); | |
| } finally { | |
| setBusy(false); | |
| } | |
| }; | |
| return ( | |
| <Sheet open={open} title="Move now" onClose={onClose}> | |
| <div class="stack"> | |
| <p class="muted"> | |
| Preview, not proof. Interrupt walk: no headphones, eyes outside. | |
| {indoorsStreak >= 2 | |
| ? ` ${indoorsStreak} days indoors — 20 min out, wallet home, first 15 min no headphones.` | |
| : ""} | |
| </p> | |
| <label class="toggle-row"> | |
| <input | |
| type="checkbox" | |
| checked={leftBed} | |
| onChange={(e) => setLeftBed((e.target as HTMLInputElement).checked)} | |
| /> | |
| <span>Left bed / room</span> | |
| </label> | |
| <label class="toggle-row"> | |
| <input | |
| type="checkbox" | |
| checked={walletHome} | |
| onChange={(e) => setWalletHome((e.target as HTMLInputElement).checked)} | |
| /> | |
| <span>Wallet stays home (no spend needed)</span> | |
| </label> | |
| <span class="field-label">Walk type</span> | |
| <div class="chip-row" role="group" aria-label="Walk type"> | |
| <Pressable | |
| className="chip" | |
| ariaPressed={walkKind === "interrupt"} | |
| onClick={() => setWalkKind("interrupt")} | |
| > | |
| Interrupt (medicine) | |
| </Pressable> | |
| <Pressable | |
| className="chip" | |
| ariaPressed={walkKind === "fantasy"} | |
| onClick={() => setWalkKind("fantasy")} | |
| > | |
| Music walk (leisure) | |
| </Pressable> | |
| </div> | |
| {walkKind === "interrupt" ? ( | |
| <p class="muted">Interrupt walk: no headphones, eyes outside.</p> | |
| ) : ( | |
| <p class="muted">Music walk: leisure, not medicine.</p> | |
| )} | |
| <label class="field-label" for="move-min"> | |
| Minutes · {minutes} | |
| </label> | |
| <input | |
| id="move-min" | |
| type="range" | |
| min={5} | |
| max={60} | |
| step={5} | |
| value={minutes} | |
| onInput={(e) => setMinutes(Number((e.target as HTMLInputElement).value))} | |
| /> | |
| <Button disabled={busy || !leftBed} onClick={() => void save(false)}> | |
| Save to Daily | |
| </Button> | |
| <Button | |
| variant="secondary" | |
| disabled={busy || !leftBed} | |
| onClick={() => void save(true)} | |
| > | |
| Save + open Log | |
| </Button> | |
| </div> | |
| </Sheet> | |
| ); | |
| } | |