🐛 fix: Add localization updates and new utility functions
This commit is contained in:
@@ -27,14 +27,14 @@ import { UserStatisticsCard } from './user-statistics-card';
|
||||
export default function Statistics() {
|
||||
const t = useTranslations('index');
|
||||
|
||||
const { data: TicketTotal } = useQuery({
|
||||
const { data: TicketTotal, isLoading: ticketLoading } = useQuery({
|
||||
queryKey: ['queryTicketWaitReply'],
|
||||
queryFn: async () => {
|
||||
const { data } = await queryTicketWaitReply();
|
||||
return data.data?.count;
|
||||
},
|
||||
});
|
||||
const { data: ServerTotal } = useQuery({
|
||||
const { data: ServerTotal, isLoading: serverLoading } = useQuery({
|
||||
queryKey: ['queryServerTotalData'],
|
||||
queryFn: async () => {
|
||||
const { data } = await queryServerTotalData();
|
||||
@@ -42,6 +42,8 @@ export default function Statistics() {
|
||||
},
|
||||
});
|
||||
|
||||
const isLoading = ticketLoading || serverLoading;
|
||||
|
||||
const [dataType, setDataType] = useState<string | 'nodes' | 'users'>('nodes');
|
||||
const [timeFrame, setTimeFrame] = useState<string | 'today' | 'yesterday'>('today');
|
||||
|
||||
@@ -76,61 +78,75 @@ export default function Statistics() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='grid grid-cols-2 gap-2 md:grid-cols-4'>
|
||||
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-5'>
|
||||
{[
|
||||
{
|
||||
title: t('onlineUsersCount'),
|
||||
value: ServerTotal?.online_users || 0,
|
||||
subtitle: t('currentlyOnline'),
|
||||
icon: 'uil:users-alt',
|
||||
href: '/dashboard/servers',
|
||||
color: 'text-blue-600 dark:text-blue-400',
|
||||
iconBg: 'bg-blue-100 dark:bg-blue-900/30',
|
||||
},
|
||||
{
|
||||
title: t('onlineNodeCount'),
|
||||
value: ServerTotal?.online_servers || 0,
|
||||
title: t('totalServers'),
|
||||
value: (ServerTotal?.online_servers || 0) + (ServerTotal?.offline_servers || 0),
|
||||
subtitle: `${t('online')} ${ServerTotal?.online_servers || 0} ${t('offline')} ${ServerTotal?.offline_servers || 0}`,
|
||||
icon: 'uil:server-network',
|
||||
href: '/dashboard/servers',
|
||||
color: 'text-green-600 dark:text-green-400',
|
||||
iconBg: 'bg-green-100 dark:bg-green-900/30',
|
||||
},
|
||||
{
|
||||
title: t('offlineNodeCount'),
|
||||
value: ServerTotal?.offline_servers || 0,
|
||||
icon: 'uil:server-network-alt',
|
||||
href: '/dashboard/servers',
|
||||
title: t('todayTraffic'),
|
||||
value: formatBytes(
|
||||
(ServerTotal?.today_upload || 0) + (ServerTotal?.today_download || 0),
|
||||
),
|
||||
subtitle: `↑${formatBytes(ServerTotal?.today_upload || 0)} ↓${formatBytes(ServerTotal?.today_download || 0)}`,
|
||||
icon: 'uil:exchange-alt',
|
||||
color: 'text-purple-600 dark:text-purple-400',
|
||||
iconBg: 'bg-purple-100 dark:bg-purple-900/30',
|
||||
},
|
||||
{
|
||||
title: t('monthTraffic'),
|
||||
value: formatBytes(
|
||||
(ServerTotal?.monthly_upload || 0) + (ServerTotal?.monthly_download || 0),
|
||||
),
|
||||
subtitle: `↑${formatBytes(ServerTotal?.monthly_upload || 0)} ↓${formatBytes(ServerTotal?.monthly_download || 0)}`,
|
||||
icon: 'uil:cloud-data-connection',
|
||||
color: 'text-orange-600 dark:text-orange-400',
|
||||
iconBg: 'bg-orange-100 dark:bg-orange-900/30',
|
||||
},
|
||||
{
|
||||
title: t('pendingTickets'),
|
||||
value: TicketTotal || 0,
|
||||
subtitle: t('pending'),
|
||||
icon: 'uil:clipboard-notes',
|
||||
href: '/dashboard/ticket',
|
||||
},
|
||||
{
|
||||
title: t('todayUploadTraffic'),
|
||||
value: formatBytes(ServerTotal?.today_upload || 0),
|
||||
icon: 'uil:arrow-up',
|
||||
},
|
||||
{
|
||||
title: t('todayDownloadTraffic'),
|
||||
value: formatBytes(ServerTotal?.today_download || 0),
|
||||
icon: 'uil:arrow-down',
|
||||
},
|
||||
{
|
||||
title: t('monthUploadTraffic'),
|
||||
value: formatBytes(ServerTotal?.monthly_upload || 0),
|
||||
icon: 'uil:cloud-upload',
|
||||
},
|
||||
{
|
||||
title: t('monthDownloadTraffic'),
|
||||
value: formatBytes(ServerTotal?.monthly_download || 0),
|
||||
icon: 'uil:cloud-download',
|
||||
color: 'text-red-600 dark:text-red-400',
|
||||
iconBg: 'bg-red-100 dark:bg-red-900/30',
|
||||
},
|
||||
].map((item, index) => (
|
||||
<Link href={item.href || '#'} key={index}>
|
||||
<Card className='cursor-pointer'>
|
||||
<CardHeader className='p-4'>
|
||||
<CardTitle>{item.title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className='flex justify-between p-4 text-xl'>
|
||||
<Icon icon={item.icon} className='text-muted-foreground' />
|
||||
<div className='text-xl font-bold tabular-nums leading-none'>{item.value}</div>
|
||||
<Link
|
||||
href={item.href || '#'}
|
||||
key={index}
|
||||
className={!item.href ? 'pointer-events-none' : ''}
|
||||
>
|
||||
<Card className={`group ${item.href ? 'cursor-pointer' : ''}`}>
|
||||
<CardContent className='p-6'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex-1'>
|
||||
<p className='text-muted-foreground mb-2 text-sm font-medium'>{item.title}</p>
|
||||
<div className={`text-2xl font-bold ${item.color} mb-1`}>{item.value}</div>
|
||||
<div className={`text-muted-foreground h-4 text-xs`}>{item.subtitle}</div>
|
||||
</div>
|
||||
<div
|
||||
className={`rounded-full p-3 ${item.iconBg} transition-transform duration-300 group-hover:scale-110`}
|
||||
>
|
||||
<Icon icon={item.icon} className={`h-6 w-6 ${item.color}`} />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
@@ -192,8 +192,8 @@ export function UserStatisticsCard() {
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => {
|
||||
// value format: "YYYY-MM-DD"
|
||||
return new Date(value).toLocaleDateString(locale, {
|
||||
const [year, month, day] = value.split('-');
|
||||
return new Date(year, month - 1, day).toLocaleDateString(locale, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ import { usePathname } from 'next/navigation';
|
||||
import { Fragment, useMemo } from 'react';
|
||||
import LanguageSwitch from './language-switch';
|
||||
import ThemeSwitch from './theme-switch';
|
||||
import TimezoneSwitch from './timezone-switch';
|
||||
import { UserNav } from './user-nav';
|
||||
|
||||
export function Header() {
|
||||
@@ -48,6 +49,7 @@ export function Header() {
|
||||
</div>
|
||||
<div className='flex items-center gap-2 px-3'>
|
||||
<LanguageSwitch />
|
||||
<TimezoneSwitch />
|
||||
<ThemeSwitch />
|
||||
<UserNav />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { Command, 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 { cn } from '@workspace/ui/lib/utils';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
interface TimezoneOption {
|
||||
value: string;
|
||||
label: string;
|
||||
offset: string;
|
||||
}
|
||||
|
||||
function getAllTimezones(): TimezoneOption[] {
|
||||
try {
|
||||
const timeZones = Intl.supportedValuesOf('timeZone');
|
||||
|
||||
return [
|
||||
{
|
||||
value: 'UTC',
|
||||
label: 'UTC',
|
||||
offset: '+00:00',
|
||||
},
|
||||
].concat(
|
||||
timeZones
|
||||
.map((tz) => {
|
||||
const parts = tz.split('/');
|
||||
let label = tz;
|
||||
|
||||
if (parts.length >= 2) {
|
||||
const region = parts[0];
|
||||
const city = parts[1]?.replace(/_/g, ' ') || '';
|
||||
label = `${city} (${region})`;
|
||||
}
|
||||
|
||||
return {
|
||||
value: tz,
|
||||
label: label,
|
||||
offset: getTimezoneOffset(tz),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.label.localeCompare(b.label)),
|
||||
);
|
||||
} catch {
|
||||
return [
|
||||
{
|
||||
value: 'UTC',
|
||||
label: 'UTC',
|
||||
offset: '+00:00',
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
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 `${sign}${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
|
||||
} catch {
|
||||
return '+00:00';
|
||||
}
|
||||
}
|
||||
|
||||
export default function TimezoneSwitch() {
|
||||
const [timezone, setTimezone] = useState<string>('UTC');
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const timezoneOptions = useMemo(() => getAllTimezones(), []);
|
||||
|
||||
const handleTimezoneChange = (newTimezone: string) => {
|
||||
setTimezone(newTimezone);
|
||||
localStorage.setItem('timezone', newTimezone);
|
||||
setOpen(false);
|
||||
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('timezoneChanged', {
|
||||
detail: { timezone: newTimezone },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
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>
|
||||
{timezoneOptions.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.label}</span>
|
||||
<span className='text-muted-foreground text-xs'>
|
||||
{option.value} • {option.offset}
|
||||
</span>
|
||||
</div>
|
||||
<Icon
|
||||
icon='uil:check'
|
||||
className={cn(
|
||||
'h-4 w-4',
|
||||
timezone === option.value ? 'opacity-100' : 'opacity-0',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user