🐛 fix: Add localization updates and new utility functions

This commit is contained in:
web
2025-09-04 22:30:33 -07:00
parent e4fbd5c754
commit 4da59609b4
54 changed files with 424 additions and 112 deletions
+53 -37
View File
@@ -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',
});
+2
View File
@@ -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>
+129
View File
@@ -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>
);
}