ShippingAddressCheckout.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. "use client";
  2. import {useMemo,useEffect, useState} from "react";
  3. import { useQuery } from "@apollo/client/react";
  4. import {
  5. useFormContext,
  6. useWatch,
  7. Controller
  8. } from "react-hook-form";
  9. import {
  10. formatFinalPhoneValue,
  11. getPhoneCodeBycountryCode
  12. } from "@/components/theme/ui/PhoneNumberInput/phoneCodeMetaData";
  13. import { GET_COUNTRY_STATES } from "@/graphql";
  14. import { EMAIL_REGEX, IS_VALID_INPUT, IS_VALID_FULL_PHONE, IS_VALID_PHONECODE } from "@utils/constants";
  15. import { Country } from "@/types/checkout/type";
  16. import InputText from "@/components/theme/ui/InputText";
  17. import Select from "@/components/theme/ui/Select";
  18. import PhoneNumberInput from "@/components/theme/ui/PhoneNumberInput/PhoneNumberInput";
  19. import { LoadingSpinner } from "@components/common/LoadingSpinner";
  20. export default function ShippingAddressCheckout ({
  21. countries,
  22. }:{
  23. countries: Country[];
  24. }) {
  25. const {
  26. getValues,
  27. setValue,
  28. register,
  29. formState: { errors },
  30. control
  31. } = useFormContext() // retrieve all hook methods
  32. const countriesOptions = useMemo(() => {
  33. return countries.map((country) => ({
  34. value: country.code,
  35. label: country.name,
  36. id: String(country._id),
  37. }))
  38. },[countries]);
  39. const selectedCountryCode = useWatch({
  40. control,
  41. name: 'shippingCountry',
  42. });
  43. const selectedCountry = useMemo(() => {
  44. return countries.find( (country) => country.code === selectedCountryCode)
  45. }, [countries, selectedCountryCode]);
  46. const { data: statesData, loading: statesLoading } = useQuery(GET_COUNTRY_STATES, {
  47. variables: {
  48. countryId: selectedCountry?._id
  49. },
  50. skip: !selectedCountryCode,
  51. });
  52. let statesOptions: { value: string; label: string, id: string }[] = [];
  53. if(statesData) {
  54. statesOptions = statesData.countryStates.map((state) => ({
  55. value: state.code,
  56. label: state.defaultName,
  57. id: String(state._id),
  58. }));
  59. }
  60. // 是否用户手动选择过区号
  61. const [hasUserSelectedPhoneCode, setHasUserSelectedPhoneCode] = useState(false);
  62. // 国家变更后 state字段的值需要清空
  63. // 选择国家时同步电话区号
  64. useEffect(() => {
  65. const preShippingPhoneNum = getValues('shippingPhoneNumber');
  66. const regPhoneCode = IS_VALID_PHONECODE;
  67. if(preShippingPhoneNum && !hasUserSelectedPhoneCode) {
  68. const newPhoneCode = getPhoneCodeBycountryCode(selectedCountryCode);
  69. const codePart = regPhoneCode.exec(preShippingPhoneNum);
  70. let phone = preShippingPhoneNum;
  71. if(codePart) {
  72. phone = preShippingPhoneNum.replace(codePart[0], '');
  73. }
  74. setValue('shippingPhoneNumber',formatFinalPhoneValue(newPhoneCode, phone))
  75. }
  76. },[selectedCountryCode,hasUserSelectedPhoneCode,getValues,setValue]);
  77. return (
  78. <div className="box-border w-full">
  79. <div className="w-full">
  80. <label className="text-ly-12 block mb-4 font-semibold">
  81. Email *
  82. </label>
  83. <InputText
  84. type="email"
  85. placeholder="Enter your email address"
  86. {...register("shippingEmail", {
  87. required: "Email is required",
  88. pattern: {
  89. value: EMAIL_REGEX,
  90. message: "Please enter a valid email address",
  91. }
  92. })}
  93. error={errors.shippingEmail && errors.shippingEmail.message as string}
  94. />
  95. </div>
  96. <div className="w-full mt-4">
  97. <label className="text-ly-12 block mb-4 font-semibold">
  98. Name *
  99. </label>
  100. <div className="w-full flex justify-between">
  101. <div className="w-41.25 flex-initial">
  102. <InputText
  103. type="text"
  104. placeholder="First name"
  105. {...register("shippingFirstName", {
  106. required: "First name is required",
  107. pattern: {
  108. value: IS_VALID_INPUT,
  109. message: "Invalid First Name",
  110. }
  111. })}
  112. error={errors.shippingFirstName && errors.shippingFirstName.message as string}
  113. />
  114. </div>
  115. <div className="w-41.25 flex-initial">
  116. <InputText
  117. type="text"
  118. placeholder="Last name"
  119. {...register("shippingLastName", {
  120. required: "Last name is required",
  121. pattern: {
  122. value: IS_VALID_INPUT,
  123. message: "Invalid Last Name",
  124. }
  125. })}
  126. error={errors.shippingLastName && errors.shippingLastName.message as string}
  127. />
  128. </div>
  129. </div>
  130. </div>
  131. <div className="w-full mt-4">
  132. <label className="text-ly-12 block mb-4 font-semibold">Address *</label>
  133. <InputText
  134. type="text"
  135. placeholder="Please Input Your Detailed Address With Apt. No"
  136. {...register("shippingAddress", {
  137. required: "Address is required",
  138. })}
  139. error={errors.shippingAddress && errors.shippingAddress.message as string}
  140. />
  141. </div>
  142. <div className="w-full mt-4">
  143. <label className="text-ly-12 block mb-4 font-semibold">Country and State/Province *</label>
  144. <div className="w-full">
  145. <div className="w-full flex justify-between">
  146. <div className="w-41.25 flex-initial">
  147. <Select placeholder="Country/Region"
  148. {...register("shippingCountry", {
  149. required: "Country/Region field is required",
  150. onChange: () => {
  151. // 清空 state/city
  152. setValue('shippingState','',{
  153. shouldDirty: true,
  154. shouldValidate: true,
  155. });
  156. setValue('shippingCity','',{
  157. shouldDirty: true,
  158. shouldValidate: true,
  159. });
  160. }
  161. })}
  162. options={countriesOptions}
  163. error={errors.shippingCountry && errors.shippingCountry.message as string}
  164. />
  165. </div>
  166. <div className="w-41.25 flex-initial relative">
  167. {statesLoading && <LoadingSpinner className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />}
  168. {statesOptions.length > 0 ?
  169. (
  170. <Select placeholder="State/Province"
  171. {...register("shippingState", {
  172. required: "State/Province field is required"
  173. })}
  174. options={statesOptions}
  175. error={errors.shippingState && errors.shippingState.message as string}
  176. />
  177. )
  178. : (
  179. <InputText
  180. type="text"
  181. placeholder="State/Province"
  182. {...register("shippingState", {
  183. required: "State/Province field is required"
  184. })}
  185. error={errors.shippingState && errors.shippingState.message as string}
  186. />
  187. )}
  188. </div>
  189. </div>
  190. <div className="w-full flex justify-between mt-4">
  191. <div className="w-41.25 flex-initial">
  192. <InputText
  193. type="text"
  194. placeholder="Zip/Postal Code"
  195. {...register("shippingPostcode", {
  196. required: "Postcode field is required",
  197. pattern: {
  198. value: IS_VALID_INPUT,
  199. message: "Invalid Postcode",
  200. },
  201. })}
  202. error={errors.shippingPostcode&& errors.shippingPostcode.message as string}
  203. />
  204. </div>
  205. <div className="w-41.25 flex-initial">
  206. <InputText
  207. type="text"
  208. placeholder="City"
  209. {...register("shippingCity", {
  210. required: "City field is required",
  211. pattern: {
  212. value: IS_VALID_INPUT,
  213. message: "Invalid City",
  214. },
  215. })}
  216. error={errors.shippingCity && errors.shippingCity.message as string}
  217. />
  218. </div>
  219. </div>
  220. </div>
  221. </div>
  222. <div className="w-full mt-4">
  223. <label className="text-ly-12 block mb-4 font-semibold">Phone number *</label>
  224. <Controller
  225. name="shippingPhoneNumber"
  226. control={control}
  227. rules={{
  228. required: "Phone Number is required",
  229. pattern: {
  230. value: IS_VALID_FULL_PHONE,
  231. message: "Invalid Phone Number",
  232. }
  233. }}
  234. render={({ field, fieldState }) => {
  235. return (
  236. <PhoneNumberInput
  237. placeholder="Please Input Your Phone Number"
  238. value={field.value}
  239. onChange={field.onChange}
  240. onBlur={field.onBlur}
  241. name={field.name}
  242. ref={field.ref}
  243. error={fieldState.error?.message}
  244. countryCode={selectedCountryCode}
  245. hasUserSelectedPhoneCode={hasUserSelectedPhoneCode}
  246. onHasUserSelectedPhoneCode={() => setHasUserSelectedPhoneCode(true)}
  247. />
  248. );
  249. }}
  250. />
  251. </div>
  252. </div>
  253. );
  254. };