index.tsx 7.0 KB

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