270 lines
9.4 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 { SubscribeBilling } from '@/components/subscribe/billing';
import CouponInput from '@/components/subscribe/coupon-input';
import { SubscribeDetail } from '@/components/subscribe/detail';
import DurationSelector from '@/components/subscribe/duration-selector';
import PaymentMethods from '@/components/subscribe/payment-methods';
import useGlobalStore from '@/config/use-global';
import { prePurchaseOrder, purchase } from '@/services/user/portal';
import { useQuery } from '@tanstack/react-query';
import { Button } from '@workspace/ui/components/button';
import { Card, CardContent, CardHeader } from '@workspace/ui/components/card';
import { Separator } from '@workspace/ui/components/separator';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { Icon } from '@workspace/ui/custom-components/icon';
import { cn } from '@workspace/ui/lib/utils';
import { LoaderCircle } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useRouter } from 'next/navigation';
import { useCallback, useEffect, useState, useTransition } from 'react';
export default function Content({ subscription }: { subscription?: API.Subscribe }) {
const t = useTranslations('subscribe');
const { common } = useGlobalStore();
const router = useRouter();
const [params, setParams] = useState<API.PortalPurchaseRequest>({
quantity: 1,
subscribe_id: 0,
payment: -1,
coupon: '',
auth_type: 'email',
identifier: '',
password: '',
});
const [loading, startTransition] = useTransition();
const [isEmailValid, setIsEmailValid] = useState({
valid: false,
message: '',
});
const { data: order } = useQuery({
enabled: !!subscription?.id && !!params.payment,
queryKey: ['preCreateOrder', params.coupon, params.quantity, params.payment],
queryFn: async () => {
const { data } = await prePurchaseOrder({
...params,
subscribe_id: subscription?.id as number,
} as API.PrePurchaseOrderRequest);
return data.data;
},
});
useEffect(() => {
if (subscription) {
setParams((prev) => ({
...prev,
quantity: 1,
subscribe_id: subscription?.id,
}));
}
}, [subscription]);
const handleChange = useCallback((field: keyof typeof params, value: string | number) => {
setParams((prev) => ({
...prev,
[field]: value,
}));
}, []);
const handleSubmit = useCallback(async () => {
startTransition(async () => {
try {
const { data } = await purchase(params);
const { order_no } = data.data!;
if (order_no) {
localStorage.setItem(
order_no,
JSON.stringify({
auth_type: params.auth_type,
identifier: params.identifier,
}),
);
router.push(`/purchasing/order?order_no=${order_no}`);
}
} catch (error) {
console.log(error);
}
});
}, [params, router]);
if (!subscription) {
return <div className='p-6 text-center'>{t('subscriptionNotFound')}</div>;
}
return (
<div className='mx-auto mt-8 flex max-w-4xl flex-col gap-8 md:grid md:grid-cols-2 md:flex-row'>
<div className='flex flex-col gap-6'>
<Card>
<CardHeader> {common.site.site_name} </CardHeader>
<CardContent className='flex flex-col gap-2'>
<div className='flex flex-col gap-2'>
<EnhancedInput
className={cn({
'border-destructive': !isEmailValid.valid && params.identifier !== '',
})}
placeholder='Email'
type='email'
value={params.identifier || ''}
onValueChange={(value) => {
const email = value as string;
setParams((prev) => ({
...prev,
identifier: email,
}));
const reg = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!reg.test(email)) {
setIsEmailValid({
valid: false,
message: '请输入有效的邮箱地址',
});
} else if (common.auth.email.enable_domain_suffix) {
const domain = email.split('@')[1];
const isValid = common.auth.email?.domain_suffix_list
.split('\n')
.includes(domain || '');
if (!isValid) {
setIsEmailValid({
valid: false,
message: '邮箱域名不在白名单中',
});
return;
}
} else {
setIsEmailValid({
valid: true,
message: '',
});
}
}}
required
/>
<p
className={cn('text-muted-foreground text-xs', {
'text-destructive': !isEmailValid.valid && params.identifier !== '',
})}
>
{isEmailValid.message || '请填写您的电子邮件地址。'}
</p>
</div>
{params.identifier && isEmailValid.valid && (
<div className='flex flex-col gap-2'>
<EnhancedInput
placeholder='Password'
type='password'
value={params.password || ''}
onValueChange={(value) => handleChange('password', value)}
/>
<p className='text-muted-foreground text-xs'>
</p>
</div>
)}
{/* <div>
<OAuthMethods />
</div> */}
</CardContent>
</Card>
<Card>
<CardContent className='grid gap-3 p-6 text-sm'>
<h2 className='text-xl font-semibold'>{subscription.name}</h2>
<ul className='flex flex-grow flex-col gap-3'>
{(() => {
let parsedDescription;
try {
parsedDescription = JSON.parse(subscription.description);
} catch {
parsedDescription = { description: '', features: [] };
}
const { description, features } = parsedDescription;
return (
<>
{description && <li className='text-muted-foreground'>{description}</li>}
{features?.map(
(
feature: {
icon: string;
label: string;
type: 'default' | 'success' | 'destructive';
},
index: number,
) => (
<li
className={cn('flex items-center gap-1', {
'text-muted-foreground line-through': feature.type === 'destructive',
})}
key={index}
>
{feature.icon && (
<Icon
icon={feature.icon}
className={cn('text-primary size-5', {
'text-green-500': feature.type === 'success',
'text-destructive': feature.type === 'destructive',
})}
/>
)}
{feature.label}
</li>
),
)}
</>
);
})()}
</ul>
<SubscribeDetail
subscribe={{
...subscription,
quantity: params.quantity,
}}
/>
<Separator />
<SubscribeBilling
order={{
...order,
quantity: params.quantity,
unit_price: subscription?.unit_price,
}}
/>
</CardContent>
</Card>
</div>
<div className='flex flex-col gap-6'>
<Card>
<CardContent className='p-6'>
<div className='grid gap-6'>
<DurationSelector
quantity={params.quantity!}
unitTime={subscription?.unit_time}
discounts={subscription?.discount}
onChange={(value) => handleChange('quantity', value)}
/>
<CouponInput
coupon={params.coupon}
onChange={(value) => handleChange('coupon', value)}
/>
<PaymentMethods
balance={false}
value={params.payment!}
onChange={(value) => handleChange('payment', value)}
/>
</div>
</CardContent>
</Card>
<Button
className='w-full'
size='lg'
disabled={!isEmailValid.valid || loading}
onClick={handleSubmit}
>
{loading && <LoaderCircle className='mr-2 animate-spin' />}
{t('buyNow')}
</Button>
</div>
</div>
);
}