feat: custom timezones in timezone widget — closes #21

This commit is contained in:
Maurice
2026-03-28 23:23:52 +01:00
parent 31124a604a
commit 83d256ebac
4 changed files with 61 additions and 1 deletions

View File

@@ -51,6 +51,9 @@ export default function TimezoneWidget() {
})
const [now, setNow] = useState(Date.now())
const [showAdd, setShowAdd] = useState(false)
const [customLabel, setCustomLabel] = useState('')
const [customTz, setCustomTz] = useState('')
const [customError, setCustomError] = useState('')
useEffect(() => {
const i = setInterval(() => setNow(Date.now()), 10000)
@@ -61,6 +64,20 @@ export default function TimezoneWidget() {
localStorage.setItem('dashboard_timezones', JSON.stringify(zones))
}, [zones])
const isValidTz = (tz: string) => {
try { Intl.DateTimeFormat('en-US', { timeZone: tz }).format(new Date()); return true } catch { return false }
}
const addCustomZone = () => {
const tz = customTz.trim()
if (!tz) { setCustomError(t('dashboard.timezoneCustomErrorEmpty')); return }
if (!isValidTz(tz)) { setCustomError(t('dashboard.timezoneCustomErrorInvalid')); return }
if (zones.find(z => z.tz === tz)) { setCustomError(t('dashboard.timezoneCustomErrorDuplicate')); return }
const label = customLabel.trim() || tz.split('/').pop()?.replace(/_/g, ' ') || tz
setZones([...zones, { label, tz }])
setCustomLabel(''); setCustomTz(''); setCustomError(''); setShowAdd(false)
}
const addZone = (zone) => {
if (!zones.find(z => z.tz === zone.tz)) {
setZones([...zones, zone])
@@ -108,7 +125,29 @@ export default function TimezoneWidget() {
{/* Add zone dropdown */}
{showAdd && (
<div className="mt-2 rounded-xl p-2 max-h-[200px] overflow-auto" style={{ background: 'var(--bg-secondary)' }}>
<div className="mt-2 rounded-xl p-2 max-h-[280px] overflow-auto" style={{ background: 'var(--bg-secondary)' }}>
{/* Custom timezone */}
<div className="px-2 py-2 mb-2 rounded-lg" style={{ background: 'var(--bg-card)' }}>
<p className="text-[10px] font-semibold uppercase tracking-wide mb-2" style={{ color: 'var(--text-faint)' }}>{t('dashboard.timezoneCustomTitle')}</p>
<div className="space-y-1.5">
<input value={customLabel} onChange={e => setCustomLabel(e.target.value)}
placeholder={t('dashboard.timezoneCustomLabelPlaceholder')}
className="w-full px-2 py-1.5 rounded-lg text-xs outline-none"
style={{ background: 'var(--bg-secondary)', color: 'var(--text-primary)', border: '1px solid var(--border-secondary)' }} />
<input value={customTz} onChange={e => { setCustomTz(e.target.value); setCustomError('') }}
placeholder={t('dashboard.timezoneCustomTzPlaceholder')}
className="w-full px-2 py-1.5 rounded-lg text-xs outline-none"
style={{ background: 'var(--bg-secondary)', color: 'var(--text-primary)', border: `1px solid ${customError ? '#ef4444' : 'var(--border-secondary)'}` }}
onKeyDown={e => { if (e.key === 'Enter') addCustomZone() }} />
{customError && <p className="text-[10px]" style={{ color: '#ef4444' }}>{customError}</p>}
<button onClick={addCustomZone}
className="w-full py-1.5 rounded-lg text-xs font-medium transition-colors"
style={{ background: 'var(--text-primary)', color: 'var(--bg-primary)' }}>
{t('dashboard.timezoneCustomAdd')}
</button>
</div>
</div>
{/* Popular zones */}
{POPULAR_ZONES.filter(z => !zones.find(existing => existing.tz === z.tz)).map(z => (
<button key={z.tz} onClick={() => addZone(z)}
className="w-full flex items-center justify-between px-2 py-1.5 rounded-lg text-xs text-left transition-colors"

View File

@@ -54,6 +54,13 @@ const de: Record<string, string> = {
'dashboard.currency': 'Währung',
'dashboard.timezone': 'Zeitzonen',
'dashboard.localTime': 'Lokal',
'dashboard.timezoneCustomTitle': 'Eigene Zeitzone',
'dashboard.timezoneCustomLabelPlaceholder': 'Bezeichnung (optional)',
'dashboard.timezoneCustomTzPlaceholder': 'z.B. America/New_York',
'dashboard.timezoneCustomAdd': 'Hinzufügen',
'dashboard.timezoneCustomErrorEmpty': 'Zeitzone eingeben',
'dashboard.timezoneCustomErrorInvalid': 'Ungültige Zeitzone. Format: Europe/Berlin',
'dashboard.timezoneCustomErrorDuplicate': 'Bereits hinzugefügt',
'dashboard.emptyTitle': 'Noch keine Reisen',
'dashboard.emptyText': 'Erstelle deine erste Reise und beginne mit der Planung von Orten, Tagesabläufen und Packlisten.',
'dashboard.emptyButton': 'Erste Reise erstellen',

View File

@@ -54,6 +54,13 @@ const en: Record<string, string> = {
'dashboard.currency': 'Currency',
'dashboard.timezone': 'Timezones',
'dashboard.localTime': 'Local',
'dashboard.timezoneCustomTitle': 'Custom Timezone',
'dashboard.timezoneCustomLabelPlaceholder': 'Label (optional)',
'dashboard.timezoneCustomTzPlaceholder': 'e.g. America/New_York',
'dashboard.timezoneCustomAdd': 'Add',
'dashboard.timezoneCustomErrorEmpty': 'Enter a timezone identifier',
'dashboard.timezoneCustomErrorInvalid': 'Invalid timezone. Use format like Europe/Berlin',
'dashboard.timezoneCustomErrorDuplicate': 'Already added',
'dashboard.emptyTitle': 'No trips yet',
'dashboard.emptyText': 'Create your first trip and start planning!',
'dashboard.emptyButton': 'Create First Trip',

View File

@@ -55,6 +55,13 @@ const es: Record<string, string> = {
'dashboard.currency': 'Divisa',
'dashboard.timezone': 'Zonas horarias',
'dashboard.localTime': 'Hora local',
'dashboard.timezoneCustomTitle': 'Zona horaria personalizada',
'dashboard.timezoneCustomLabelPlaceholder': 'Nombre (opcional)',
'dashboard.timezoneCustomTzPlaceholder': 'ej. America/New_York',
'dashboard.timezoneCustomAdd': 'Añadir',
'dashboard.timezoneCustomErrorEmpty': 'Introduce una zona horaria',
'dashboard.timezoneCustomErrorInvalid': 'Zona horaria no válida. Usa formato como Europe/Madrid',
'dashboard.timezoneCustomErrorDuplicate': 'Ya añadida',
'dashboard.emptyTitle': 'Aún no hay viajes',
'dashboard.emptyText': 'Crea tu primer viaje y empieza a planificar',
'dashboard.emptyButton': 'Crear primer viaje',