index.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import { memo, useEffect } from 'react';
  2. import { Form } from 'antd';
  3. import { useTranslation } from 'react-i18next';
  4. import { useResponsive } from '@/hooks/useSize';
  5. import OrderSummary from './components/OrderSummary';
  6. import PayMethodCard from './components/PayMethodCard';
  7. import PlanCard from './components/PlanCard';
  8. import UserInfo from './components/UserInfo';
  9. import { useAction } from './useAction';
  10. import { useService } from './useService';
  11. import type { Plan } from './useService';
  12. const PRICING_FORM_ID = 'pricing-form';
  13. export interface PlanSelectorProps {
  14. value?: string;
  15. onChange?: (planId: string) => void;
  16. plans: Plan[];
  17. isMobile: boolean;
  18. }
  19. function PlanSelector({ value, onChange, plans, isMobile }: PlanSelectorProps) {
  20. return (
  21. <div
  22. data-count={plans.length}
  23. style={{ gridAutoRows: '1fr' }}
  24. className={
  25. isMobile
  26. ? 'flex-col-c gap-5'
  27. : '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'
  28. }
  29. >
  30. {plans.map((plan) => (
  31. <PlanCard
  32. key={plan.id}
  33. id={plan.id}
  34. title={plan.title}
  35. subTitle={plan.subTitle}
  36. introduce={plan.introduce}
  37. tag={plan.tag}
  38. tagType={plan.tagType}
  39. isSelected={value === plan.id}
  40. onClick={() => onChange?.(plan.id)}
  41. />
  42. ))}
  43. </div>
  44. );
  45. }
  46. export interface PayMethodSelectorProps {
  47. value?: string;
  48. onChange?: (payType: string) => void;
  49. payMethods: API.PayTypeItem[];
  50. isMobile: boolean;
  51. }
  52. function PayMethodSelector({ value, onChange, payMethods, isMobile }: PayMethodSelectorProps) {
  53. return (
  54. <div
  55. className={
  56. isMobile
  57. ? 'flex-col-c gap-4'
  58. : 'flex flex-wrap justify-start gap-x-8 gap-y-4 [&>*]:flex-[1_1_calc(50%-1rem)] [&>*]:max-w-[calc(50%-1rem)]'
  59. }
  60. >
  61. {payMethods.map((item) => (
  62. <PayMethodCard
  63. key={item.payType}
  64. item={item}
  65. isSelected={value === item.payType}
  66. onClick={() => onChange?.(item.payType)}
  67. />
  68. ))}
  69. </div>
  70. );
  71. }
  72. const Pricing = memo(() => {
  73. const { t } = useTranslation();
  74. const { isMobile } = useResponsive();
  75. const [form] = Form.useForm<{ planId: string; payMethod: string }>();
  76. const { plans, payMethods } = useService();
  77. const { handlePayNow } = useAction();
  78. const planId = Form.useWatch('planId', form);
  79. const payMethod = Form.useWatch('payMethod', form);
  80. const selectedPlan = plans.find((p) => p.id === planId) ?? null;
  81. const selectedPayMethod = payMethod ?? null;
  82. useEffect(() => {
  83. if (plans.length === 0) return;
  84. const defaultPlan = plans.find((p) => p.isDefault);
  85. if (defaultPlan) {
  86. form.setFieldsValue({ planId: defaultPlan.id });
  87. }
  88. }, [plans, form]);
  89. const onFinish = (values: { planId: string; payMethod: string }) => {
  90. const plan = plans.find((p) => p.id === values.planId);
  91. if (!plan || !values.payMethod) return;
  92. handlePayNow(plan, values.payMethod);
  93. };
  94. return (
  95. <div className="flex items-start justify-center">
  96. <div className={`max-w-[1440px] w-full ${isMobile ? 'px-0' : 'px-[30px]'}`}>
  97. <div
  98. className={`bg-[#0F1116] px-5 sm:px-5 lg:px-[100px] py-[30px] flex flex-col ${isMobile ? 'gap-5 mt-0 pb-[30px]' : 'gap-10 my-[50px] rounded-[12px] pb-[100px]'}`}
  99. >
  100. <span
  101. className={`text-white font-semibold leading-[1.43] text-center uppercase ${isMobile ? 'text-[22px]' : 'text-[35px]'}`}
  102. >
  103. {t('pages.pricing.title')}
  104. </span>
  105. <UserInfo />
  106. <Form
  107. form={form}
  108. id={PRICING_FORM_ID}
  109. layout="vertical"
  110. onFinish={onFinish}
  111. className="flex flex-col gap-5"
  112. >
  113. <div className="flex flex-col gap-5">
  114. <span
  115. className={`text-white font-semibold leading-[1.43] ${isMobile ? 'text-[16px]' : 'text-[22px]'}`}
  116. >
  117. {t('pages.pricing.selecPlan')}
  118. </span>
  119. <Form.Item
  120. name="planId"
  121. rules={[
  122. {
  123. required: true,
  124. message: t('pages.pricing.pleaseSelectPlan'),
  125. },
  126. ]}
  127. className="mb-0 [&_.ant-form-item-explain]:mt-3"
  128. >
  129. <PlanSelector plans={plans} isMobile={isMobile} />
  130. </Form.Item>
  131. </div>
  132. <div className="flex flex-col gap-4 mt-1">
  133. <span
  134. className={`text-white font-semibold leading-[1.43] ${isMobile ? 'text-[16px]' : 'text-[22px]'}`}
  135. >
  136. {t('pages.pricing.selectPayMethod')}
  137. </span>
  138. <Form.Item
  139. name="payMethod"
  140. rules={[
  141. {
  142. required: true,
  143. message: t('pages.pricing.pleaseSelectPayMethod'),
  144. },
  145. ]}
  146. className="mb-0 [&_.ant-form-item-explain]:mt-3"
  147. >
  148. <PayMethodSelector payMethods={payMethods} isMobile={isMobile} />
  149. </Form.Item>
  150. </div>
  151. <OrderSummary
  152. formId={PRICING_FORM_ID}
  153. selectedPlan={selectedPlan}
  154. selectedPayMethod={selectedPayMethod}
  155. />
  156. </Form>
  157. </div>
  158. </div>
  159. </div>
  160. );
  161. });
  162. Pricing.displayName = 'Pricing';
  163. export default Pricing;