CredentialModal.tsx 9.6 KB

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