287 lines
12 KiB
TypeScript

'use client';
import { Display } from '@/components/display';
import { queryApplicationConfig } from '@/services/user/subscribe';
import { queryUserSubscribe } from '@/services/user/user';
import { Icon } from '@iconify/react';
import { useQuery } from '@tanstack/react-query';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@workspace/ui/components/accordion';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@workspace/ui/components/alert-dialog';
import { Button } from '@workspace/ui/components/button';
import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/components/card';
import { Separator } from '@workspace/ui/components/separator';
import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
import { getNextResetDate, isBrowser } from '@workspace/ui/utils';
import { differenceInDays } from 'date-fns';
import { useTranslations } from 'next-intl';
import Image from 'next/image';
import Link from 'next/link';
import { QRCodeCanvas } from 'qrcode.react';
import { useState } from 'react';
import { toast } from 'sonner';
import useGlobalStore from '@/config/use-global';
import { getStat } from '@/services/common/common';
import { getPlatform } from '@/utils/common';
import CopyToClipboard from 'react-copy-to-clipboard';
import Renewal from '../order/renewal';
import ResetTraffic from '../order/reset-traffic';
import Subscribe from '../subscribe/page';
export default function Content() {
const t = useTranslations('dashboard');
const { getUserSubscribe, getAppSubLink } = useGlobalStore();
const [protocol, setProtocol] = useState('');
const { data: userSubscribe = [] } = useQuery({
queryKey: ['queryUserSubscribe'],
queryFn: async () => {
const { data } = await queryUserSubscribe();
return data.data?.list || [];
},
});
const { data: application } = useQuery({
queryKey: ['queryApplicationConfig'],
queryFn: async () => {
const { data } = await queryApplicationConfig();
return data.data as API.ApplicationResponse;
},
});
const [platform, setPlatform] = useState<keyof API.ApplicationResponse>(getPlatform());
const { data } = useQuery({
queryKey: ['getStat'],
queryFn: async () => {
const { data } = await getStat({
skipErrorHandler: true,
});
return data.data;
},
refetchOnWindowFocus: false,
});
return (
<>
{userSubscribe.length ? (
<>
<h2 className='flex items-center gap-1.5 font-semibold'>
<Icon icon='uil:servers' className='size-5' />
{t('mySubscriptions')}
</h2>
<div className='flex flex-wrap justify-between gap-4'>
<Tabs
value={platform}
onValueChange={(value) => setPlatform(value as keyof API.ApplicationResponse)}
className='w-full max-w-full md:w-auto'
>
<TabsList className='flex *:flex-auto'>
{application &&
Object.keys(application)?.map((item) => (
<TabsTrigger value={item} key={item} className='px-1 uppercase lg:px-3'>
{item}
</TabsTrigger>
))}
</TabsList>
</Tabs>
{data?.protocol && data?.protocol.length > 1 && (
<Tabs
value={protocol}
onValueChange={setProtocol}
className='w-full max-w-full md:w-auto'
>
<TabsList className='flex *:flex-auto'>
{['all', ...(data?.protocol || [])].map((item) => (
<TabsTrigger
value={item === 'all' ? '' : item}
key={item}
className='px-1 uppercase lg:px-3'
>
{item}
</TabsTrigger>
))}
</TabsList>
</Tabs>
)}
</div>
{userSubscribe.map((item) => (
<Card key={item.id}>
<CardHeader className='flex flex-row flex-wrap items-center justify-between gap-2 space-y-0'>
<CardTitle className='font-medium'>{item.subscribe.name}</CardTitle>
<div className='flex gap-2'>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button size='sm' variant='destructive'>
{t('resetSubscription')}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t('prompt')}</AlertDialogTitle>
<AlertDialogDescription>
{t('confirmResetSubscription')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
<AlertDialogAction onClick={() => toast.success(t('resetSuccess'))}>
{t('confirm')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<ResetTraffic
id={item.subscribe_id}
token={item.token}
replacement={item.subscribe.replacement}
/>
<Renewal token={item.token} subscribe={item.subscribe} />
</div>
</CardHeader>
<CardContent>
<ul className='grid grid-cols-2 gap-3 *:flex *:flex-col *:justify-between lg:grid-cols-4'>
<li>
<span className='text-muted-foreground'>{t('used')}</span>
<span className='text-2xl font-bold'>
<Display
type='traffic'
value={item.upload + item.download}
unlimited={!item.traffic}
/>
</span>
</li>
<li>
<span className='text-muted-foreground'>{t('totalTraffic')}</span>
<span className='text-2xl font-bold'>
<Display type='traffic' value={item.traffic} unlimited={!item.traffic} />
</span>
</li>
<li>
<span className='text-muted-foreground'>{t('nextResetDays')}</span>
<span className='text-2xl font-semibold'>
{differenceInDays(getNextResetDate(item.start_time), new Date()) ||
t('unknown')}
</span>
</li>
<li>
<span className='text-muted-foreground'>{t('expirationDays')}</span>
<span className='text-2xl font-semibold'>
{differenceInDays(new Date(item.expire_time), new Date()) || t('unknown')}
</span>
</li>
</ul>
<Separator className='mt-4' />
<Accordion type='single' collapsible defaultValue='0' className='w-full'>
{getUserSubscribe(item.token, protocol)?.map((url, index) => (
<AccordionItem key={url} value={String(index)}>
<AccordionTrigger className='hover:no-underline'>
<div className='flex w-full flex-row items-center justify-between'>
<CardTitle className='text-sm font-medium'>
{t('subscriptionUrl')} {index + 1}
</CardTitle>
<CopyToClipboard
text={url}
onCopy={(text, result) => {
if (result) {
toast.success(t('copySuccess'));
}
}}
>
<span
className='text-primary hover:bg-accent mr-4 flex cursor-pointer rounded p-2 text-sm'
onClick={(e) => {
e.stopPropagation();
}}
>
<Icon icon='uil:copy' className='mr-2 size-5' />
{t('copy')}
</span>
</CopyToClipboard>
</div>
</AccordionTrigger>
<AccordionContent>
<div className='grid grid-cols-3 gap-4 lg:grid-cols-4 xl:grid-cols-7'>
{application &&
application[platform]?.map((app) => (
<div
key={app.name}
className='text-muted-foreground flex size-full flex-col items-center justify-between gap-2 text-xs'
>
<span>{app.name}</span>
{app.icon && (
<Image src={app.icon} alt={app.name} width={50} height={50} />
)}
<div className='flex'>
<Button size='sm' variant='secondary' className='px-1.5' asChild>
<Link href={app.url!}>{t('download')}</Link>
</Button>
<CopyToClipboard
text={url}
onCopy={(text, result) => {
const href = getAppSubLink(app.subscribe_type, url);
if (isBrowser() && href) {
window.location.href = href;
} else if (result) {
toast.success(
<>
<p>{t('copySuccess')}</p>
<p>{t('manualImportMessage')}</p>
</>,
);
}
}}
>
<Button size='sm' className='p-2'>
{t('import')}
</Button>
</CopyToClipboard>
</div>
</div>
))}
<div className='text-muted-foreground hidden size-full flex-col items-center justify-between gap-2 text-sm lg:flex'>
<span>{t('qrCode')}</span>
<QRCodeCanvas
value={url}
size={80}
bgColor='transparent'
fgColor='rgb(59, 130, 246)'
/>
<span className='text-center'>{t('scanToSubscribe')}</span>
</div>
</div>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</CardContent>
</Card>
))}
</>
) : (
<>
<h2 className='flex items-center gap-1.5 font-semibold'>
<Icon icon='uil:shop' className='size-5' />
{t('purchaseSubscription')}
</h2>
<Subscribe />
</>
)}
</>
);
}