diff --git a/apps/pwa/src/routes/AdminPage.tsx b/apps/pwa/src/routes/AdminPage.tsx index df9e5c5..ccacc83 100644 --- a/apps/pwa/src/routes/AdminPage.tsx +++ b/apps/pwa/src/routes/AdminPage.tsx @@ -30,6 +30,8 @@ import { fetchAdminMembers, fetchAdminCalendars, setSharedCalendar, + fetchAdminTimezone, + setAdminTimezone, type AdminMember, type AdminCalendar, } from '../api/client.js'; @@ -60,6 +62,9 @@ export function AdminPage() { // Shared calendar picker state const [selectedCalendarId, setSelectedCalendarId] = useState(null); + // Timezone picker state + const [timezoneInput, setTimezoneInput] = useState(null); + // Members query const membersQuery = useQuery({ queryKey: ['admin', 'members'], @@ -82,6 +87,42 @@ export function AdminPage() { // Effective selected = user pick OR fallback to current saved const effectiveSelected = selectedCalendarId ?? currentSharedId; + // Timezone query + const timezoneQuery = useQuery({ + queryKey: ['admin', 'timezone'], + queryFn: fetchAdminTimezone, + retry: false, + staleTime: 60 * 1000, + }); + + // Timezone save mutation + const timezoneMutation = useMutation({ + mutationFn: (tz: string) => setAdminTimezone(tz), + onSuccess: () => { + void queryClient.invalidateQueries({ queryKey: ['admin', 'timezone'] }); + setTimezoneInput(null); // reset local override after save + }, + }); + + // Detected browser timezone (D-02) + const detectedTz = Intl.DateTimeFormat().resolvedOptions().timeZone; + + // Effective timezone input value: local override → stored value → '' + const storedTimezone = timezoneQuery.data?.timezone ?? ''; + const effectiveTimezoneInput = timezoneInput ?? storedTimezone; + + // Save is disabled when pending, or when input matches what's stored + const timezoneSaveDisabled = + timezoneMutation.isPending || + effectiveTimezoneInput === '' || + effectiveTimezoneInput === storedTimezone; + + // IANA zones list for the datalist (Intl.supportedValuesOf may not be present in all runtimes) + const ianaZones: string[] = + typeof (Intl as { supportedValuesOf?: (key: string) => string[] }).supportedValuesOf === 'function' + ? (Intl as { supportedValuesOf: (key: string) => string[] }).supportedValuesOf('timeZone') + : []; + // Save shared calendar mutation const sharedCalMutation = useMutation({ mutationFn: (calId: number) => setSharedCalendar(calId), @@ -180,7 +221,7 @@ export function AdminPage() { {/* ── SHARED CALENDAR section ──────────────────────────────────────── */} -
+
Shared Calendar

)}

+ + {/* ── TIMEZONE section ─────────────────────────────────────────────── */} +
+
Timezone
+ + {timezoneQuery.isLoading && ( +
+ Loading timezone… +
+ )} + + {timezoneQuery.isError && ( +
+ Could not load timezone setting. +
+ )} + + {timezoneQuery.data && ( + <> + {/* System-default notice (D-06) */} + {!timezoneQuery.data.isExplicitlySet && ( +
+ Using system default — save a timezone to make it explicit. +
+ )} + + {/* Searchable IANA picker */} +
+ setTimezoneInput(e.target.value)} + placeholder="e.g. America/Chicago" + aria-label="Household timezone" + style={{ + width: '100%', + boxSizing: 'border-box', + padding: 'var(--space-2, 8px) var(--space-3, 12px)', + fontSize: 'var(--text-body-size, 15px)', + fontFamily: 'var(--font-family-base)', + border: '1px solid var(--color-border)', + borderRadius: 'var(--space-1, 4px)', + color: 'var(--color-text-primary)', + background: 'var(--color-surface, #ffffff)', + minHeight: '44px', + }} + /> + + {ianaZones.map((tz) => ( + +
+ + {/* Use detected zone affordance (D-02) */} + {detectedTz && detectedTz !== effectiveTimezoneInput && ( +
+ +
+ )} + + {/* Save button */} +
+ +
+ + {timezoneMutation.isError && ( +
+ Could not save timezone. Please check the value and try again. +
+ )} + + )} +
{/* Credential sheet — admin-rotate or admin-add */}