From 65ce9fd4c8c12986ae603c339aaa30d799e59a64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 14 May 2026 12:01:58 +0200 Subject: [PATCH] feat(profile): timezone picker on account settings page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v1.4.25 W7 phase 4 — proposal §5. Adds a per-user timezone picker to /settings#account between date-of-birth and language. The control is a native ` over `Intl.supportedValuesOf('timeZone')` + * plus a "use my browser zone" button. The control is uncontrolled + * w.r.t. its parent — the parent passes in `value` + `onChange`, but + * the parent owns the network round-trip so this component stays + * synchronous + presentational. The save POSTs + * `PUT /api/auth/me/timezone` in the parent form's submit handler. + * + * Fallback: on older engines without `Intl.supportedValuesOf` we + * render a free-text input. Every modern browser back to mid-2022 + * (Chrome 99 / Safari 15.4 / Firefox 99) ships the API; the fallback + * is defensive only. + */ +import { useMemo, useState } from "react"; +import { Compass } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + detectBrowserTimezone, + listSupportedTimezones, +} from "@/lib/tz/resolver"; +import { useTranslations } from "@/lib/i18n/context"; + +const NATIVE_SELECT_CLASS = + "border-input bg-background text-foreground ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border px-3 py-1 text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"; + +export interface TimezonePickerProps { + /** The user's current stored timezone. */ + value: string; + /** Called when the user picks (or types) a new zone. */ + onChange: (next: string) => void; + /** Render-only id for the label association. */ + id?: string; + /** Optional override of the hint copy under the picker. */ + hint?: string; +} + +export function TimezonePicker({ + value, + onChange, + id = "timezone", + hint, +}: TimezonePickerProps) { + const { t } = useTranslations(); + // `listSupportedTimezones()` is engine-side; memoise so the list + // isn't reconstructed on every render. + const zones = useMemo(() => listSupportedTimezones(), []); + const [detected] = useState(() => detectBrowserTimezone()); + + const handleUseBrowserTz = () => { + if (detected) onChange(detected); + }; + + const labelHint = hint ?? t("settings.timezoneHint"); + + if (zones.length === 0) { + // Older engine — free-text fallback. + return ( +
+ + onChange(e.target.value)} + placeholder="Europe/Berlin" + autoComplete="off" + /> +

{labelHint}

+ +
+ ); + } + + return ( +
+ +
+ + +
+

{labelHint}

+
+ ); +}