mirror of
https://github.com/perfect-panel/ppanel-web.git
synced 2026-02-06 11:40:28 -05:00
282 lines
9.3 KiB
TypeScript
282 lines
9.3 KiB
TypeScript
'use client';
|
|
|
|
import { Button } from '@workspace/ui/components/button';
|
|
import {
|
|
Command,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
CommandList,
|
|
} from '@workspace/ui/components/command';
|
|
import { Popover, PopoverContent, PopoverTrigger } from '@workspace/ui/components/popover';
|
|
import { Icon } from '@workspace/ui/custom-components/icon';
|
|
import { useLocale } from 'next-intl';
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
|
|
interface TimezoneOption {
|
|
value: string;
|
|
label: string;
|
|
timezone: string;
|
|
}
|
|
|
|
function getCurrentTime(timezone: string): string {
|
|
try {
|
|
const now = new Date();
|
|
return now.toLocaleTimeString('en-US', {
|
|
timeZone: timezone,
|
|
hour12: false,
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
} catch {
|
|
return '--:--';
|
|
}
|
|
}
|
|
|
|
function getAllTimezones(locale: string = 'en-US'): TimezoneOption[] {
|
|
try {
|
|
const timeZones = Intl.supportedValuesOf('timeZone');
|
|
|
|
const processed = timeZones
|
|
.map((tz) => {
|
|
try {
|
|
return {
|
|
value: tz,
|
|
label: tz,
|
|
timezone: getTimezoneOffset(tz),
|
|
};
|
|
} catch {
|
|
return {
|
|
value: tz,
|
|
label: tz,
|
|
timezone: 'UTC+00:00',
|
|
};
|
|
}
|
|
})
|
|
.filter(Boolean)
|
|
.sort((a, b) => a.label.localeCompare(b.label, locale));
|
|
|
|
const hasUTC = processed.some((tz) => tz.value === 'UTC');
|
|
if (!hasUTC) {
|
|
processed.unshift({
|
|
value: 'UTC',
|
|
label: 'UTC',
|
|
timezone: 'UTC+00:00',
|
|
});
|
|
}
|
|
|
|
return processed;
|
|
} catch {
|
|
return [
|
|
{
|
|
value: 'UTC',
|
|
label: 'UTC',
|
|
timezone: 'UTC+00:00',
|
|
},
|
|
];
|
|
}
|
|
}
|
|
|
|
function getServerTimezones(): string[] {
|
|
return ['UTC'];
|
|
}
|
|
|
|
function getRecommendedTimezones(): string[] {
|
|
try {
|
|
const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
|
|
if (browserTimezone.startsWith('Asia/')) {
|
|
return ['Asia/Shanghai', 'Asia/Tokyo', 'Asia/Kolkata', 'Asia/Singapore', 'Asia/Seoul'];
|
|
} else if (browserTimezone.startsWith('Europe/')) {
|
|
return ['Europe/London', 'Europe/Paris', 'Europe/Berlin', 'Europe/Rome', 'Europe/Madrid'];
|
|
} else if (browserTimezone.startsWith('America/')) {
|
|
return [
|
|
'America/New_York',
|
|
'America/Los_Angeles',
|
|
'America/Chicago',
|
|
'America/Denver',
|
|
'America/Toronto',
|
|
];
|
|
} else if (browserTimezone.startsWith('Australia/')) {
|
|
return ['Australia/Sydney', 'Australia/Melbourne', 'Australia/Perth', 'Australia/Brisbane'];
|
|
} else {
|
|
return [
|
|
'America/New_York',
|
|
'Europe/London',
|
|
'Asia/Shanghai',
|
|
'Asia/Tokyo',
|
|
'Australia/Sydney',
|
|
];
|
|
}
|
|
} catch {
|
|
return ['America/New_York', 'Europe/London', 'Asia/Shanghai', 'Asia/Tokyo', 'Australia/Sydney'];
|
|
}
|
|
}
|
|
|
|
function getTimezoneOffset(timezone: string): string {
|
|
try {
|
|
const now = new Date();
|
|
|
|
const utc = new Date(now.getTime() + now.getTimezoneOffset() * 60000);
|
|
const targetTime = new Date(utc.toLocaleString('en-US', { timeZone: timezone }));
|
|
const offset = (targetTime.getTime() - utc.getTime()) / (1000 * 60 * 60);
|
|
const sign = offset >= 0 ? '+' : '-';
|
|
const hours = Math.floor(Math.abs(offset));
|
|
const minutes = Math.floor((Math.abs(offset) - hours) * 60);
|
|
|
|
return `UTC${sign}${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
|
|
} catch {
|
|
return 'UTC+00:00';
|
|
}
|
|
}
|
|
|
|
export default function TimezoneSwitch() {
|
|
const locale = useLocale();
|
|
const [timezone, setTimezone] = useState<string>('UTC');
|
|
const [open, setOpen] = useState(false);
|
|
|
|
const timezoneOptions = useMemo(() => getAllTimezones(locale), [locale]);
|
|
|
|
useEffect(() => {
|
|
const savedTimezone = localStorage.getItem('timezone');
|
|
if (savedTimezone) {
|
|
setTimezone(savedTimezone);
|
|
} else {
|
|
try {
|
|
const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
setTimezone(browserTimezone);
|
|
localStorage.setItem('timezone', browserTimezone);
|
|
} catch {
|
|
setTimezone('UTC');
|
|
}
|
|
}
|
|
}, []);
|
|
|
|
const handleTimezoneChange = (newTimezone: string) => {
|
|
setTimezone(newTimezone);
|
|
localStorage.setItem('timezone', newTimezone);
|
|
setOpen(false);
|
|
|
|
window.dispatchEvent(
|
|
new CustomEvent('timezoneChanged', {
|
|
detail: { timezone: newTimezone },
|
|
}),
|
|
);
|
|
};
|
|
const serverTimezones = timezoneOptions.filter(
|
|
(option) => getServerTimezones().includes(option.value) && option.value !== timezone,
|
|
);
|
|
|
|
return (
|
|
<Popover open={open} onOpenChange={setOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button variant='ghost' size='icon' className='p-0'>
|
|
<Icon icon='flat-color-icons:overtime' className='!size-6' />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className='w-80 p-0' align='end'>
|
|
<Command>
|
|
<CommandInput placeholder='Search...' />
|
|
<CommandList>
|
|
<CommandGroup heading='Current'>
|
|
{timezoneOptions
|
|
.filter((option) => option.value === timezone)
|
|
.map((option) => (
|
|
<CommandItem
|
|
key={option.value}
|
|
value={`${option.label} ${option.value}`}
|
|
onSelect={() => handleTimezoneChange(option.value)}
|
|
className='bg-primary/10'
|
|
>
|
|
<div className='flex w-full items-center gap-3'>
|
|
<div className='flex flex-1 flex-col'>
|
|
<span className='font-medium'>{option.value}</span>
|
|
<span className='text-muted-foreground text-xs'>
|
|
{option.timezone} • {getCurrentTime(option.value)}
|
|
</span>
|
|
</div>
|
|
<Icon icon='uil:check' className='h-4 w-4 opacity-100' />
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
{serverTimezones.length > 0 && (
|
|
<CommandGroup heading='Server'>
|
|
{serverTimezones.map((option) => (
|
|
<CommandItem
|
|
key={option.value}
|
|
value={`${option.label} ${option.value}`}
|
|
onSelect={() => handleTimezoneChange(option.value)}
|
|
>
|
|
<div className='flex w-full items-center gap-3'>
|
|
<div className='flex flex-1 flex-col'>
|
|
<span className='font-medium'>{option.value}</span>
|
|
<span className='text-muted-foreground text-xs'>
|
|
{option.timezone} • {getCurrentTime(option.value)}
|
|
</span>
|
|
</div>
|
|
<Icon icon='uil:check' className='h-4 w-4 opacity-0' />
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
)}
|
|
|
|
<CommandGroup heading='Recommended'>
|
|
{timezoneOptions
|
|
.filter(
|
|
(option) =>
|
|
getRecommendedTimezones().includes(option.value) && option.value !== timezone,
|
|
)
|
|
.map((option) => (
|
|
<CommandItem
|
|
key={option.value}
|
|
value={`${option.label} ${option.value}`}
|
|
onSelect={() => handleTimezoneChange(option.value)}
|
|
>
|
|
<div className='flex w-full items-center gap-3'>
|
|
<div className='flex flex-1 flex-col'>
|
|
<span className='font-medium'>{option.value}</span>
|
|
<span className='text-muted-foreground text-xs'>
|
|
{option.timezone} • {getCurrentTime(option.value)}
|
|
</span>
|
|
</div>
|
|
<Icon icon='uil:check' className='h-4 w-4 opacity-0' />
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
|
|
<CommandGroup heading='All'>
|
|
{timezoneOptions
|
|
.filter(
|
|
(option) =>
|
|
!getServerTimezones().includes(option.value) &&
|
|
!getRecommendedTimezones().includes(option.value) &&
|
|
option.value !== timezone,
|
|
)
|
|
.map((option) => (
|
|
<CommandItem
|
|
key={option.value}
|
|
value={`${option.label} ${option.value}`}
|
|
onSelect={() => handleTimezoneChange(option.value)}
|
|
>
|
|
<div className='flex w-full items-center gap-3'>
|
|
<div className='flex flex-1 flex-col'>
|
|
<span className='font-medium'>{option.value}</span>
|
|
<span className='text-muted-foreground text-xs'>
|
|
{option.timezone} • {getCurrentTime(option.value)}
|
|
</span>
|
|
</div>
|
|
<Icon icon='uil:check' className='h-4 w-4 opacity-0' />
|
|
</div>
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
);
|
|
}
|