LoginModal.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. "use client";
  2. import {useState, useLayoutEffect} from "react";
  3. import { useRouter } from 'next/navigation';
  4. import { signIn } from "next-auth/react";
  5. import clsx from "clsx";
  6. import { useForm, Controller } from "react-hook-form";
  7. import { EMAIL_REGEX, IS_VALID_INPUT, IS_VALID_FULL_PHONE } from "@utils/constants";
  8. import {mergeCartAction} from "@/actions/mergeCartAction";
  9. import { useAppDispatch } from "@/store/hooks";
  10. import { updateCart } from "@/store/slices/cart-slice";
  11. import { useCustomToast } from "@/utils/hooks/useToast";
  12. import { useUserRegister } from "@/utils/hooks/useUserRegister";
  13. import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
  14. import { formatCartDetail } from "@/utils/cartDetailTools";
  15. import {CartDetail} from "@/types/cart/type";
  16. import CommonModal from "@/components/theme/ui/CommonModal";
  17. import InputText from "@/components/theme/ui/InputText";
  18. import PasswordInput from "@/components/theme/ui/PasswordInput";
  19. import PhoneNumberInput from "@/components/theme/ui/PhoneNumberInput/PhoneNumberInput";
  20. type TabFlag = "login"|"register";
  21. interface LoginFormData {
  22. username:string;
  23. password:string;
  24. }
  25. interface RegisterFormData {
  26. firstName: string;
  27. lastName: string;
  28. email: string;
  29. password: string;
  30. confirmPassword: string;
  31. phone: string;
  32. subscribedToNewsLetter: boolean;
  33. isAgree: boolean;
  34. }
  35. export interface LoginModalReult {
  36. loginSuccess: boolean;
  37. registerSuccess: boolean;
  38. mergeCartSuccess: boolean;
  39. cartAfterMerge: CartDetail | null;
  40. msg: string;
  41. }
  42. export default function LoginModal({
  43. isShow,
  44. callbackUrl,
  45. closeModal,
  46. onFinalResult = async () => {},
  47. }: {
  48. isShow: boolean;
  49. callbackUrl?: string
  50. closeModal: (bool: boolean) => void;
  51. onFinalResult?: (param: LoginModalReult) =>void | Promise<void>;
  52. }) {
  53. const router = useRouter();
  54. const dispatch = useAppDispatch();
  55. const {userRegister} = useUserRegister();
  56. const { showToast } = useCustomToast();
  57. const loginForm = useForm({
  58. mode: "onChange",
  59. reValidateMode: "onChange",
  60. defaultValues: {
  61. username: '',
  62. password: ''
  63. }
  64. });
  65. const registerForm = useForm({
  66. mode: "onChange",
  67. reValidateMode: "onChange",
  68. defaultValues: {
  69. firstName: '',
  70. lastName: '',
  71. email: '',
  72. password: '',
  73. confirmPassword: '',
  74. phone: '',
  75. subscribedToNewsLetter: true,
  76. isAgree: true
  77. }
  78. });
  79. const [activeTab, setActiveTab] = useState<TabFlag>('login');
  80. useLayoutEffect(() => {
  81. if(isShow) {
  82. loginForm.reset();
  83. registerForm.reset();
  84. }
  85. },[isShow]);
  86. const toggleLoginRegister = (tab: TabFlag) => {
  87. setActiveTab(tab);
  88. };
  89. const toSignin = async (data: {username:string;password:string;flag:'login'|'register';}) => {
  90. const result = await signIn("credentials", {
  91. redirect: false,
  92. username: data.username,
  93. password: data.password,
  94. callbackUrl
  95. });
  96. const finalRes: LoginModalReult = {
  97. loginSuccess: false,
  98. registerSuccess: data.flag === "register" ? true : false,
  99. mergeCartSuccess: false,
  100. cartAfterMerge: null,
  101. msg: ''
  102. };
  103. if (!result?.ok) { // 登录失败
  104. let txt = '';
  105. if(finalRes.registerSuccess) {
  106. txt = "Register Successfully! But fail to login: " + result?.error;
  107. } else {
  108. txt = result?.error || "Fail to login.";
  109. }
  110. showToast(txt, "danger");
  111. finalRes.msg = txt;
  112. onFinalResult(finalRes);
  113. return;
  114. }
  115. // 登录成功
  116. finalRes.loginSuccess = true;
  117. showToast("Successfully logged in.", "success");
  118. // 合并购物车
  119. const mergeRes = await mergeCartAction();
  120. finalRes.mergeCartSuccess = mergeRes.success;
  121. finalRes.cartAfterMerge = mergeRes.cartDetail;
  122. console.log('cartAfterMerge ========', mergeRes);
  123. if(mergeRes.cartDetail) {
  124. const newCartDetail = formatCartDetail(mergeRes.cartDetail);
  125. dispatch(updateCart(newCartDetail));
  126. }
  127. await onFinalResult(finalRes);
  128. // 关闭登录弹窗
  129. closeModal(false);
  130. if(callbackUrl){
  131. router.push(callbackUrl); // 跳转指定页面
  132. }else{
  133. router.refresh(); // 当前页面重新渲染
  134. }
  135. };
  136. const loginSubmit = async (data:LoginFormData) => {
  137. console.log(data);
  138. overlayLoading.start();
  139. await toSignin({
  140. username: data.username,
  141. password: data.password,
  142. flag: 'login'
  143. });
  144. overlayLoading.stop();
  145. };
  146. const registerSubmit = async (registerData: RegisterFormData) => {
  147. console.log(registerData);
  148. overlayLoading.start();
  149. const registerRes = await userRegister({
  150. input: {
  151. firstName: registerData.firstName,
  152. lastName: registerData.lastName,
  153. email: registerData.email,
  154. password: registerData.password,
  155. confirmPassword: registerData.confirmPassword,
  156. phone: registerData.phone,
  157. subscribedToNewsLetter: registerData.subscribedToNewsLetter
  158. }
  159. });
  160. if(!registerRes.error) {
  161. // 注册成功,登录
  162. await toSignin({
  163. username: registerData.email,
  164. password: registerData.password,
  165. flag: 'register'
  166. });
  167. overlayLoading.stop();
  168. } else {
  169. overlayLoading.stop();
  170. showToast("Fail to register: " + registerRes.msg, "danger");
  171. onFinalResult({
  172. loginSuccess: false,
  173. registerSuccess: false,
  174. mergeCartSuccess: false,
  175. cartAfterMerge: null,
  176. msg: registerRes.msg
  177. })
  178. }
  179. };
  180. return (<>
  181. <CommonModal
  182. isOpen={isShow}
  183. onClose={closeModal}
  184. header={
  185. <div className="relative flex justify-center items-center pt-6 px-4 pb-4">
  186. <button
  187. className={clsx("flex-none",{
  188. "text-ly-18 font-bold": activeTab === "register",
  189. "text-ly-16 font-media text-ly-deepgray": activeTab !== "register"
  190. })}
  191. onClick={() => toggleLoginRegister("register")}
  192. >
  193. Register
  194. </button>
  195. <span className="flex-none w-0.5 h-4.5 bg-ly-deepgray mx-5"></span>
  196. <button
  197. className={clsx("flex-none",{
  198. "text-ly-18 font-bold": activeTab === "login",
  199. "text-ly-16 font-media text-ly-deepgray": activeTab !== "login"
  200. })}
  201. onClick={() => toggleLoginRegister("login")}
  202. >
  203. Sign in
  204. </button>
  205. <button className="absolute top-4 right-4 w-6 h-6" onClick={() => closeModal(false)}>
  206. <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon" className="h-6 transition-all ease-in-out hover:scale-110"><path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12"></path></svg>
  207. </button>
  208. </div>
  209. }
  210. body={
  211. <>
  212. <form
  213. className={clsx("relative px-4 pt-2 pb-6",{
  214. "hidden": activeTab !== 'register'
  215. })}
  216. onSubmit={registerForm.handleSubmit(registerSubmit)}
  217. >
  218. <div className="w-full flex justify-between">
  219. <div className="w-41.25 flex-none">
  220. <label className="text-ly-12 block mb-3 font-semibold">
  221. First Name *
  222. </label>
  223. <InputText
  224. type="text"
  225. placeholder="First Name"
  226. {...registerForm.register("firstName", {
  227. required: "First name is required",
  228. pattern: {
  229. value: IS_VALID_INPUT,
  230. message: "Invalid First Name",
  231. }
  232. })}
  233. error={registerForm.formState.errors.firstName && registerForm.formState.errors.firstName.message as string}
  234. />
  235. </div>
  236. <div className="w-41.25 flex-none">
  237. <label className="text-ly-12 block mb-3 font-semibold">
  238. Last Name *
  239. </label>
  240. <InputText
  241. type="text"
  242. placeholder="Last Name"
  243. {...registerForm.register("lastName", {
  244. required: "Last Name is required",
  245. pattern: {
  246. value: IS_VALID_INPUT,
  247. message: "Invalid Last Name",
  248. }
  249. })}
  250. error={registerForm.formState.errors.lastName && registerForm.formState.errors.lastName.message as string}
  251. />
  252. </div>
  253. </div>
  254. <div className="w-full mt-4">
  255. <label className="text-ly-12 block mb-3 font-semibold">
  256. Email *
  257. </label>
  258. <InputText
  259. type="email"
  260. placeholder="Enter your email address"
  261. {...registerForm.register("email", {
  262. required: "Email is required",
  263. pattern: {
  264. value: EMAIL_REGEX,
  265. message: "Please enter a valid email address",
  266. }
  267. })}
  268. error={registerForm.formState.errors.email && registerForm.formState.errors.email.message as string}
  269. />
  270. </div>
  271. <div className="w-full mt-4">
  272. <label className="text-ly-12 block mb-3 font-semibold">
  273. Password *
  274. </label>
  275. <PasswordInput
  276. placeholder="Enter your password"
  277. {...registerForm.register("password", {
  278. required: "Password is required",
  279. minLength: {
  280. value: 6,
  281. message: "Be at least 6 characters long",
  282. },
  283. validate: (value: string) => {
  284. const pass=value.trim(); /*strip leading and trailing spaces*/
  285. const res = pass.length>0 && pass.length < 6;
  286. if(res) {
  287. return 'Please enter 6 or more characters. Leading or trailing spaces will be ignored.';
  288. }
  289. return true;
  290. },
  291. })}
  292. error={registerForm.formState.errors.password && registerForm.formState.errors.password.message as string}
  293. />
  294. </div>
  295. <div className="w-full mt-4">
  296. <label className="text-ly-12 block mb-3 font-semibold">
  297. Confirm Password *
  298. </label>
  299. <PasswordInput
  300. placeholder="Confirm password"
  301. {...registerForm.register("confirmPassword", {
  302. required: "Confirm Password is required",
  303. minLength: {
  304. value: 6,
  305. message: "Be at least 6 characters long",
  306. },
  307. validate: (value: string, {password}) => {
  308. const pass = value.trim(); /*strip leading and trailing spaces*/
  309. const res = pass.length>0 && pass.length < 6;
  310. if(res) {
  311. return 'Please enter 6 or more characters. Leading or trailing spaces will be ignored.';
  312. }
  313. if(pass !== password) {
  314. return 'The Confirm Passwords do not match the Password';
  315. }
  316. return true;
  317. },
  318. })}
  319. error={registerForm.formState.errors.confirmPassword && registerForm.formState.errors.confirmPassword.message as string}
  320. />
  321. </div>
  322. <div className="w-full mt-4">
  323. <label className="text-ly-12 block mb-3 font-semibold">Phone number *</label>
  324. <Controller
  325. name="phone"
  326. control={registerForm.control}
  327. rules={{
  328. required: "Phone Number is required",
  329. pattern: {
  330. value: IS_VALID_FULL_PHONE,
  331. message: "Invalid Phone Number",
  332. }
  333. }}
  334. render={({ field, fieldState }) => {
  335. return (
  336. <PhoneNumberInput
  337. placeholder="Please Input Your Phone Number"
  338. value={field.value}
  339. onChange={field.onChange}
  340. onBlur={field.onBlur}
  341. name={field.name}
  342. ref={field.ref}
  343. error={fieldState.error?.message}
  344. />
  345. );
  346. }}
  347. />
  348. </div>
  349. <button className="flex items-center justify-center mt-6 w-full h-12 bg-ly-green text-white rounded-3xl text-ly-16 font-bold">
  350. Register
  351. </button>
  352. <div className="relative w-full mt-4">
  353. <label htmlFor="is_agree" className="group relative flex">
  354. <input type="checkbox"
  355. id="is_agree"
  356. className="absolute opacity-0"
  357. {...registerForm.register("isAgree", {
  358. validate: (value: boolean) => {
  359. if(!value) {
  360. showToast('Please Agree To The Terms & Conditions And Privacy Policy.', "warning");
  361. return 'Please Agree To The Terms & Conditions And Privacy Policy.';
  362. }
  363. return true;
  364. },
  365. })}
  366. />
  367. <span className="flex-none mr-2">
  368. <svg className="icon-unchecked block w-4 h-4 group-has-[:checked]:hidden" 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="1" y="1" width="14" height="14" rx="1" fill="#FFFFFF" ></rect><path fill="rgba(0, 0, 0, 1)" d="M2 0.5L14 0.5C14.8284 0.5 15.5 1.17157 15.5 2L15.5 14C15.5 14.8284 14.8284 15.5 14 15.5L2 15.5C1.17157 15.5 0.5 14.8284 0.5 14L0.5 2C0.5 1.17157 1.17157 0.5 2 0.5ZM2 1.5C1.72386 1.5 1.5 1.72386 1.5 2L1.5 14C1.5 14.2761 1.72386 14.5 2 14.5L14 14.5C14.2761 14.5 14.5 14.2761 14.5 14L14.5 2C14.5 1.72386 14.2761 1.5 14 1.5L2 1.5Z"></path></svg>
  369. <svg className="icon-checked hidden w-4 h-4 group-has-[:checked]:block" 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="1" y="1" width="14" height="14" rx="1" fill="#000000" ></rect><path stroke="rgba(255, 255, 255, 1)" strokeWidth="1.5" strokeLinecap="square" d="M5.3335 8L7.3335 10L11.3335 6"></path></svg>
  370. </span>
  371. <span className="text-ly-12 text-[#666666]">
  372. I Agree To The
  373. Terms & Conditions
  374. And
  375. Privacy Policy
  376. Of asteriahair.com
  377. </span>
  378. </label>
  379. </div>
  380. <div className="relative w-full mt-4">
  381. <label htmlFor="is_subscribed" className="group relative flex">
  382. <input type="checkbox"
  383. id="is_subscribed"
  384. className="absolute opacity-0"
  385. {...registerForm.register("subscribedToNewsLetter")}
  386. />
  387. <span className="flex-none mr-2">
  388. <svg className="icon-unchecked block w-4 h-4 group-has-[:checked]:hidden" 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="1" y="1" width="14" height="14" rx="1" fill="#FFFFFF" ></rect><path fill="rgba(0, 0, 0, 1)" d="M2 0.5L14 0.5C14.8284 0.5 15.5 1.17157 15.5 2L15.5 14C15.5 14.8284 14.8284 15.5 14 15.5L2 15.5C1.17157 15.5 0.5 14.8284 0.5 14L0.5 2C0.5 1.17157 1.17157 0.5 2 0.5ZM2 1.5C1.72386 1.5 1.5 1.72386 1.5 2L1.5 14C1.5 14.2761 1.72386 14.5 2 14.5L14 14.5C14.2761 14.5 14.5 14.2761 14.5 14L14.5 2C14.5 1.72386 14.2761 1.5 14 1.5L2 1.5Z"></path></svg>
  389. <svg className="icon-checked hidden w-4 h-4 group-has-[:checked]:block" 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="1" y="1" width="14" height="14" rx="1" fill="#000000" ></rect><path stroke="rgba(255, 255, 255, 1)" strokeWidth="1.5" strokeLinecap="square" d="M5.3335 8L7.3335 10L11.3335 6"></path></svg>
  390. </span>
  391. <span className="text-ly-12 text-[#666666]">I'd Like To Receive Emails Or Sms With Exclusive Discounts And News Fromaupearlhair.</span>
  392. </label>
  393. </div>
  394. </form>
  395. <form
  396. className={clsx("relative px-4 pt-2",{
  397. "hidden": activeTab !== "login"
  398. })}
  399. onSubmit={loginForm.handleSubmit(loginSubmit)}
  400. >
  401. <div className="w-full">
  402. <label className="text-ly-12 block mb-3 font-semibold">
  403. Email *
  404. </label>
  405. <InputText
  406. type="email"
  407. placeholder="Enter your email address"
  408. {...loginForm.register("username", {
  409. required: "Email is required",
  410. pattern: {
  411. value: EMAIL_REGEX,
  412. message: "Please enter a valid email address",
  413. }
  414. })}
  415. error={loginForm.formState.errors.username && loginForm.formState.errors.username.message as string}
  416. />
  417. </div>
  418. <div className="w-full mt-4">
  419. <label className="text-ly-12 block mb-3 font-semibold">
  420. Password *
  421. </label>
  422. <PasswordInput
  423. placeholder="Enter your password"
  424. {...loginForm.register("password", {
  425. required: "Password is required",
  426. minLength: {
  427. value: 6,
  428. message: "Be at least 6 characters long",
  429. },
  430. validate: (value: string) => {
  431. const pass=value.trim(); /*strip leading and trailing spaces*/
  432. const res = pass.length>0 && pass.length < 6;
  433. if(res) {
  434. return 'Please enter 6 or more characters. Leading or trailing spaces will be ignored.';
  435. }
  436. return true;
  437. },
  438. })}
  439. error={loginForm.formState.errors.password && loginForm.formState.errors.password.message as string}
  440. />
  441. </div>
  442. <button className="flex items-center justify-center mt-6 w-full h-12 bg-ly-green text-white rounded-3xl text-ly-16 font-bold">Log In</button>
  443. </form>
  444. </>
  445. }
  446. />
  447. </>);
  448. }