Fastwhisper / frontend /src /components /MoveNowSheet.tsx
Mbonea's picture
Close P1 arch-code gaps: loop summary, template-echo Rules, Move now, proof kinds.
57ed4c2
Raw
History Blame Contribute Delete
6.56 kB
/** 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>
);
}