| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448 |
- "use client";
- import {useMemo,useEffect, useState, useCallback} from "react";
- import clsx from "clsx";
- import { useQuery } from "@apollo/client/react";
- import {
- useFormContext,
- useWatch,
- Controller
- } from "react-hook-form";
- import {
- formatFinalPhoneValue,
- getPhoneCodeBycountryCode
- } from "@/components/theme/ui/PhoneNumberInput/phoneCodeMetaData";
- import { GET_COUNTRY_STATES } from "@/graphql";
- import { EMAIL_REGEX, IS_VALID_INPUT, IS_VALID_FULL_PHONE, IS_VALID_PHONECODE } from "@utils/constants";
- import { CustomerAddressItem } from "@/types/customer/type";
- import { useConfig } from "@utils/hooks/useConfig";
- import InputText from "@/components/theme/ui/InputText";
- import Select from "@/components/theme/ui/Select";
- import PhoneNumberInput from "@/components/theme/ui/PhoneNumberInput/PhoneNumberInput";
- import { LoadingSpinner } from "@components/common/LoadingSpinner";
- import {LyDropDown} from "@/components/theme/ui/LyDropDown";
- export default function ShippingAddressCheckout({
- noAddress,
- isGuest,
- showFormField,
- onShowFormFieldChange,
- customerAddressList,
- onEmailChange
- }: {
- noAddress: boolean;
- isGuest: boolean;
- showFormField: boolean;
- onShowFormFieldChange: (e: boolean) => void;
- customerAddressList: CustomerAddressItem[];
- onEmailChange: (value: string) => void;
- }) {
- const {countries} = useConfig();
- const {
- getValues,
- setValue,
- register,
- formState: { errors },
- control,
- } = useFormContext() // retrieve all hook methods
-
-
- const countriesOptions = useMemo(() => {
- return countries.map((country) => ({
- value: country.code,
- label: country.name,
- id: String(country._id),
- }))
- },[countries]);
- const [
- selectedCountryCode,
- firstName,
- lastName,
- streetAddress,
- stateProvince,
- postCode,
- city,
- phoneNumber
- ] = useWatch({
- control,
- name: [
- 'shippingCountry',
- 'shippingFirstName',
- 'shippingLastName',
- 'shippingAddress',
- 'shippingState',
- 'shippingPostcode',
- 'shippingCity',
- 'shippingPhoneNumber'
- ]
- });
- const selectedCountry = useMemo(() => {
- return countries.find( (country) => country.code === selectedCountryCode)
- }, [countries, selectedCountryCode]);
- const { data: statesData, loading: statesLoading } = useQuery(GET_COUNTRY_STATES, {
- variables: {
- countryId: selectedCountry?._id
- },
- skip: !selectedCountryCode,
- });
- let statesOptions: { value: string; label: string, id: string }[] = [];
- if(statesData) {
- statesOptions = statesData.countryStates.map((state) => ({
- value: state.code,
- label: state.defaultName,
- id: String(state._id),
- }));
- }
- const [showDropDown, setShowDropDown] = useState(false);
- // 是否用户手动选择过区号
- const [hasUserSelectedPhoneCode, setHasUserSelectedPhoneCode] = useState(false);
- // 国家变更后 state字段的值需要清空
- // 选择国家时同步电话区号
- useEffect(() => {
- const preShippingPhoneNum = getValues('shippingPhoneNumber');
- const regPhoneCode = IS_VALID_PHONECODE;
-
- if(preShippingPhoneNum && !hasUserSelectedPhoneCode) {
- const newPhoneCode = getPhoneCodeBycountryCode(selectedCountryCode);
- const codePart = regPhoneCode.exec(preShippingPhoneNum);
- let phone = preShippingPhoneNum;
- if(codePart) {
- phone = preShippingPhoneNum.replace(codePart[0], '');
- }
- if(codePart && newPhoneCode !== codePart[0].trim()) {
- return;
- }
- setValue('shippingPhoneNumber',formatFinalPhoneValue(newPhoneCode, phone))
- }
-
- },[selectedCountryCode,hasUserSelectedPhoneCode,getValues,setValue]);
- const showMyAddressList = () => {
- setShowDropDown(true);
- };
- const closeMyaddressList = useCallback(() => {
- setShowDropDown(false);
- },[]);
- // 从地址列表选择地址
- const selectAddress = (param: CustomerAddressItem | 'edit') => {
- if(param === 'edit') {
- if(!showFormField) onShowFormFieldChange(true);
- } else {
- setValue('shippingFirstName',param.firstName);
- setValue('shippingLastName',param.lastName);
- setValue('shippingAddress',param.address);
- setValue('shippingCountry',param.country);
- setValue('shippingState',param.state);
- setValue('shippingPostcode',param.postcode);
- setValue('shippingCity',param.city);
- setValue('shippingPhoneNumber',param.phone);
- }
- closeMyaddressList();
- };
- /**
- * 游客:
- * 1. 购物车没地址 -- 进来显示填写新地址,地址表单;填完地址,保存,关闭弹窗,购物车地址更新;再次进来显示填写的地址,隐藏表单;如果想修改,就点击修改,显示地址表单;
- * 2. 购物车有地址 -- 进来显示已有的地址,隐藏表单;如果想修改,就点击修改,显示地址表单;
- *
- * 登录用户:
- * 1. 购物车没地址 -- 进来显示填写新地址,地址表单;
- * 如果想选已有地址,点击箭头,弹出地址列表,选择地址,选完地址更新到表单,关闭地址列表(打上选中状态);如果地址列表没数据,显示没有地址供选择;
- * 如果不想选地址,就填写地址表单。
- * 保存地址,关闭弹窗;
- * 再次进来,显示购物车中的地址;
- *
- * 2. 购物车有地址 -- 进来显示购物车中的地址;
- * 如果想修改地址,点击箭头,出现地址列表
- *
- * 地址列表数据直接在 父组件获取
- */
- return (<>
-
- <div className="w-full">
- {noAddress ?
- <div onClick={showMyAddressList}
- className="box-border w-full h-9 justify-between flex items-center px-4 relative bg-[url(/image/address-bg.webp)] bg-position-[0_-70%] bg-size-[100%_auto]"
- >
- <span className="text-ly-12">+New Shipping Address</span>
- {!isGuest &&
- <button className="flex-none" title="change address" type="button">
- <svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="16" y="0" width="16" height="16" transform="rotate(90 16 0)" fill="#FFFFFF" fillOpacity="0"></rect><path stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="square" d="M11.7295 6.32028L7.62964 10.4201L3.52978 6.32028"></path></svg>
- </button>
- }
- </div>
- :
- <div onClick={showMyAddressList}
- className="w-full box-border px-3.75 bg-[url(/image/address-bg.webp)] bg-position-[0_-84%] bg-size-[100%_auto]"
- >
- <div className="border-b-1 flex h-11.25 items-center justify-between">
- <p className="text-ly-13 font-medium">
- {firstName} {lastName}
- <span className="border-r border-ly-gray h-3.5 mx-2"></span>
- {phoneNumber}
- </p>
- {!isGuest &&
- <svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="16" y="0" width="16" height="16" transform="rotate(90 16 0)" fill="#FFFFFF" fillOpacity="0"></rect><path stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="square" d="M11.7295 6.32028L7.62964 10.4201L3.52978 6.32028"></path></svg>
- }
- </div>
- <div className="flex items-center h-10">
- <svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" fill="none">
- <path stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="round" d="M6.01652 15.917C4.48522 16.3764 3.53809 17.0111 3.53809 17.712C3.53809 19.1141 7.32661 20.2507 12 20.2507C16.6734 20.2507 20.4619 19.1141 20.4619 17.712C20.4619 17.0111 19.5148 16.3764 17.9835 15.917"></path>
- <path d="M12.0002 16.8657C12.0002 16.8657 17.5005 13.271 17.5005 9.11522C17.5005 6.15182 15.0379 3.74951 12.0002 3.74951C8.96254 3.74951 6.5 6.15182 6.5 9.11522C6.5 13.271 12.0002 16.8657 12.0002 16.8657Z" stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" ></path>
- <path d="M11.9983 11.3653C13.1666 11.3653 14.1138 10.4181 14.1138 9.24979C14.1138 8.08144 13.1666 7.13428 11.9983 7.13428C10.83 7.13428 9.88281 8.08144 9.88281 9.24979C9.88281 10.4181 10.83 11.3653 11.9983 11.3653Z" stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" ></path>
- </svg>
- <span className="text-ly-12 ml-2.5">
- {streetAddress},
- {city},
- {stateProvince},
- {postCode},
- {selectedCountryCode}
- </span>
- </div>
- </div>
- }
- <LyDropDown
- showDropDown={showDropDown}
- onClose={closeMyaddressList}
- >
- <div className="w-full p-3 text-ly-13 font-medium" key={'edit'} onClick={() => {selectAddress('edit')}}>
- Edit Address
- </div>
- {customerAddressList.map((item) => {
- return (
- <div className="w-full p-3" key={item.id} onClick={() => selectAddress(item)}>
-
- <p className="text-ly-13 leading-4">
- {item.firstName} {item.lastName},
- {item.address},
- {item.city},
- {item.state},
- {item.postcode},
- {item.country},
- {item.phone}
- </p>
- </div>
- );
- })}
-
- </LyDropDown>
-
- </div>
-
-
- <p className="text-ly-12 leading-ly-20 mt-2 mb-2">
- Select a shipping address from your address book or enter a new address.
- </p>
- {/**
- * 登录用户: noAddres=false and showFormField = false 时 隐藏; noAddres=true 显示
- * 游客: noAddres=false and showFormField = false 时 隐藏; noAddres=true 显示
- */}
- <div className={clsx("box-border w-full", {
- "hidden": !showFormField && !noAddress
- })}>
- <div className={clsx("w-full",{
- "hidden": !isGuest
- })}>
- <label className="text-ly-12 block mb-4 font-semibold">
- Email *
- </label>
- <InputText
- type="email"
- placeholder="Enter your email address"
- {...register("shippingEmail", {
- required: "Email is required",
- pattern: {
- value: EMAIL_REGEX,
- message: "Please enter a valid email address",
- },
- onChange: (e) => {
- onEmailChange(e.target.value);
- // console.log(e.target.value);
- // if(isGuest) {
- // verifyEmailIsRegistered(e.target.value);
- // }
- }
- })}
- error={errors.shippingEmail && errors.shippingEmail.message as string}
- />
- </div>
- <div className="w-full mt-4">
- <label className="text-ly-12 block mb-4 font-semibold">
- Name *
- </label>
- <div className="w-full flex justify-between">
- <div className="w-41.25 flex-none">
- <InputText
- type="text"
- placeholder="First name"
- {...register("shippingFirstName", {
- required: "First name is required",
- pattern: {
- value: IS_VALID_INPUT,
- message: "Invalid First Name",
- }
- })}
- error={errors.shippingFirstName && errors.shippingFirstName.message as string}
- />
- </div>
- <div className="w-41.25 flex-none">
- <InputText
- type="text"
- placeholder="Last name"
- {...register("shippingLastName", {
- required: "Last name is required",
- pattern: {
- value: IS_VALID_INPUT,
- message: "Invalid Last Name",
- }
- })}
- error={errors.shippingLastName && errors.shippingLastName.message as string}
- />
- </div>
- </div>
- </div>
- <div className="w-full mt-4">
- <label className="text-ly-12 block mb-4 font-semibold">Address *</label>
- <InputText
- type="text"
- placeholder="Please Input Your Detailed Address With Apt. No"
- {...register("shippingAddress", {
- required: "Address is required",
- })}
- error={errors.shippingAddress && errors.shippingAddress.message as string}
- />
-
- </div>
- <div className="w-full mt-4">
- <label className="text-ly-12 block mb-4 font-semibold">Country and State/Province *</label>
- <div className="w-full">
- <div className="w-full flex justify-between">
- <div className="w-41.25 flex-none">
- <Select placeholder="Country/Region"
- {...register("shippingCountry", {
- required: "Country/Region field is required",
- onChange: () => {
- // 清空 state/city
- setValue('shippingState','',{
- shouldDirty: true,
- shouldValidate: true,
- });
- setValue('shippingCity','',{
- shouldDirty: true,
- shouldValidate: true,
- });
- }
- })}
- options={countriesOptions}
- error={errors.shippingCountry && errors.shippingCountry.message as string}
- />
- </div>
- <div className="w-41.25 flex-none relative">
- {statesLoading && <LoadingSpinner className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />}
- {statesOptions.length > 0 ?
- (
- <Select placeholder="State/Province"
- {...register("shippingState", {
- required: "State/Province field is required"
- })}
- options={statesOptions}
- error={errors.shippingState && errors.shippingState.message as string}
- />
- )
- : (
- <InputText
- type="text"
- placeholder="State/Province"
- {...register("shippingState", {
- required: "State/Province field is required"
- })}
- error={errors.shippingState && errors.shippingState.message as string}
- />
- )}
-
- </div>
-
- </div>
- <div className="w-full flex justify-between mt-4">
- <div className="w-41.25 flex-none">
- <InputText
- type="text"
- placeholder="Zip/Postal Code"
- {...register("shippingPostcode", {
- required: "Postcode field is required",
- pattern: {
- value: IS_VALID_INPUT,
- message: "Invalid Postcode",
- },
- })}
- error={errors.shippingPostcode&& errors.shippingPostcode.message as string}
- />
- </div>
- <div className="w-41.25 flex-none">
- <InputText
- type="text"
- placeholder="City"
- {...register("shippingCity", {
- required: "City field is required",
- pattern: {
- value: IS_VALID_INPUT,
- message: "Invalid City",
- },
- })}
- error={errors.shippingCity && errors.shippingCity.message as string}
- />
- </div>
- </div>
-
- </div>
- </div>
- <div className="w-full mt-4">
- <label className="text-ly-12 block mb-4 font-semibold">Phone number *</label>
- <Controller
- name="shippingPhoneNumber"
- control={control}
- rules={{
- required: "Phone Number is required",
- pattern: {
- value: IS_VALID_FULL_PHONE,
- message: "Invalid Phone Number",
- }
- }}
- render={({ field, fieldState }) => {
- return (
- <PhoneNumberInput
- placeholder="Please Input Your Phone Number"
- value={field.value}
- onChange={field.onChange}
- onBlur={field.onBlur}
- name={field.name}
- ref={field.ref}
- error={fieldState.error?.message}
- countryCode={selectedCountryCode}
- hasUserSelectedPhoneCode={hasUserSelectedPhoneCode}
- onHasUserSelectedPhoneCode={() => setHasUserSelectedPhoneCode(true)}
- />
- );
- }}
- />
-
- </div>
- </div>
- </>);
- };
|