| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482 |
- "use client";
- import {useState, useLayoutEffect} from "react";
- import { useRouter } from 'next/navigation';
- import { signIn } from "next-auth/react";
- import clsx from "clsx";
- import { useForm, Controller } from "react-hook-form";
- import { EMAIL_REGEX, IS_VALID_INPUT, IS_VALID_FULL_PHONE } from "@utils/constants";
- import {mergeCartAction} from "@/actions/mergeCartAction";
- import { useAppDispatch } from "@/store/hooks";
- import { updateCart } from "@/store/slices/cart-slice";
- import { useCustomToast } from "@/utils/hooks/useToast";
- import { useUserRegister } from "@/utils/hooks/useUserRegister";
- import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
- import { formatCartDetail } from "@/utils/cartDetailTools";
- import {CartDetail} from "@/types/cart/type";
- import CommonModal from "@/components/theme/ui/CommonModal";
- import InputText from "@/components/theme/ui/InputText";
- import PasswordInput from "@/components/theme/ui/PasswordInput";
- import PhoneNumberInput from "@/components/theme/ui/PhoneNumberInput/PhoneNumberInput";
- type TabFlag = "login"|"register";
- interface LoginFormData {
- username:string;
- password:string;
- }
- interface RegisterFormData {
- firstName: string;
- lastName: string;
- email: string;
- password: string;
- confirmPassword: string;
- phone: string;
- subscribedToNewsLetter: boolean;
- isAgree: boolean;
- }
- export interface LoginModalReult {
- loginSuccess: boolean;
- registerSuccess: boolean;
- mergeCartSuccess: boolean;
- cartAfterMerge: CartDetail | null;
- msg: string;
- }
- export default function LoginModal({
- isShow,
- callbackUrl,
- closeModal,
- onFinalResult = async () => {},
- }: {
- isShow: boolean;
- callbackUrl?: string
- closeModal: (bool: boolean) => void;
- onFinalResult?: (param: LoginModalReult) =>void | Promise<void>;
- }) {
- const router = useRouter();
- const dispatch = useAppDispatch();
- const {userRegister} = useUserRegister();
- const { showToast } = useCustomToast();
- const loginForm = useForm({
- mode: "onChange",
- reValidateMode: "onChange",
- defaultValues: {
- username: '',
- password: ''
- }
- });
- const registerForm = useForm({
- mode: "onChange",
- reValidateMode: "onChange",
- defaultValues: {
- firstName: '',
- lastName: '',
- email: '',
- password: '',
- confirmPassword: '',
- phone: '',
- subscribedToNewsLetter: true,
- isAgree: true
- }
- });
- const [activeTab, setActiveTab] = useState<TabFlag>('login');
- useLayoutEffect(() => {
- if(isShow) {
- loginForm.reset();
- registerForm.reset();
- }
- },[isShow]);
- const toggleLoginRegister = (tab: TabFlag) => {
- setActiveTab(tab);
- };
- const toSignin = async (data: {username:string;password:string;flag:'login'|'register';}) => {
- const result = await signIn("credentials", {
- redirect: false,
- username: data.username,
- password: data.password,
- callbackUrl
- });
- const finalRes: LoginModalReult = {
- loginSuccess: false,
- registerSuccess: data.flag === "register" ? true : false,
- mergeCartSuccess: false,
- cartAfterMerge: null,
- msg: ''
- };
- if (!result?.ok) { // 登录失败
- let txt = '';
- if(finalRes.registerSuccess) {
- txt = "Register Successfully! But fail to login: " + result?.error;
- } else {
- txt = result?.error || "Fail to login.";
- }
- showToast(txt, "danger");
- finalRes.msg = txt;
- onFinalResult(finalRes);
- return;
- }
- // 登录成功
- finalRes.loginSuccess = true;
- showToast("Successfully logged in.", "success");
- // 合并购物车
- const mergeRes = await mergeCartAction();
- finalRes.mergeCartSuccess = mergeRes.success;
- finalRes.cartAfterMerge = mergeRes.cartDetail;
- console.log('cartAfterMerge ========', mergeRes);
- if(mergeRes.cartDetail) {
- const newCartDetail = formatCartDetail(mergeRes.cartDetail);
- dispatch(updateCart(newCartDetail));
- }
- await onFinalResult(finalRes);
-
- // 关闭登录弹窗
- closeModal(false);
- if(callbackUrl){
- router.push(callbackUrl); // 跳转指定页面
- }else{
- router.refresh(); // 当前页面重新渲染
- }
- };
- const loginSubmit = async (data:LoginFormData) => {
- console.log(data);
- overlayLoading.start();
- await toSignin({
- username: data.username,
- password: data.password,
- flag: 'login'
- });
- overlayLoading.stop();
- };
- const registerSubmit = async (registerData: RegisterFormData) => {
- console.log(registerData);
- overlayLoading.start();
- const registerRes = await userRegister({
- input: {
- firstName: registerData.firstName,
- lastName: registerData.lastName,
- email: registerData.email,
- password: registerData.password,
- confirmPassword: registerData.confirmPassword,
- phone: registerData.phone,
- subscribedToNewsLetter: registerData.subscribedToNewsLetter
- }
- });
- if(!registerRes.error) {
- // 注册成功,登录
- await toSignin({
- username: registerData.email,
- password: registerData.password,
- flag: 'register'
- });
- overlayLoading.stop();
- } else {
- overlayLoading.stop();
- showToast("Fail to register: " + registerRes.msg, "danger");
- onFinalResult({
- loginSuccess: false,
- registerSuccess: false,
- mergeCartSuccess: false,
- cartAfterMerge: null,
- msg: registerRes.msg
- })
- }
- };
- return (<>
- <CommonModal
- isOpen={isShow}
- onClose={closeModal}
- header={
- <div className="relative flex justify-center items-center pt-6 px-4 pb-4">
- <button
- className={clsx("flex-none",{
- "text-ly-18 font-bold": activeTab === "register",
- "text-ly-16 font-media text-ly-deepgray": activeTab !== "register"
- })}
- onClick={() => toggleLoginRegister("register")}
- >
- Register
- </button>
- <span className="flex-none w-0.5 h-4.5 bg-ly-deepgray mx-5"></span>
- <button
- className={clsx("flex-none",{
- "text-ly-18 font-bold": activeTab === "login",
- "text-ly-16 font-media text-ly-deepgray": activeTab !== "login"
- })}
- onClick={() => toggleLoginRegister("login")}
- >
- Sign in
- </button>
- <button className="absolute top-4 right-4 w-6 h-6" onClick={() => closeModal(false)}>
- <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>
- </button>
- </div>
- }
- body={
- <>
- <form
- className={clsx("relative px-4 pt-2 pb-6",{
- "hidden": activeTab !== 'register'
- })}
- onSubmit={registerForm.handleSubmit(registerSubmit)}
- >
-
- <div className="w-full flex justify-between">
- <div className="w-41.25 flex-none">
- <label className="text-ly-12 block mb-3 font-semibold">
- First Name *
- </label>
- <InputText
- type="text"
- placeholder="First Name"
- {...registerForm.register("firstName", {
- required: "First name is required",
- pattern: {
- value: IS_VALID_INPUT,
- message: "Invalid First Name",
- }
- })}
- error={registerForm.formState.errors.firstName && registerForm.formState.errors.firstName.message as string}
- />
- </div>
- <div className="w-41.25 flex-none">
- <label className="text-ly-12 block mb-3 font-semibold">
- Last Name *
- </label>
- <InputText
- type="text"
- placeholder="Last Name"
- {...registerForm.register("lastName", {
- required: "Last Name is required",
- pattern: {
- value: IS_VALID_INPUT,
- message: "Invalid Last Name",
- }
- })}
- error={registerForm.formState.errors.lastName && registerForm.formState.errors.lastName.message as string}
- />
- </div>
- </div>
- <div className="w-full mt-4">
- <label className="text-ly-12 block mb-3 font-semibold">
- Email *
- </label>
- <InputText
- type="email"
- placeholder="Enter your email address"
- {...registerForm.register("email", {
- required: "Email is required",
- pattern: {
- value: EMAIL_REGEX,
- message: "Please enter a valid email address",
- }
- })}
- error={registerForm.formState.errors.email && registerForm.formState.errors.email.message as string}
- />
- </div>
- <div className="w-full mt-4">
- <label className="text-ly-12 block mb-3 font-semibold">
- Password *
- </label>
- <PasswordInput
- placeholder="Enter your password"
- {...registerForm.register("password", {
- required: "Password is required",
- minLength: {
- value: 6,
- message: "Be at least 6 characters long",
- },
- validate: (value: string) => {
- const pass=value.trim(); /*strip leading and trailing spaces*/
- const res = pass.length>0 && pass.length < 6;
- if(res) {
- return 'Please enter 6 or more characters. Leading or trailing spaces will be ignored.';
- }
- return true;
- },
- })}
- error={registerForm.formState.errors.password && registerForm.formState.errors.password.message as string}
- />
- </div>
- <div className="w-full mt-4">
- <label className="text-ly-12 block mb-3 font-semibold">
- Confirm Password *
- </label>
- <PasswordInput
- placeholder="Confirm password"
- {...registerForm.register("confirmPassword", {
- required: "Confirm Password is required",
- minLength: {
- value: 6,
- message: "Be at least 6 characters long",
- },
- validate: (value: string, {password}) => {
- const pass = value.trim(); /*strip leading and trailing spaces*/
- const res = pass.length>0 && pass.length < 6;
- if(res) {
- return 'Please enter 6 or more characters. Leading or trailing spaces will be ignored.';
- }
- if(pass !== password) {
- return 'The Confirm Passwords do not match the Password';
- }
- return true;
- },
- })}
- error={registerForm.formState.errors.confirmPassword && registerForm.formState.errors.confirmPassword.message as string}
- />
- </div>
- <div className="w-full mt-4">
- <label className="text-ly-12 block mb-3 font-semibold">Phone number *</label>
- <Controller
- name="phone"
- control={registerForm.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}
-
- />
- );
- }}
- />
-
- </div>
- <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">
- Register
- </button>
- <div className="relative w-full mt-4">
-
- <label htmlFor="is_agree" className="group relative flex">
- <input type="checkbox"
- id="is_agree"
- className="absolute opacity-0"
- {...registerForm.register("isAgree", {
- validate: (value: boolean) => {
- if(!value) {
- showToast('Please Agree To The Terms & Conditions And Privacy Policy.', "warning");
- return 'Please Agree To The Terms & Conditions And Privacy Policy.';
- }
- return true;
- },
- })}
- />
- <span className="flex-none mr-2">
- <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>
- <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>
- </span>
- <span className="text-ly-12 text-[#666666]">
- I Agree To The
- Terms & Conditions
- And
- Privacy Policy
- Of asteriahair.com
- </span>
-
- </label>
- </div>
- <div className="relative w-full mt-4">
-
- <label htmlFor="is_subscribed" className="group relative flex">
- <input type="checkbox"
- id="is_subscribed"
- className="absolute opacity-0"
- {...registerForm.register("subscribedToNewsLetter")}
- />
- <span className="flex-none mr-2">
- <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>
- <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>
- </span>
- <span className="text-ly-12 text-[#666666]">I'd Like To Receive Emails Or Sms With Exclusive Discounts And News Fromaupearlhair.</span>
-
- </label>
- </div>
- </form>
- <form
- className={clsx("relative px-4 pt-2",{
- "hidden": activeTab !== "login"
- })}
- onSubmit={loginForm.handleSubmit(loginSubmit)}
- >
-
- <div className="w-full">
- <label className="text-ly-12 block mb-3 font-semibold">
- Email *
- </label>
- <InputText
- type="email"
- placeholder="Enter your email address"
- {...loginForm.register("username", {
- required: "Email is required",
- pattern: {
- value: EMAIL_REGEX,
- message: "Please enter a valid email address",
- }
- })}
- error={loginForm.formState.errors.username && loginForm.formState.errors.username.message as string}
- />
- </div>
- <div className="w-full mt-4">
- <label className="text-ly-12 block mb-3 font-semibold">
- Password *
- </label>
- <PasswordInput
- placeholder="Enter your password"
- {...loginForm.register("password", {
- required: "Password is required",
- minLength: {
- value: 6,
- message: "Be at least 6 characters long",
- },
- validate: (value: string) => {
- const pass=value.trim(); /*strip leading and trailing spaces*/
- const res = pass.length>0 && pass.length < 6;
- if(res) {
- return 'Please enter 6 or more characters. Leading or trailing spaces will be ignored.';
- }
- return true;
- },
- })}
- error={loginForm.formState.errors.password && loginForm.formState.errors.password.message as string}
- />
- </div>
-
- <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>
-
- </form>
- </>
- }
- />
- </>);
- }
|