CredentialModal.tsx 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. "use client";
  2. import { useDisclosure } from "@heroui/use-disclosure";
  3. import { AnimatePresence, motion } from "framer-motion";
  4. import clsx from "clsx";
  5. import { useSession, signOut } from "next-auth/react";
  6. import Link from "next/link";
  7. import { Avatar } from "@heroui/avatar";
  8. import { useForm } from "react-hook-form";
  9. import { usePathname, useRouter } from "next/navigation";
  10. import { useCustomToast } from '@/utils/hooks/useToast';
  11. import { useBodyScrollLock } from "@utils/hooks/useBodyScrollLock";
  12. import OpenAuth from "../OpenAuth";
  13. import { isObject } from '@/utils/type-guards';
  14. import LoadingDots from "@components/common/icons/LoadingDots";
  15. import { logoutAction } from "@utils/actions";
  16. import { useAppDispatch } from "@/store/hooks";
  17. import { clearCart } from "@/store/slices/cart-slice";
  18. import {
  19. setCookie,
  20. deleteGuestCookie
  21. } from "@utils/cookie-tools";
  22. import { IS_GUEST } from "@/utils/constants";
  23. export default function CredentialModal({
  24. children,
  25. className,
  26. onOpen,
  27. onClose,
  28. isOpen,
  29. }: {
  30. children?: React.ReactNode;
  31. className?: string;
  32. onOpen?: () => void;
  33. onClose?: () => void;
  34. isOpen?: boolean;
  35. }) {
  36. const {
  37. isOpen: internalIsOpen,
  38. onOpen: internalOnOpen,
  39. onClose: internalOnClose,
  40. onOpenChange: _internalOnOpenChange,
  41. } = useDisclosure();
  42. const isControlled = isOpen !== undefined;
  43. const finalIsOpen = isControlled ? isOpen : internalIsOpen;
  44. const finalOnOpen = isControlled ? onOpen : internalOnOpen;
  45. const finalOnClose = isControlled ? onClose : internalOnClose;
  46. const pathname = usePathname();
  47. const router = useRouter();
  48. const dispatch = useAppDispatch();
  49. const { showToast } = useCustomToast();
  50. useBodyScrollLock(finalIsOpen );
  51. const {
  52. handleSubmit,
  53. formState: { isSubmitting },
  54. } = useForm();
  55. const {data: session} = useSession();
  56. const onSubmit = async () => {
  57. try {
  58. const res = await logoutAction();
  59. if (!res.success) {
  60. showToast(res.message, "danger");
  61. }
  62. await signOut({
  63. callbackUrl: "/customer/login",
  64. redirect: false,
  65. });
  66. deleteGuestCookie(); // 这里本质是要删除登录用户的token
  67. setCookie(IS_GUEST, 'true');
  68. dispatch(clearCart());
  69. showToast("You are logged out successfully!", "success");
  70. setTimeout(() => {
  71. router.push("/customer/login");
  72. router.refresh();
  73. }, 100);
  74. } catch (err: unknown) {
  75. const message = err instanceof Error ? err.message : "Logout failed";
  76. showToast(message, "danger");
  77. }
  78. };
  79. const innerContent = (_onClose?: () => void) => (
  80. <div className={clsx("flex w-full flex-col rounded-md py-4", {
  81. "gap-y-6": !!session?.user,
  82. "gap-y-10": !session?.user,
  83. })}>
  84. {isObject(session?.user) ? (
  85. <>
  86. <header>
  87. <div className={clsx("flex flex-col gap-3", "items-center justify-center")}>
  88. <div className={clsx("flex gap-3", "flex-col items-center")}>
  89. <Avatar
  90. isBordered
  91. showFallback
  92. color="default"
  93. icon={<OpenAuth className={clsx("h-12 w-12")} />}
  94. size={"lg"}
  95. className={clsx( "h-24 w-24 text-large")}
  96. />
  97. <div className={clsx("flex flex-col justify-center", "items-center gap-1")}>
  98. <h4 className={clsx("leading-none dark:text-white", "text-xl font-bold text-black")}>
  99. {session?.user?.name}
  100. </h4>
  101. <h5 className={clsx("tracking-tight dark:text-white", "text-sm text-gray-500")}>
  102. {session?.user?.email}
  103. </h5>
  104. </div>
  105. </div>
  106. <p className={clsx("text-default-500 dark:text-white", "text-center mt-2")}>
  107. Manage Cart, Orders
  108. <span aria-label="confetti" className="px-2" role="img">
  109. 🎉
  110. </span>
  111. </p>
  112. </div>
  113. </header>
  114. <footer>
  115. <form onSubmit={handleSubmit(onSubmit)} className={clsx("flex justify-center")}>
  116. <button
  117. className={clsx(
  118. "rounded-full bg-gray-800 px-5 py-2.5 text-sm font-medium text-white hover:bg-gray-900 focus:outline-none focus:ring-4 focus:ring-gray-300 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700 dark:focus:ring-gray-700",
  119. isSubmitting ? " cursor-not-allowed" : " cursor-pointer",
  120. "w-40 min-w-[150px] mt-2"
  121. )}
  122. type="submit"
  123. >
  124. <div className="mx-1">
  125. {isSubmitting ? (
  126. <div className="flex items-center justify-center">
  127. <p>Loading</p>
  128. <LoadingDots className="bg-white" />
  129. </div>
  130. ) : (
  131. <p> Log Out</p>
  132. )}
  133. </div>
  134. </button>
  135. </form>
  136. </footer>
  137. </>
  138. ) : (
  139. <>
  140. <header className="text-center">
  141. <div className="flex flex-col gap-y-2">
  142. <h4 className="font-bold leading-none text-black dark:text-white text-3xl">
  143. Welcome Guest
  144. </h4>
  145. <p className="text-default-500 dark:text-neutral-400 text-lg">
  146. Manage Cart, Orders
  147. <span aria-label="confetti" className="px-2" role="img">
  148. 🎉
  149. </span>
  150. </p>
  151. </div>
  152. </header>
  153. <footer className="flex gap-4">
  154. <Link className="w-full" href="/customer/login" onClick={finalOnClose} aria-label="Go to sign in page">
  155. <button
  156. className={clsx(
  157. "w-full rounded-full bg-blue-600 px-5 py-3 text-center text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-4 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800",
  158. pathname === "/customer/login"
  159. ? " cursor-not-allowed"
  160. : " cursor-pointer"
  161. )}
  162. disabled={pathname === "/customer/login"}
  163. type="button"
  164. >
  165. Sign In
  166. </button>
  167. </Link>
  168. <Link className="w-full" href="/customer/register" onClick={finalOnClose} aria-label="Go to create account page">
  169. <button
  170. className={clsx(
  171. "w-full rounded-full bg-[#1e293b] px-5 py-3 text-sm font-medium text-white hover:bg-gray-900 focus:outline-none focus:ring-4 focus:ring-gray-300 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700 dark:focus:ring-gray-700",
  172. pathname === "/customer/register"
  173. ? " cursor-not-allowed"
  174. : " cursor-pointer"
  175. )}
  176. disabled={pathname === "/customer/register"}
  177. type="button"
  178. >
  179. Sign Up
  180. </button>
  181. </Link>
  182. </footer>
  183. </>
  184. )}
  185. </div>
  186. );
  187. return (
  188. <>
  189. <button
  190. type="button"
  191. aria-label="Open account"
  192. className={clsx(className, "cursor-pointer bg-transparent")}
  193. onClick={finalOnOpen}
  194. >
  195. {children ? children : <OpenAuth />}
  196. </button>
  197. <AnimatePresence>
  198. {finalIsOpen && (
  199. <>
  200. <motion.div
  201. initial={{ opacity: 0 }}
  202. animate={{ opacity: 1 }}
  203. exit={{ opacity: 0 }}
  204. onClick={finalOnClose}
  205. className="fixed inset-0 z-40 bg-transparent"
  206. style={{ top: "68px", bottom: "64px" }}
  207. />
  208. <motion.div
  209. initial={{ x: "100%" }}
  210. animate={{ x: 0 }}
  211. exit={{ x: "100%" }}
  212. transition={{ type: "spring", damping: 30, stiffness: 300, mass: 0.8 }}
  213. className="fixed right-0 z-50 flex flex-col border-l border-neutral-200 bg-white dark:border-neutral-800 dark:bg-black lg:hidden"
  214. style={{
  215. top: "68px",
  216. bottom: "64px",
  217. width: "100%",
  218. maxWidth: "448px",
  219. height: "calc(var(--visual-viewport-height) - 132px)",
  220. }}
  221. >
  222. <div className="flex flex-col gap-1 border-b border-neutral-100 p-4 dark:border-neutral-800">
  223. <div className="flex items-center justify-between">
  224. <p className="text-xl font-semibold dark:text-white">Account</p>
  225. <button
  226. aria-label="Close account"
  227. className="rounded-full p-1 hover:bg-neutral-100 "
  228. onClick={finalOnClose}
  229. type="button"
  230. >
  231. <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>
  232. </button>
  233. </div>
  234. </div>
  235. <div className="flex flex-1 flex-col justify-center px-4 py-0 drawer-scrollbar-hidden">
  236. {innerContent(finalOnClose)}
  237. </div>
  238. <div className="p-4" />
  239. </motion.div>
  240. </>
  241. )}
  242. </AnimatePresence>
  243. </>
  244. );
  245. }