diff options
Diffstat (limited to 'src/components/SettingsPanel.tsx')
| -rw-r--r-- | src/components/SettingsPanel.tsx | 240 |
1 files changed, 240 insertions, 0 deletions
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 ( + <div + onClick={onClose} + style={{ + position: 'fixed', + inset: 0, + background: 'rgba(11, 14, 19, 0.70)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + zIndex: 100, + }} + > + {/* Modal card */} + <div + onClick={(e) => 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', + }} + > + <span className="eyebrow" style={{ letterSpacing: '0.18em' }}> + Settings + </span> + + <div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}> + <NumberField + label="Work duration" + unit="min" + value={workMins} + min={1} + max={120} + onChange={setWorkMins} + /> + <NumberField + label="Short break" + unit="min" + value={shortBreakMins} + min={1} + max={60} + onChange={setShortBreakMins} + /> + <NumberField + label="Long break" + unit="min" + value={longBreakMins} + min={1} + max={60} + onChange={setLongBreakMins} + /> + <NumberField + label="Sessions before long break" + unit="sessions" + value={sessionsBeforeLong} + min={1} + max={10} + onChange={setSessionsBeforeLong} + /> + </div> + + <div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end' }}> + <button + onClick={onClose} + style={{ + fontFamily: 'var(--font-sans)', + fontSize: '14px', + fontWeight: 500, + padding: '8px 16px', + borderRadius: 'var(--r-1)', + border: '1px solid var(--line-2)', + background: 'transparent', + color: 'var(--brass)', + cursor: 'pointer', + transition: 'border-color 0.15s ease', + }} + onMouseEnter={(e) => { + (e.currentTarget as HTMLButtonElement).style.borderColor = 'var(--brass)'; + }} + onMouseLeave={(e) => { + (e.currentTarget as HTMLButtonElement).style.borderColor = 'var(--line-2)'; + }} + > + Cancel + </button> + <button + onClick={handleSave} + style={{ + fontFamily: 'var(--font-sans)', + fontSize: '14px', + fontWeight: 600, + padding: '8px 20px', + borderRadius: 'var(--r-1)', + border: 'none', + background: 'var(--brass)', + color: 'var(--brass-ink)', + cursor: 'pointer', + transition: 'background 0.15s ease', + }} + onMouseEnter={(e) => { + (e.currentTarget as HTMLButtonElement).style.background = 'var(--brass-bright)'; + }} + onMouseLeave={(e) => { + (e.currentTarget as HTMLButtonElement).style.background = 'var(--brass)'; + }} + onMouseDown={(e) => { + (e.currentTarget as HTMLButtonElement).style.background = 'var(--brass-deep)'; + }} + onMouseUp={(e) => { + (e.currentTarget as HTMLButtonElement).style.background = 'var(--brass-bright)'; + }} + > + Save + </button> + </div> + </div> + </div> + ); +} + +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 ( + <div + style={{ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: '16px', + }} + > + <label + style={{ + fontFamily: 'var(--font-sans)', + fontSize: '14px', + color: 'var(--fg-2)', + flex: 1, + }} + > + {label} + </label> + <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}> + <input + type="number" + min={min} + max={max} + value={value} + onChange={(e) => 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'; + }} + /> + <span + style={{ + fontFamily: 'var(--font-sans)', + fontSize: '12px', + color: 'var(--fg-4)', + minWidth: '44px', + }} + > + {unit} + </span> + </div> + </div> + ); +} |
