LoginForm.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. "use client";
  2. import clsx from "clsx";
  3. import { getSession, signIn } from "next-auth/react";
  4. import Image from "next/image";
  5. import Link from "next/link";
  6. import { useRouter } from "next/navigation";
  7. import { SubmitHandler, useForm } from "react-hook-form";
  8. import { Button } from "@components/common/button/Button";
  9. import { EMAIL_REGEX, SIGNIN_IMG } from "@/utils/constants";
  10. import InputText from "@components/common/form/Input";
  11. import { useCustomToast } from "@/utils/hooks/useToast";
  12. import { useMergeCart } from "@utils/hooks/useMergeCart";
  13. import {
  14. setCookie,
  15. getGuestCartToken,
  16. getGuestCartId,
  17. deleteGuestCookie
  18. } from "@/utils/cookie-tools";
  19. import { useAppSelector } from "@/store/hooks";
  20. import { useCartDetail } from "@utils/hooks/useCartDetail";
  21. import { IS_GUEST } from "@/utils/constants";
  22. type LoginFormInputs = {
  23. username: string;
  24. password: string;
  25. };
  26. export default function LoginForm() {
  27. const router = useRouter();
  28. const { showToast } = useCustomToast();
  29. const { getCartDetail } = useCartDetail()
  30. const { mergeCart } = useMergeCart();
  31. const cart = useAppSelector((state) => state.cartDetail.cart);
  32. const {
  33. register,
  34. handleSubmit,
  35. formState: { errors, isSubmitting },
  36. } = useForm<LoginFormInputs>({
  37. mode: "onSubmit",
  38. reValidateMode: "onChange",
  39. });
  40. const onSubmit: SubmitHandler<LoginFormInputs> = async (data) => {
  41. try {
  42. // First, handle cart merging before sign in
  43. const guestCartId = getGuestCartId();
  44. const guestCartToken = getGuestCartToken();
  45. /**
  46. * @todo 使用 signInAuth 重写
  47. * result: {error: null,ok: true,status: 200,url: "http://localhost:3001/"}
  48. */
  49. const result = await signIn("credentials", {
  50. redirect: false,
  51. ...data,
  52. callbackUrl: "/", // The callbackUrl specifies to which URL the user will be redirected after signing in. Defaults to the page URL the sign-in is initiated from.
  53. });
  54. console.log('result --- ',result);
  55. if (!result?.ok) {
  56. showToast(result?.error || "Invalid login credentials.", "warning");
  57. return;
  58. }
  59. showToast("Welcome! Successfully logged in.", "success");
  60. const session = await getSession();
  61. const userToken: string | undefined = session?.user?.accessToken;
  62. if (!userToken) {
  63. console.warn("No API token available in session after login");
  64. console.error('userToken is required');
  65. }
  66. // Only merge cart if user had a guest cart before login
  67. if (userToken && guestCartId && guestCartToken) {
  68. try {
  69. if(cart) {
  70. await mergeCart({ variables: { token: userToken, cartId: parseInt(guestCartId, 10) } });
  71. }
  72. } catch (err) {
  73. console.error("mergeCart failed:", err);
  74. }
  75. // 登录成功后不应该保存用户的token
  76. setCookie(IS_GUEST, "false");
  77. deleteGuestCookie();
  78. await getCartDetail();
  79. } else if (userToken) {
  80. // User logged in without a guest cart, just set the token
  81. setCookie(IS_GUEST, "false");
  82. deleteGuestCookie();
  83. }
  84. setTimeout(() => {
  85. router.push("/");
  86. router.refresh();
  87. }, 100);
  88. } catch (error) {
  89. console.error(error);
  90. showToast("Something went wrong. Please try again.", "danger");
  91. }
  92. };
  93. return (
  94. <div className="flex w-full items-center max-w-screen-2xl mx-auto px-4 xss:px-7.5 justify-between gap-4 lg:my-16 xl:my-28">
  95. <div className="flex w-full max-w-[583px] flex-col gap-y-4 lg:gap-y-12">
  96. <div className="font-outfit">
  97. <h2 className="py-1 text-2xl font-semibold sm:text-4xl">
  98. Sign in to your account
  99. </h2>
  100. <p className="mt-2 text-base md:text-lg font-normal text-black/60 dark:text-neutral-400">
  101. If you have an account, sign in with your email address.
  102. </p>
  103. </div>
  104. <form
  105. noValidate
  106. className="flex flex-col gap-y-4 lg:gap-y-12"
  107. onSubmit={handleSubmit(onSubmit)}
  108. >
  109. <div className="flex flex-col gap-y-2.5 lg:gap-4">
  110. <InputText
  111. {...register("username", {
  112. required: "Email is required",
  113. pattern: {
  114. value: EMAIL_REGEX,
  115. message: "Please enter a valid email.",
  116. },
  117. })}
  118. errorMsg={
  119. errors.username?.message ? [errors.username.message] : undefined
  120. }
  121. label="Enter Your Email Address"
  122. labelPlacement="outside"
  123. name="username"
  124. placeholder="Enter your email address"
  125. rounded="md"
  126. size="lg"
  127. typeName="email"
  128. />
  129. <InputText
  130. {...register("password", {
  131. required: "Password is required",
  132. minLength: {
  133. value: 2,
  134. message: "Be at least 2 characters long",
  135. },
  136. validate: (value) => {
  137. if (!/[0-2]/.test(value))
  138. return "Contain at least one number.";
  139. return true;
  140. },
  141. })}
  142. errorMsg={
  143. errors.password?.message ? [errors.password.message] : undefined
  144. }
  145. label="Enter Password"
  146. labelPlacement="outside"
  147. name="password"
  148. placeholder="Enter your password"
  149. rounded="md"
  150. size="lg"
  151. typeName="password"
  152. />
  153. <Link
  154. className="text-end text-sm font-medium text-blue-600 underline hover:text-blue-500 underline"
  155. href="/customer/forget-password"
  156. aria-label="Go to forgot password page"
  157. >
  158. Forgot your password ?
  159. </Link>
  160. </div>
  161. <div className="flex flex-col gap-2 lg:gap-y-3">
  162. <Button
  163. className="cursor-pointer"
  164. disabled={isSubmitting}
  165. loading={isSubmitting}
  166. title="Sign In"
  167. type="submit"
  168. />
  169. <span className="mx-auto font-outfit sm:mx-0">
  170. New customer?{" "}
  171. <Link
  172. className="font-medium text-blue-600 hover:text-blue-500 underline"
  173. href="/customer/register"
  174. aria-label="Go to create account page"
  175. >
  176. Create your account
  177. </Link>
  178. </span>
  179. </div>
  180. </form>
  181. </div>
  182. <div className="relative hidden aspect-[0.9] max-h-[692px] w-full max-w-[790px] sm:block md:aspect-[1.14]">
  183. <Image
  184. fill
  185. priority
  186. alt="Sign In Image"
  187. className={clsx(
  188. "relative h-full w-full object-fill",
  189. "transition duration-300 ease-in-out group-hover:scale-105"
  190. )}
  191. sizes={"(min-width: 768px) 66vw, 100vw"}
  192. src={SIGNIN_IMG}
  193. />
  194. </div>
  195. </div>
  196. );
  197. }