diff --git a/components/mobile/profile/ProfileTimezoneSection.tsx b/components/mobile/profile/ProfileTimezoneSection.tsx new file mode 100644 index 0000000..de50b8e --- /dev/null +++ b/components/mobile/profile/ProfileTimezoneSection.tsx @@ -0,0 +1,165 @@ +'use client'; + +import { useState, useEffect, useMemo, useRef, useCallback } from 'react'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; +import { + Command, + CommandEmpty, + CommandInput, + CommandItem, + CommandList, +} from '@/components/ui/command'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { Button } from '@/components/ui/button'; +import { ChevronsUpDown, Check } from 'lucide-react'; +import { toast } from 'sonner'; +import ProfileSectionSkeleton from './ProfileSectionSkeleton'; + +const EXTRA_ALLOWED_TIMEZONES = ['UTC', 'Etc/UTC', 'GMT', 'Etc/GMT']; + +function formatCurrentTime(zone: string): string { + try { + return new Intl.DateTimeFormat('en-US', { + dateStyle: 'short', + timeStyle: 'short', + timeZone: zone, + }).format(new Date()); + } catch { + return ''; + } +} + +export function ProfileTimezoneSection() { + const [loading, setLoading] = useState(true); + const [selectedZone, setSelectedZone] = useState('UTC'); + const [open, setOpen] = useState(false); + const [saveError, setSaveError] = useState(null); + // Tick state to force re-render every minute for live clock + const [, setTick] = useState(0); + const debounceTimerRef = useRef | null>(null); + + // Build sorted, de-duplicated timezone list once + const timezones = useMemo(() => { + let zones: string[] = []; + try { + zones = Intl.supportedValuesOf('timeZone'); + } catch { + zones = []; + } + const combined = new Set([...zones, ...EXTRA_ALLOWED_TIMEZONES]); + return Array.from(combined).sort(); + }, []); + + // Fetch current timezone on mount + useEffect(() => { + fetch('/api/me/timezone') + .then((r) => r.json()) + .then((data: { timezone: string }) => { + setSelectedZone(data.timezone); + setLoading(false); + }) + .catch(() => { + setLoading(false); + }); + }, []); + + // Re-tick every 60s so the "current time" line stays fresh + useEffect(() => { + const interval = setInterval(() => { + setTick((t) => t + 1); + }, 60000); + return () => clearInterval(interval); + }, []); + + const handleSelect = useCallback( + (zone: string) => { + setOpen(false); + setSaveError(null); + + const previousZone = selectedZone; + setSelectedZone(zone); + + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); + } + + debounceTimerRef.current = setTimeout(async () => { + try { + const res = await fetch('/api/me/timezone', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ timezone: zone }), + }); + if (!res.ok) { + throw new Error(`HTTP ${res.status}`); + } + const data: { timezone: string } = await res.json(); + setSelectedZone(data.timezone); + toast.success('Timezone updated'); + } catch { + setSelectedZone(previousZone); + setSaveError("Couldn't save. Try again."); + toast.error('Failed to update timezone'); + } + }, 400); + }, + [selectedZone], + ); + + if (loading) { + return ; + } + + return ( + + + Timezone + + + + + + + + + + No matches. + + {timezones.map((zone) => ( + + {zone} + {selectedZone === zone && ( + + )} + + ))} + + + + + + {saveError && ( +

{saveError}

+ )} + +

+ Your current time:{' '} + {formatCurrentTime(selectedZone)}{' '} + in {selectedZone} +

+
+
+ ); +}