From 6cb006cc136f1fc5c83537cc30c64d223d1755e4 Mon Sep 17 00:00:00 2001 From: Solstice Date: Tue, 9 Jun 2026 00:22:18 -0700 Subject: feat: frontend view, state management, and user interface --- src/components/SettingsPanel.tsx | 240 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 src/components/SettingsPanel.tsx (limited to 'src/components/SettingsPanel.tsx') diff --git a/src/components/SettingsPanel.tsx b/src/components/SettingsPanel.tsx new file mode 100644 index 0000000..0d3cbf9 --- /dev/null +++ b/src/components/SettingsPanel.tsx @@ -0,0 +1,240 @@ +import { useState, useEffect } from 'react'; +import { useSettingsStore, Settings } from '../store/settingsStore'; + +interface SettingsPanelProps { + open: boolean; + onClose: () => void; +} + +export function SettingsPanel({ open, onClose }: SettingsPanelProps) { + const { settings, fetchSettings, updateSettings } = useSettingsStore(); + + const [workMins, setWorkMins] = useState(25); + const [shortBreakMins, setShortBreakMins] = useState(5); + const [longBreakMins, setLongBreakMins] = useState(15); + const [sessionsBeforeLong, setSessionsBeforeLong] = useState(4); + + useEffect(() => { + if (open && !settings) { + fetchSettings(); + } + }, [open, settings, fetchSettings]); + + useEffect(() => { + if (settings) { + setWorkMins(Math.round(settings.work_duration_secs / 60)); + setShortBreakMins(Math.round(settings.short_break_secs / 60)); + setLongBreakMins(Math.round(settings.long_break_secs / 60)); + setSessionsBeforeLong(settings.sessions_before_long_break); + } + }, [settings]); + + if (!open) return null; + + const handleSave = async () => { + const s: Settings = { + work_duration_secs: workMins * 60, + short_break_secs: shortBreakMins * 60, + long_break_secs: longBreakMins * 60, + sessions_before_long_break: sessionsBeforeLong, + }; + await updateSettings(s); + onClose(); + }; + + return ( +
+ {/* Modal card */} +
e.stopPropagation()} + style={{ + background: 'var(--ink-1)', + border: '1px solid var(--line-2)', + borderRadius: 'var(--r-3)', + boxShadow: 'var(--shadow-3)', + padding: '32px', + width: '360px', + display: 'flex', + flexDirection: 'column', + gap: '24px', + }} + > + + Settings + + +
+ + + + +
+ +
+ + +
+
+
+ ); +} + +interface NumberFieldProps { + label: string; + unit: string; + value: number; + min: number; + max: number; + onChange: (v: number) => void; +} + +function NumberField({ label, unit, value, min, max, onChange }: NumberFieldProps) { + return ( +
+ +
+ onChange(Math.min(max, Math.max(min, parseInt(e.target.value) || min)))} + style={{ + fontFamily: 'var(--font-mono)', + fontSize: '14px', + fontVariantNumeric: 'tabular-nums', + padding: '5px 8px', + borderRadius: 'var(--r-2)', + border: '1px solid var(--line-2)', + background: 'var(--ink-3)', + color: 'var(--fg-1)', + outline: 'none', + width: '56px', + textAlign: 'center', + }} + onFocus={(e) => { + (e.currentTarget as HTMLInputElement).style.boxShadow = 'var(--shadow-brass)'; + }} + onBlur={(e) => { + (e.currentTarget as HTMLInputElement).style.boxShadow = 'none'; + }} + /> + + {unit} + +
+
+ ); +} -- cgit v1.3-2-g0d8e