Skip to content

Commit 8c9a761

Browse files
leopu00claude
andcommitted
feat(web): UserPreferences — tema, lingua, notifiche toggle, shortcut con persistenza API
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent aaef0fc commit 8c9a761

1 file changed

Lines changed: 133 additions & 0 deletions

File tree

web/components/UserPreferences.tsx

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
'use client'
2+
3+
import { useEffect, useState, useCallback } from 'react'
4+
import { useTheme } from '../app/theme-provider'
5+
6+
type Prefs = {
7+
theme: 'dark' | 'light'
8+
language: 'it' | 'en'
9+
notifications: { enabled: boolean; sound: boolean; desktop: boolean }
10+
shortcuts: Record<string, string>
11+
}
12+
13+
const DEFAULTS: Prefs = {
14+
theme: 'dark', language: 'it',
15+
notifications: { enabled: true, sound: false, desktop: false },
16+
shortcuts: {},
17+
}
18+
19+
const SHORTCUT_LABELS: Record<string, string> = {
20+
'cmd+k': 'Apri Command Palette',
21+
'cmd+/': 'Toggle sidebar',
22+
'g d': 'Vai a Dashboard',
23+
'g a': 'Vai a Agenti',
24+
}
25+
26+
function Toggle({ checked, onChange, label }: { checked: boolean; onChange: (v: boolean) => void; label: string }) {
27+
return (
28+
<label className="flex items-center justify-between cursor-pointer py-1.5">
29+
<span className="text-[11px]" style={{ color: 'var(--color-muted)' }}>{label}</span>
30+
<div onClick={() => onChange(!checked)}
31+
className="w-8 h-4 rounded-full transition-colors cursor-pointer relative"
32+
style={{ background: checked ? 'var(--color-green)' : 'var(--color-border)' }}>
33+
<div className="absolute top-0.5 w-3 h-3 rounded-full transition-all"
34+
style={{ background: 'var(--color-bg)', left: checked ? '1.125rem' : '0.125rem' }} />
35+
</div>
36+
</label>
37+
)
38+
}
39+
40+
function Section({ title, children }: { title: string; children: React.ReactNode }) {
41+
return (
42+
<div className="flex flex-col gap-1">
43+
<p className="text-[9px] font-semibold tracking-[0.18em] uppercase mb-1" style={{ color: 'var(--color-dim)' }}>{title}</p>
44+
{children}
45+
</div>
46+
)
47+
}
48+
49+
export default function UserPreferences({ onClose }: { onClose: () => void }) {
50+
const { theme, setTheme } = useTheme()
51+
const [prefs, setPrefs] = useState<Prefs>({ ...DEFAULTS, theme })
52+
const [status, setStatus] = useState<'idle' | 'saving' | 'ok'>('idle')
53+
54+
useEffect(() => {
55+
fetch('/api/preferences').then(r => r.json()).then(p => setPrefs({ ...DEFAULTS, ...p, notifications: { ...DEFAULTS.notifications, ...(p.notifications ?? {}) } })).catch(() => {})
56+
}, [])
57+
58+
const patch = useCallback(async (partial: Partial<Prefs>) => {
59+
const next = { ...prefs, ...partial, notifications: { ...prefs.notifications, ...((partial.notifications as object) ?? {}) } } as Prefs
60+
setPrefs(next)
61+
setStatus('saving')
62+
try {
63+
await fetch('/api/preferences', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(partial) })
64+
if (partial.theme) setTheme(partial.theme)
65+
setStatus('ok')
66+
setTimeout(() => setStatus('idle'), 1500)
67+
} catch { setStatus('idle') }
68+
}, [prefs, setTheme])
69+
70+
const reset = useCallback(async () => {
71+
await fetch('/api/preferences', { method: 'DELETE' })
72+
setPrefs({ ...DEFAULTS })
73+
setTheme('dark')
74+
}, [setTheme])
75+
76+
return (
77+
<div className="flex flex-col gap-5 min-w-[280px]">
78+
<Section title="Aspetto">
79+
<label className="flex items-center justify-between py-1.5">
80+
<span className="text-[11px]" style={{ color: 'var(--color-muted)' }}>Tema</span>
81+
<div className="flex gap-1.5">
82+
{(['dark', 'light'] as const).map(t => (
83+
<button key={t} onClick={() => patch({ theme: t })}
84+
className="px-2.5 py-1 rounded text-[10px] font-semibold cursor-pointer transition-colors"
85+
style={{ background: prefs.theme === t ? 'var(--color-green)' : 'var(--color-border)', color: prefs.theme === t ? 'var(--color-bg)' : 'var(--color-muted)' }}>
86+
{t === 'dark' ? 'Scuro' : 'Chiaro'}
87+
</button>
88+
))}
89+
</div>
90+
</label>
91+
<label className="flex items-center justify-between py-1.5">
92+
<span className="text-[11px]" style={{ color: 'var(--color-muted)' }}>Lingua</span>
93+
<div className="flex gap-1.5">
94+
{(['it', 'en'] as const).map(l => (
95+
<button key={l} onClick={() => patch({ language: l })}
96+
className="px-2.5 py-1 rounded text-[10px] font-semibold cursor-pointer transition-colors"
97+
style={{ background: prefs.language === l ? 'var(--color-green)' : 'var(--color-border)', color: prefs.language === l ? 'var(--color-bg)' : 'var(--color-muted)' }}>
98+
{l.toUpperCase()}
99+
</button>
100+
))}
101+
</div>
102+
</label>
103+
</Section>
104+
105+
<Section title="Notifiche">
106+
<Toggle checked={prefs.notifications.enabled} label="Notifiche attive" onChange={v => patch({ notifications: { ...prefs.notifications, enabled: v } })} />
107+
<Toggle checked={prefs.notifications.sound} label="Suono" onChange={v => patch({ notifications: { ...prefs.notifications, sound: v } })} />
108+
<Toggle checked={prefs.notifications.desktop} label="Desktop" onChange={v => patch({ notifications: { ...prefs.notifications, desktop: v } })} />
109+
</Section>
110+
111+
<Section title="Scorciatoie">
112+
{Object.entries(SHORTCUT_LABELS).map(([key, label]) => (
113+
<div key={key} className="flex items-center justify-between py-1">
114+
<span className="text-[11px]" style={{ color: 'var(--color-muted)' }}>{label}</span>
115+
<kbd className="text-[9px] px-1.5 py-0.5 rounded font-mono" style={{ background: 'var(--color-border)', color: 'var(--color-dim)' }}>
116+
{prefs.shortcuts[key] ?? key}
117+
</kbd>
118+
</div>
119+
))}
120+
</Section>
121+
122+
<div className="flex items-center justify-between pt-1 border-t" style={{ borderColor: 'var(--color-border)' }}>
123+
<button onClick={reset} className="text-[10px] cursor-pointer" style={{ color: 'var(--color-dim)', background: 'none', border: 'none' }}>
124+
Ripristina default
125+
</button>
126+
<button onClick={onClose} className="px-3 py-1.5 rounded text-[11px] font-semibold cursor-pointer"
127+
style={{ background: 'var(--color-green)', color: 'var(--color-bg)' }}>
128+
{status === 'saving' ? '…' : status === 'ok' ? '✓ Salvato' : 'Chiudi'}
129+
</button>
130+
</div>
131+
</div>
132+
)
133+
}

0 commit comments

Comments
 (0)