ShippingMethod.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. "use client";
  2. import { FieldValues, useForm, Controller } from "react-hook-form";
  3. import { cn } from "@heroui/theme";
  4. import { Radio, RadioGroup } from "@heroui/radio";
  5. import { useState } from "react";
  6. import { ProceedToCheckout } from "../ProceedToCheckout";
  7. import { useCustomToast } from "@utils/hooks/useToast";
  8. import { useCheckout } from "@utils/hooks/useCheckout";
  9. import { CustomRadioProps, ShippingMethodType } from "@components/checkout/type";
  10. import { useDispatch } from "react-redux";
  11. import { updateCart } from "@/store/slices/cart-slice";
  12. export default function ShippingMethod({
  13. shippingMethod,
  14. selectedShippingRate,
  15. currentStep
  16. }: {
  17. shippingMethod?: ShippingMethodType[];
  18. selectedShippingRate?: any;
  19. methodDesc?: string;
  20. currentStep?: string;
  21. }) {
  22. const { isSaving, saveCheckoutShipping } = useCheckout();
  23. const { showToast } = useCustomToast();
  24. const [isOpen, setIsOpen] = useState(currentStep !== "shipping");
  25. const [prevValues, setPrevValues] = useState({ currentStep, selectedShippingRate });
  26. if (currentStep !== prevValues.currentStep || selectedShippingRate !== prevValues.selectedShippingRate) {
  27. setPrevValues({ currentStep, selectedShippingRate });
  28. if (currentStep === "shipping") {
  29. setIsOpen(false);
  30. } else if (selectedShippingRate) {
  31. setIsOpen(true);
  32. }
  33. }
  34. const { control, handleSubmit } = useForm({
  35. mode: "onSubmit",
  36. defaultValues: {
  37. method: selectedShippingRate ?? "",
  38. },
  39. });
  40. const selectedMethodTitle = shippingMethod?.find(
  41. (method) => method.method === selectedShippingRate
  42. )?.label;
  43. const selectedMethodPrice = shippingMethod?.find(
  44. (method) => method.method === selectedShippingRate
  45. )?.price;
  46. const dispatch = useDispatch();
  47. const onSubmit = async (data: FieldValues) => {
  48. if (!data?.method) {
  49. showToast("Please Choose the Shipping Method", "warning");
  50. return;
  51. }
  52. try {
  53. const selectedRate = shippingMethod?.find(m => m.method === data.method);
  54. if (selectedRate) {
  55. dispatch(updateCart({
  56. // shippingMethod: selectedRate?.method || "",
  57. selectedShippingRate: selectedRate?.method || "",
  58. selectedShippingRateTitle: selectedRate?.label || "",
  59. }));
  60. }
  61. await saveCheckoutShipping(data?.method);
  62. } catch {
  63. showToast("Failed to save shipping method. Please try again.");
  64. }
  65. };
  66. return (
  67. <div>
  68. {(selectedShippingRate) ? (
  69. isOpen ? (
  70. <>
  71. <div className="mt-4 justify-between hidden sm:flex">
  72. <div className="flex">
  73. <p className="w-auto text-base font-normal text-black/60 dark:text-white/60 sm:w-[192px]">
  74. Shipping Method
  75. </p>
  76. <p className="text-base font-normal">{selectedMethodTitle} (${selectedMethodPrice})</p>
  77. </div>
  78. <div className="flex">
  79. <button
  80. onClick={() => {
  81. setIsOpen(!isOpen);
  82. }}
  83. className="cursor-pointer text-base font-normal text-black/[60%] underline dark:text-neutral-300"
  84. >
  85. Change
  86. </button>
  87. </div>
  88. </div>
  89. <div className="mt-4 block sm:hidden flex flex-col justify-between sm:flex-row relative ">
  90. <div className="flex justify-between flex-1 wrap">
  91. <p className="w-auto text-base font-normal text-black/60 dark:text-white/60 sm:w-[192px]">
  92. Shipping Method
  93. </p>
  94. <p className="text-base font-normal">{selectedMethodTitle} (${selectedMethodPrice})</p>
  95. </div>
  96. <button
  97. onClick={() => {
  98. setIsOpen(!isOpen);
  99. }}
  100. className="cursor-pointer absolute right-0 text-base font-normal text-black/[60%] underline dark:text-neutral-300"
  101. style={{ top: "-36px" }}
  102. >
  103. Change
  104. </button>
  105. </div>
  106. </>
  107. ) : (
  108. <form className="mt-6" onSubmit={handleSubmit(onSubmit)}>
  109. <div className="flex flex-col gap-5">
  110. {Array.isArray(shippingMethod) && (
  111. <Controller
  112. control={control}
  113. name="method"
  114. render={({ field }) => (
  115. <RadioGroup
  116. {...field}
  117. label=""
  118. value={field.value ?? ""}
  119. onValueChange={field.onChange}
  120. >
  121. {shippingMethod.map((method: any) => (
  122. <CustomRadio
  123. key={method?.code}
  124. className="inset-0 my-1 border border-solid border-neutral-300 dark:border-neutral-500"
  125. description={"$" + method?.price}
  126. value={method?.method}
  127. >
  128. <span className="text-neutral-700 dark:text-white">
  129. {method?.label}
  130. </span>
  131. </CustomRadio>
  132. ))}
  133. </RadioGroup>
  134. )}
  135. />
  136. )}
  137. </div>
  138. <div className="my-6 justify-self-end">
  139. <ProceedToCheckout buttonName="Next" pending={isSaving} />
  140. </div>
  141. </form>
  142. )
  143. ) : (
  144. <form className="mt-6" onSubmit={handleSubmit(onSubmit)}>
  145. <div className="flex flex-col gap-5">
  146. {Array.isArray(shippingMethod) && (
  147. <Controller
  148. control={control}
  149. name="method"
  150. render={({ field }) => {
  151. return (
  152. <RadioGroup
  153. {...field}
  154. label=""
  155. value={field.value ?? ""}
  156. onValueChange={(value) => {
  157. field.onChange(value);
  158. }}
  159. >
  160. {shippingMethod.map((method : any) => (
  161. <CustomRadio
  162. key={method?.code}
  163. className="inset-0 my-1 border border-solid border-neutral-300 dark:border-neutral-500"
  164. description={"$" + method?.price}
  165. value={method?.method}
  166. >
  167. <span className="text-neutral-700 dark:text-white">
  168. {method?.label}
  169. </span>
  170. </CustomRadio>
  171. ))}
  172. </RadioGroup>
  173. )
  174. }}
  175. />
  176. )}
  177. </div>
  178. <div className="my-6 justify-self-end">
  179. <ProceedToCheckout buttonName="Next" pending={isSaving} />
  180. </div>
  181. </form>
  182. )}
  183. </div>
  184. );
  185. }
  186. const CustomRadio = (props: CustomRadioProps) => {
  187. const { children, className, ...otherProps } = props;
  188. return (
  189. <Radio
  190. {...otherProps}
  191. classNames={{
  192. base: cn(
  193. "inline-flex m-0 bg-transparent hover:bg-transparent items-center",
  194. "flex-row items-baseline max-w-full cursor-pointer rounded-lg gap-4 p-4 border-2 border-transparent",
  195. "data-[selected=true]:border-primary",
  196. className
  197. ),
  198. hiddenInput: "peer absolute h-0 w-0 opacity-0",
  199. }}
  200. >
  201. {children}
  202. </Radio>
  203. );
  204. };