|
|
@@ -1,5 +1,6 @@
|
|
|
import { memo, useRef } from 'react';
|
|
|
|
|
|
+import { Form } from 'antd';
|
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
|
|
import { useResponsive } from '@/hooks/useSize';
|
|
|
@@ -11,20 +12,101 @@ import UserInfo from './components/UserInfo';
|
|
|
import { useAction } from './useAction';
|
|
|
import { usePlanCardsHeightSync } from './usePlanCardsHeightSync';
|
|
|
import { useService } from './useService';
|
|
|
+import type { Plan } from './useService';
|
|
|
+
|
|
|
+const PRICING_FORM_ID = 'pricing-form';
|
|
|
+
|
|
|
+export interface PlanSelectorProps {
|
|
|
+ value?: string;
|
|
|
+ onChange?: (planId: string) => void;
|
|
|
+ plans: Plan[];
|
|
|
+ planCardsContainerRef: React.RefObject<HTMLDivElement | null>;
|
|
|
+ isMobile: boolean;
|
|
|
+}
|
|
|
+
|
|
|
+function PlanSelector({
|
|
|
+ value,
|
|
|
+ onChange,
|
|
|
+ plans,
|
|
|
+ planCardsContainerRef,
|
|
|
+ isMobile,
|
|
|
+}: PlanSelectorProps) {
|
|
|
+ return (
|
|
|
+ <div
|
|
|
+ ref={planCardsContainerRef as React.RefObject<HTMLDivElement>}
|
|
|
+ data-count={plans.length}
|
|
|
+ className={
|
|
|
+ isMobile
|
|
|
+ ? 'flex-col-c gap-5'
|
|
|
+ : 'grid grid-cols-4 gap-x-8 gap-y-4 [&[data-count="1"]]:grid-cols-3 [&[data-count="2"]]:grid-cols-3 [&[data-count="3"]]:grid-cols-3'
|
|
|
+ }
|
|
|
+ >
|
|
|
+ {plans.map((plan) => (
|
|
|
+ <PlanCard
|
|
|
+ key={plan.id}
|
|
|
+ id={plan.id}
|
|
|
+ title={plan.title}
|
|
|
+ subTitle={plan.subTitle}
|
|
|
+ introduce={plan.introduce}
|
|
|
+ tag={plan.tag}
|
|
|
+ tagType={plan.tagType}
|
|
|
+ isSelected={value === plan.id}
|
|
|
+ onClick={() => onChange?.(plan.id)}
|
|
|
+ />
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+export interface PayMethodSelectorProps {
|
|
|
+ value?: string;
|
|
|
+ onChange?: (payType: string) => void;
|
|
|
+ payMethods: API.PayTypeItem[];
|
|
|
+ isMobile: boolean;
|
|
|
+}
|
|
|
+
|
|
|
+function PayMethodSelector({ value, onChange, payMethods, isMobile }: PayMethodSelectorProps) {
|
|
|
+ return (
|
|
|
+ <div
|
|
|
+ className={
|
|
|
+ isMobile
|
|
|
+ ? 'flex-col-c gap-4'
|
|
|
+ : 'flex flex-wrap justify-start gap-x-8 gap-y-4 [&>*]:flex-[1_1_calc(50%-1rem)] [&>*]:max-w-[calc(50%-1rem)]'
|
|
|
+ }
|
|
|
+ >
|
|
|
+ {payMethods.map((item) => (
|
|
|
+ <PayMethodCard
|
|
|
+ key={item.payType}
|
|
|
+ item={item}
|
|
|
+ isSelected={value === item.payType}
|
|
|
+ onClick={() => onChange?.(item.payType)}
|
|
|
+ />
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|
|
|
|
|
|
const Pricing = memo(() => {
|
|
|
const { t } = useTranslation();
|
|
|
const { isMobile } = useResponsive();
|
|
|
+ const [form] = Form.useForm<{ planId: string; payMethod: string }>();
|
|
|
const planCardsContainerRef = useRef<HTMLDivElement>(null);
|
|
|
- const { selectedPlanId, handlePlanClick, selectedPayMethod, handlePayMethodClick } =
|
|
|
- useAction();
|
|
|
-
|
|
|
const { plans, payMethods } = useService();
|
|
|
+ const { handlePayNow } = useAction();
|
|
|
|
|
|
- const selectedPlan = plans.find((plan) => plan.id === selectedPlanId) || null;
|
|
|
+ const planId = Form.useWatch('planId', form);
|
|
|
+ const payMethod = Form.useWatch('payMethod', form);
|
|
|
+ const selectedPlan = plans.find((p) => p.id === planId) ?? null;
|
|
|
+ const selectedPayMethod = payMethod ?? null;
|
|
|
|
|
|
usePlanCardsHeightSync(planCardsContainerRef, isMobile, plans.length);
|
|
|
|
|
|
+ const onFinish = (values: { planId: string; payMethod: string }) => {
|
|
|
+ const plan = plans.find((p) => p.id === values.planId);
|
|
|
+ if (!plan || !values.payMethod) return;
|
|
|
+ handlePayNow(plan, values.payMethod);
|
|
|
+ };
|
|
|
+
|
|
|
return (
|
|
|
<div className="flex items-start justify-center">
|
|
|
<div className={`max-w-[1440px] w-full ${isMobile ? 'px-0' : 'px-[30px]'}`}>
|
|
|
@@ -37,60 +119,61 @@ const Pricing = memo(() => {
|
|
|
{t('pages.pricing.title')}
|
|
|
</span>
|
|
|
<UserInfo />
|
|
|
- <div className="flex flex-col gap-5">
|
|
|
- <span
|
|
|
- className={`text-white font-semibold leading-[1.43] ${isMobile ? 'text-[16px]' : 'text-[22px]'}`}
|
|
|
- >
|
|
|
- {t('pages.pricing.selecPlan')}
|
|
|
- </span>
|
|
|
- <div
|
|
|
- ref={planCardsContainerRef}
|
|
|
- data-count={plans.length}
|
|
|
- className={
|
|
|
- isMobile
|
|
|
- ? 'flex-col-c gap-5'
|
|
|
- : 'grid grid-cols-4 gap-x-8 gap-y-4 [&[data-count="1"]]:grid-cols-3 [&[data-count="2"]]:grid-cols-3 [&[data-count="3"]]:grid-cols-3'
|
|
|
- }
|
|
|
- >
|
|
|
- {plans.map((plan) => (
|
|
|
- <PlanCard
|
|
|
- key={plan.id}
|
|
|
- id={plan.id}
|
|
|
- title={plan.title}
|
|
|
- subTitle={plan.subTitle}
|
|
|
- introduce={plan.introduce}
|
|
|
- tag={plan.tag}
|
|
|
- tagType={plan.tagType}
|
|
|
- isSelected={selectedPlanId === plan.id}
|
|
|
- onClick={() => handlePlanClick(plan.id)}
|
|
|
+ <Form
|
|
|
+ form={form}
|
|
|
+ id={PRICING_FORM_ID}
|
|
|
+ layout="vertical"
|
|
|
+ onFinish={onFinish}
|
|
|
+ className="flex flex-col gap-5"
|
|
|
+ >
|
|
|
+ <div className="flex flex-col gap-5">
|
|
|
+ <span
|
|
|
+ className={`text-white font-semibold leading-[1.43] ${isMobile ? 'text-[16px]' : 'text-[22px]'}`}
|
|
|
+ >
|
|
|
+ {t('pages.pricing.selecPlan')}
|
|
|
+ </span>
|
|
|
+ <Form.Item
|
|
|
+ name="planId"
|
|
|
+ rules={[
|
|
|
+ {
|
|
|
+ required: true,
|
|
|
+ message: t('pages.pricing.pleaseSelectPlan'),
|
|
|
+ },
|
|
|
+ ]}
|
|
|
+ className="mb-0 [&_.ant-form-item-explain]:mt-3"
|
|
|
+ >
|
|
|
+ <PlanSelector
|
|
|
+ plans={plans}
|
|
|
+ planCardsContainerRef={planCardsContainerRef}
|
|
|
+ isMobile={isMobile}
|
|
|
/>
|
|
|
- ))}
|
|
|
+ </Form.Item>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- <div className="flex flex-col gap-4 mt-1">
|
|
|
- <span
|
|
|
- className={`text-white font-semibold leading-[1.43] ${isMobile ? 'text-[16px]' : 'text-[22px]'}`}
|
|
|
- >
|
|
|
- {t('pages.pricing.selectPayMethod')}
|
|
|
- </span>
|
|
|
- <div
|
|
|
- className={
|
|
|
- isMobile
|
|
|
- ? 'flex-col-c gap-4'
|
|
|
- : 'flex flex-wrap justify-start gap-x-8 gap-y-4 [&>*]:flex-[1_1_calc(50%-1rem)] [&>*]:max-w-[calc(50%-1rem)]'
|
|
|
- }
|
|
|
- >
|
|
|
- {payMethods.map((item) => (
|
|
|
- <PayMethodCard
|
|
|
- key={item.payType}
|
|
|
- item={item}
|
|
|
- isSelected={selectedPayMethod === item.payType}
|
|
|
- onClick={() => handlePayMethodClick(item.payType)}
|
|
|
- />
|
|
|
- ))}
|
|
|
+ <div className="flex flex-col gap-4 mt-1">
|
|
|
+ <span
|
|
|
+ className={`text-white font-semibold leading-[1.43] ${isMobile ? 'text-[16px]' : 'text-[22px]'}`}
|
|
|
+ >
|
|
|
+ {t('pages.pricing.selectPayMethod')}
|
|
|
+ </span>
|
|
|
+ <Form.Item
|
|
|
+ name="payMethod"
|
|
|
+ rules={[
|
|
|
+ {
|
|
|
+ required: true,
|
|
|
+ message: t('pages.pricing.pleaseSelectPayMethod'),
|
|
|
+ },
|
|
|
+ ]}
|
|
|
+ className="mb-0 [&_.ant-form-item-explain]:mt-3"
|
|
|
+ >
|
|
|
+ <PayMethodSelector payMethods={payMethods} isMobile={isMobile} />
|
|
|
+ </Form.Item>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- <OrderSummary selectedPlan={selectedPlan} selectedPayMethod={selectedPayMethod}/>
|
|
|
+ <OrderSummary
|
|
|
+ formId={PRICING_FORM_ID}
|
|
|
+ selectedPlan={selectedPlan}
|
|
|
+ selectedPayMethod={selectedPayMethod}
|
|
|
+ />
|
|
|
+ </Form>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|