Files
supabase/apps/studio/components/interfaces/UserDropdown/TimezoneDropdown.tsx
Jordi Enric 0920e48d12 feat(timezone-picker): pin UTC as second option FE-3570 (#46934)
## Problem

UTC was buried in the middle of the timezone list. Users managing
servers (which run in UTC) had to scroll or search to find it.

## Fix

Hardcode a UTC entry immediately after "Auto detect" so the two most
common choices are always at the top. The entry uses the standard `UTC`
IANA name and shows the checkmark when selected, consistent with all
other entries.

## How to test

- Open any page in Studio and click your user avatar.
- Open the Timezone submenu.
- Confirm the order is: Auto detect, (UTC) Coordinated Universal Time,
then the rest of the list.
- Select UTC and confirm the checkmark appears and the trigger label
updates.
- Search "UTC" in the search box and confirm it still matches.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added explicit "UTC (Coordinated Universal Time)" option at the top of
the timezone selector dropdown for easier access.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 16:26:25 +02:00

127 lines
4.6 KiB
TypeScript

import { CheckIcon } from 'lucide-react'
import { useMemo, useState } from 'react'
import {
cn,
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
ScrollArea,
} from 'ui'
import { findTimezoneByIana, TIMEZONES_BY_IANA } from '@/lib/constants/timezones'
import { useTimezone } from '@/lib/datetime'
import { guessLocalTimezone } from '@/lib/dayjs'
import { useTrack } from '@/lib/telemetry/track'
const AUTO_OPTION_VALUE = '__auto__'
export const TimezoneDropdown = () => {
const { timezone, storedTimezone, setTimezone, isAutoDetected } = useTimezone()
const track = useTrack()
const [open, setOpen] = useState(false)
// The "Auto detect" row always advertises the browser's own timezone, even
// when the user is currently overriding it with a manual pick.
const browserTimezone = useMemo(() => guessLocalTimezone(), [])
const triggerLabel = useMemo(() => {
return findTimezoneByIana(timezone)?.text ?? timezone
}, [timezone])
const handleSelect = (nextStored: string) => {
setTimezone(nextStored)
const resolvedNext = nextStored || guessLocalTimezone()
track('timezone_picker_clicked', {
previousTimezone: timezone,
nextTimezone: resolvedNext,
isAutoDetected: nextStored === '',
source: 'user_dropdown',
})
setOpen(false)
}
return (
<DropdownMenuSub open={open} onOpenChange={setOpen}>
<DropdownMenuSubTrigger className="flex gap-2 cursor-pointer">
<div className="flex flex-col min-w-0">
<span>Timezone</span>
<span className="text-xs text-foreground-lighter truncate" title={triggerLabel}>
{isAutoDetected ? `Auto (${timezone})` : triggerLabel}
</span>
</div>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent className="p-0 w-[320px]" sideOffset={4}>
<Command>
<CommandInput placeholder="Search timezone..." className="h-9" />
<CommandList>
<CommandEmpty>No timezones found</CommandEmpty>
<CommandGroup>
<ScrollArea className="h-72">
<CommandItem
key={AUTO_OPTION_VALUE}
value={`Auto detect ${browserTimezone}`}
onSelect={() => handleSelect('')}
>
<div className="flex flex-col">
<span>Auto detect</span>
<span className="text-xs text-foreground-lighter">{browserTimezone}</span>
</div>
<CheckIcon
className={cn(
'ml-auto h-4 w-4',
isAutoDetected ? 'opacity-100' : 'opacity-0'
)}
/>
</CommandItem>
<CommandItem
key="UTC"
value="UTC Coordinated Universal Time"
onSelect={() => handleSelect('UTC')}
>
{'(UTC) Coordinated Universal Time'}
<CheckIcon
className={cn(
'ml-auto h-4 w-4',
!isAutoDetected && storedTimezone === 'UTC' ? 'opacity-100' : 'opacity-0'
)}
/>
</CommandItem>
{TIMEZONES_BY_IANA.map((entry) => {
const ianaName = entry.utc[0]
const isSelected = !isAutoDetected && storedTimezone === ianaName
return (
<CommandItem
key={ianaName}
// CommandItem matches against the `value` prop for the input filter — include
// both the human label and the IANA name so search works for either.
value={`${entry.text} ${ianaName}`}
onSelect={() => handleSelect(ianaName)}
>
{entry.text}
<CheckIcon
className={cn(
'ml-auto h-4 w-4',
isSelected ? 'opacity-100' : 'opacity-0'
)}
/>
</CommandItem>
)
})}
</ScrollArea>
</CommandGroup>
</CommandList>
</Command>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
)
}