diff --git a/client/src/components/Dashboard/TimezoneWidget.tsx b/client/src/components/Dashboard/TimezoneWidget.tsx
index 5ee97e51..087937a7 100644
--- a/client/src/components/Dashboard/TimezoneWidget.tsx
+++ b/client/src/components/Dashboard/TimezoneWidget.tsx
@@ -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 && (
-
+
+ {/* Custom timezone */}
+
+
{t('dashboard.timezoneCustomTitle')}
+
+
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)' }} />
+
{ 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 &&
{customError}
}
+
+
+
+ {/* Popular zones */}
{POPULAR_ZONES.filter(z => !zones.find(existing => existing.tz === z.tz)).map(z => (