308 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import {
AffiliateDialog,
AffiliateDialogRef,
} from '@/components/affiliate/components/AffiliateDialog';
import { Display } from '@/components/display';
import { Empty } from '@/components/empty';
import SvgIcon from '@/components/SvgIcon';
import useGlobalStore from '@/config/use-global';
import { queryUserAffiliate, queryUserAffiliateList } from '@/services/user/user';
import { useQuery } from '@tanstack/react-query';
import { Button } from '@workspace/ui/components/button';
import { Card, CardContent } from '@workspace/ui/components/card';
import { Input } from '@workspace/ui/components/input';
import { formatDate } from '@workspace/ui/utils';
import { useTranslations } from 'next-intl';
import { useRef, useState } from 'react';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import { toast } from 'sonner';
export default function Affiliate() {
const t = useTranslations('affiliate');
const { user, common } = useGlobalStore();
const [sum, setSum] = useState<number>();
const { data } = useQuery({
queryKey: ['queryUserAffiliate'],
queryFn: async () => {
const response = await queryUserAffiliate();
return response.data.data;
},
});
const { data: inviteList = [] } = useQuery({
queryKey: ['queryUserAffiliateList'],
queryFn: async () => {
const response = await queryUserAffiliateList({
page: 1,
size: 3,
});
return response.data.data?.list || [];
},
});
const dialogRef = useRef<AffiliateDialogRef>(null);
return (
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2'>
<Card
className={
'rounded-[20px] border border-[#D9D9D9] p-6 shadow-[0px_0px_52.6px_1px_rgba(15,44,83,0.05)]'
}
>
<CardContent className={'p-0 text-[#666]'}>
<div className={'sm:mb-6'}>
<div className={'font-bold sm:text-xl'}>{t('totalCommission')}</div>
<div className={'text-xs font-light sm:text-[15px]'}>{t('commissionInfo')}</div>
</div>
<div className={'mb-3 text-xl font-bold text-[#091B33] sm:text-[32px]'}>
{t('historicalRecommendedUsers')}
</div>
<div className={'grid grid-cols-2 gap-[10px] sm:grid-cols-1 sm:gap-5 lg:grid-cols-2'}>
<div className='rounded-[20px] bg-[#EAEAEA] px-4 py-2 shadow-sm transition-all duration-300 hover:shadow-md sm:py-4'>
<p className='font-medium text-[#666] opacity-80 sm:mb-3 sm:text-sm'>
{t('totalCommission')}
</p>
<p className='text-xl font-bold text-[#225BA9] sm:text-2xl'>
<Display type='currency' value={data?.total_commission} />
</p>
</div>
<div className='rounded-[20px] border-2 border-[#D9D9D9] px-4 py-2 shadow-sm transition-all duration-300 hover:shadow-md sm:py-4'>
<p className='flex justify-between font-medium text-[#666] opacity-80 sm:mb-3 sm:text-sm'>
{t('commissionInviteCode')}
<CopyToClipboard
text={`${location?.origin}/?invite=${user?.refer_code}`}
onCopy={(text, result) => {
if (result) {
toast.success(t('copySuccess'));
}
}}
>
<Button
variant='link'
size='sm'
className='h-auto p-0 px-0 sm:hidden [&_svg]:size-5'
>
<SvgIcon name={'copy'}></SvgIcon>
</Button>
</CopyToClipboard>
</p>
<p className='flex justify-between text-xl font-bold text-[#225BA9] sm:text-2xl'>
<span> {user?.refer_code}</span>
<CopyToClipboard
text={`${location?.origin}/?invite=${user?.refer_code}`}
onCopy={(text, result) => {
if (result) {
toast.success(t('copySuccess'));
}
}}
>
<Button
variant='link'
size='sm'
className='hidden gap-2 p-0 sm:block [&_svg]:size-5'
>
<SvgIcon name={'copy'}></SvgIcon>
</Button>
</CopyToClipboard>
</p>
</div>
</div>
</CardContent>
</Card>
<Card className='order-2 rounded-[20px] border border-[#EAEAEA] bg-gradient-to-b from-white to-[#EAEAEA] p-6 md:order-none'>
<div className='mb-4 flex items-center justify-between'>
<h3 className='font-medium text-[#666666] sm:text-xl'>{t('inviteRecords')}</h3>
<span
className='cursor-pointer text-sm text-[#225BA9]'
onClick={() => dialogRef.current?.open()}
>
{t('more')}
</span>
</div>
<div className='space-y-2 sm:space-y-4'>
{inviteList?.length ? (
<div className='relative space-y-3'>
<div
className={
'absolute bottom-0 left-0 right-0 h-[60px] bg-white/30 backdrop-blur-[1px]'
}
></div>
{inviteList?.map((invite) => {
return (
<div className='flex flex-wrap justify-between gap-2 rounded-[20px] bg-white px-6 py-2 text-[10px] sm:text-base'>
<div>
<div className={'text-[#225BA9]'}>{t('userIdentifier')}</div>
<div className={'font-bold text-[#091B33]'}>{invite.identifier}</div>
</div>
<div>
<div className={'text-[#225BA9]'}>{t('time')}</div>
<div className={'font-bold text-[#091B33]'}>
{formatDate(invite.registered_at)}
</div>
</div>
</div>
);
})}
</div>
) : (
<Empty />
)}
</div>
</Card>
<Card className='min-w-[322px] rounded-[20px] border border-[#EAEAEA] bg-[#EAEAEA] p-6 text-[12px] sm:text-[16px] md:min-w-[496px]'>
<div className='flex items-center justify-between'>
<h3 className='text-[15px] font-medium text-[#0F2C53] sm:text-xl'>
{t('commissionCalculation')}
</h3>
</div>
<div className={'mb-4 text-[10px] font-light text-[#0F2C53] sm:text-base'}>
{t('commissionCalculationInfo')}
</div>
{(() => {
// Pro plan: $60/month, $576/year
const MONTHLY_PRICE = 60;
const YEARLY_PRICE = 576;
const [count, setCount] = useState<number>(10);
const clamp = (n: number) => Math.max(0, Math.min(10000, Math.floor(n)));
const firstMonth = count * MONTHLY_PRICE * 0.5; // 50%
const firstYear = count * YEARLY_PRICE * 0.3; // 30%
const recurMonth = count * MONTHLY_PRICE * 0.2; // 20%
const recurYear = count * YEARLY_PRICE * 0.2; // 20%
return (
<div className='space-y-4'>
{/* Calculator panel container */}
<div className='grid grid-cols-[1.5fr_2.5fr_3fr] items-stretch rounded-[34px] bg-white/10 px-5 pb-6 shadow-[inset_0_0_15.7px_0_rgba(0,0,0,0.25)]'>
{/* Left: row headers (Monthly Plan / Yearly Plan) */}
<div className='flex flex-col justify-stretch font-semibold text-[#0F2C53]'>
<div className='flex h-[56px] items-center justify-center border-b-[3px] border-white'></div>
<div className='flex h-[81px] items-center justify-center border-b-[3px] border-white'>
{t('monthlyPackage')}
</div>
<div className='flex h-[81px] items-center justify-center border-b-[3px] border-white'>
{t('annualPackage')}
</div>
</div>
{/* Middle: First-time top-up users (double rounded corners + three rows) */}
<div className='relative'>
<div
className={
'absolute left-1 top-6 z-10 h-[90%] w-full rounded-[14px] bg-[#225BA9] sm:left-2.5 sm:top-8'
}
></div>
<div className={'absolute bottom-0 z-0 h-[3px] w-full bg-white'}></div>
<div className={'absolute z-20 w-full'}>
<div className='overflow-hidden rounded-[14px] text-center text-[#0F2C53]'>
<div className={'rounded-t-[14px] bg-[#A8D4ED] px-1 sm:px-4'}>
<div className='mt-3 flex h-[44px] items-center justify-center border-b-[3px] border-white font-bold'>
{t('firstTimeTopUpUser')}
</div>
</div>
<div className={'bg-[#A8D4ED] px-1 sm:px-4'}>
<div className='grid h-[81px] grid-cols-1 grid-rows-2 border-b-[3px] border-white'>
<div className='flex h-full items-center justify-center border-b-[1px] border-white font-semibold'>
50%
</div>
<div className='flex h-full flex-1 items-center justify-center text-[10px] text-[#225BA9] sm:text-[15px]'>
up to{' '}
<span className='font-semibold'>
<Display type='currency' value={firstMonth} />
</span>
</div>
</div>
</div>
<div className={'bg-[#A8D4ED]'}>
<div className='grid h-[81px] grid-cols-1 grid-rows-2 bg-[#A8D4ED]'>
<div className='flex h-full items-center justify-center border-b-[1px] border-white font-semibold'>
30%
</div>
<div className='flex h-full items-center justify-center text-[10px] text-[#225BA9] sm:text-[15px]'>
up to{' '}
<span className='font-semibold'>
<Display type='currency' value={firstYear} />
</span>
</div>
</div>
</div>
</div>
</div>
{/* Blue shadow block */}
<div className='pointer-events-none absolute inset-0 -z-10 translate-x-2 translate-y-2 rounded-[20px] bg-[#225BA9]' />
</div>
{/* Right: Repeat top-up users (gray card + three rows) */}
<div className='text-center text-[#0F2C53]'>
<div className='flex h-[56px] items-center justify-center border-b-[3px] border-white pt-3 font-bold'>
{t('repeatTopUpUser')}
</div>
<div className='grid h-[81px] grid-cols-1 grid-rows-2 items-center justify-center border-b-[3px] border-white'>
<div className='flex h-full items-center justify-center border-b-[1px] border-white font-semibold'>
20%
</div>
<div className='flex h-full w-full items-center justify-center text-[10px] text-[#225BA9] sm:text-[15px]'>
up to{' '}
<span className='font-semibold'>
<Display type='currency' value={recurMonth} />
</span>{' '}
{t('perMonth')}
</div>
</div>
<div className='grid h-[81px] grid-cols-1 grid-rows-2 border-b-[3px] border-white'>
<div className='flex h-full items-center justify-center border-b-[1px] border-white font-semibold'>
20%
</div>
<div className='flex h-full items-center justify-center text-[10px] text-[#225BA9] sm:text-[15px]'>
up to{' '}
<span className='font-semibold'>
<Display type='currency' value={recurYear} />
</span>{' '}
{t('perYear')}
</div>
</div>
</div>
</div>
{/* User count adjustment */}
<div className='flex items-center justify-center gap-4'>
<Button
variant='secondary'
size='icon'
className='h-6 w-9 rounded-full bg-[#0F2C53] font-bold text-white hover:bg-[#225BA9] sm:h-9'
onClick={() => setCount((v) => clamp(v - 1))}
>
</Button>
<div className='inline-flex items-center gap-1 rounded-full bg-[#D9D9D9] px-3 font-medium text-[#0F2C53] shadow-[inset_0px_0px_4px_0px_rgba(0,0,0,0.25)]'>
<Input
value={count}
onChange={(e) => setCount(clamp(Number(e.target.value) || 0))}
className='h-6 border-0 p-0 text-center text-sm focus-visible:ring-0 sm:h-8'
style={{ width: `${Math.max(3, String(count).length + 1)}ch` }}
/>
<span className='text-sm'>{t('users')}</span>
</div>
<Button
variant='secondary'
size='icon'
className='h-6 w-9 rounded-full bg-[#0F2C53] font-bold text-white hover:bg-[#225BA9] sm:h-9'
onClick={() => setCount((v) => clamp(v + 1))}
>
+
</Button>
</div>
</div>
);
})()}
</Card>
<AffiliateDialog ref={dialogRef} />
</div>
);
}