From 3b4a8c34a1e04dc9ec7ddafc6580bbd6e8f097c6 Mon Sep 17 00:00:00 2001 From: Fingertips Date: Wed, 25 Sep 2024 23:39:32 +0800 Subject: [PATCH] Refactor auth store to selectively persist properties and manage global loading state to actions - Updated the Zustand auth store to persist only specific properties (`email` and `authorized`) while excluding others to optimize storage. - Implemented a global loading state to provide a consistent user experience across all authentication-related actions, enhancing feedback during loading processes. - Ensured that the loading state reflects the ongoing status of user actions, allowing for better UI responsiveness and user engagement. --- client/package.json | 2 +- client/src/App.tsx | 2 -- client/src/components/switch-auth.tsx | 8 +++-- client/src/lib/stores/auth-store.ts | 36 ++++++++++++------- .../_components/forgot-password-back.tsx | 11 ++++++ .../_components/forgot-password-form.tsx | 10 +++++- client/src/pages/forgot-password/page.tsx | 8 ++--- client/src/pages/resend-verify/page.tsx | 11 ------ .../_components/reset-password-back.tsx | 11 ++++++ .../_components/reset-password-form.tsx | 11 +++++- client/src/pages/reset-password/page.tsx | 5 ++- .../sign-in/_components/no-account-yet.tsx | 18 ++++++++++ .../sign-in/_components/sign-in-form.tsx | 11 +++++- client/src/pages/sign-in/page.tsx | 9 ++--- .../_components/already-have-account.tsx | 18 ++++++++++ .../sign-up/_components/sign-up-form.tsx | 10 +++++- client/src/pages/sign-up/page.tsx | 9 ++--- .../verify-email/_components/resend-code.tsx | 27 ++++++++++---- .../_components/verify-email-form.tsx | 19 +++++++--- 19 files changed, 172 insertions(+), 64 deletions(-) create mode 100644 client/src/pages/forgot-password/_components/forgot-password-back.tsx delete mode 100644 client/src/pages/resend-verify/page.tsx create mode 100644 client/src/pages/reset-password/_components/reset-password-back.tsx create mode 100644 client/src/pages/sign-in/_components/no-account-yet.tsx create mode 100644 client/src/pages/sign-up/_components/already-have-account.tsx diff --git a/client/package.json b/client/package.json index 5c29965..39bd94f 100644 --- a/client/package.json +++ b/client/package.json @@ -1,5 +1,5 @@ { - "name": "client", + "name": "go-react-auth-client", "private": true, "version": "1.0.0", "type": "module", diff --git a/client/src/App.tsx b/client/src/App.tsx index 85c4508..30811c4 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -2,7 +2,6 @@ import { Route, Routes } from "react-router-dom"; import { ForgotPasswordPage } from "@/pages/forgot-password/page"; import { ResetPasswordPage } from "@/pages/reset-password/page"; -import { ResendVerifyPage } from "@/pages/resend-verify/page"; import VerifyEmailPage from "@/pages/verify-email/page"; import PrivateGuard from "@/guards/private-guard"; import { AppRoutes } from "@/constants/routes"; @@ -22,7 +21,6 @@ function App() { } /> } /> } /> - } /> { +const SwitchAuth = ({ label, tag, href, disabled }: SwitchAuthProps) => { return (
{label}

} {tag} diff --git a/client/src/lib/stores/auth-store.ts b/client/src/lib/stores/auth-store.ts index 3581a56..2532eab 100644 --- a/client/src/lib/stores/auth-store.ts +++ b/client/src/lib/stores/auth-store.ts @@ -1,4 +1,4 @@ -import { createJSONStorage, persist } from "zustand/middleware"; +import { createJSONStorage, devtools, persist } from "zustand/middleware"; import { create } from "zustand"; interface AuthStoreState { @@ -6,19 +6,29 @@ interface AuthStoreState { setEmail: (email: string) => void; authorized: boolean; setAuthorized: (authorized: boolean) => void; + loading: boolean; + setLoading: (loading: boolean) => void; } -export const useAuthStore = create( - persist( - (set) => ({ - email: "", - setEmail: (email: string) => set({ email }), - authorized: false, - setAuthorized: (authorized: boolean) => set({ authorized }), - }), - { - name: "email", - storage: createJSONStorage(() => sessionStorage), - } +export const useAuthStore = create()( + devtools( + persist( + (set) => ({ + email: "", + setEmail: (email: string) => set({ email }), + authorized: false, + setAuthorized: (authorized: boolean) => set({ authorized }), + loading: false, + setLoading: (loading: boolean) => set({ loading }), + }), + { + name: "go-react-auth", + partialize: (state) => ({ + email: state.email, + authorized: state.authorized, + }), + storage: createJSONStorage(() => sessionStorage), + } + ) ) ); diff --git a/client/src/pages/forgot-password/_components/forgot-password-back.tsx b/client/src/pages/forgot-password/_components/forgot-password-back.tsx new file mode 100644 index 0000000..33c53a1 --- /dev/null +++ b/client/src/pages/forgot-password/_components/forgot-password-back.tsx @@ -0,0 +1,11 @@ +import { useAuthStore } from "@/lib/stores/auth-store"; +import { SwitchAuth } from "@/components/switch-auth"; +import { AppRoutes } from "@/constants/routes"; + +const ForgotPasswordBack = () => { + const { loading } = useAuthStore(); + + return ; +}; + +export { ForgotPasswordBack }; diff --git a/client/src/pages/forgot-password/_components/forgot-password-form.tsx b/client/src/pages/forgot-password/_components/forgot-password-form.tsx index a841d7e..7696de5 100644 --- a/client/src/pages/forgot-password/_components/forgot-password-form.tsx +++ b/client/src/pages/forgot-password/_components/forgot-password-form.tsx @@ -7,21 +7,28 @@ import { GenericResponse } from "@/lib/classes/generic-response-class"; import { ErrorResponse } from "@/lib/classes/error-response-class"; import { AuthService } from "@/lib/services/auth-service"; import { ValidateEmail } from "@/lib/utils/validations"; +import { useAuthStore } from "@/lib/stores/auth-store"; import { FORGOTPASSWORDKEY } from "@/constants/keys"; import { Button } from "@/components/text-button"; import { Input } from "@/components/input"; const ForgotPasswordForm = () => { + const { setLoading: setGlobalLoading } = useAuthStore(); const [submitted, setSubmitted] = useState(false); const [email, setEmail] = useState(""); + const { mutate, isPending } = useMutation({ mutationKey: [FORGOTPASSWORDKEY], mutationFn: AuthService.forgotPassword, onSuccess: (res: GenericResponse) => { toast.success(res.message); setSubmitted(true); + setGlobalLoading(false); + }, + onError: (error: ErrorResponse) => { + toast.error(error.message); + setGlobalLoading(false); }, - onError: (error: ErrorResponse) => toast.error(error.message), }); const onSubmit = (e: FormEvent) => { @@ -34,6 +41,7 @@ const ForgotPasswordForm = () => { const emailData = forgotPasswordData["email"] as string; setEmail(emailData); + setGlobalLoading(true); mutate(emailData); }; diff --git a/client/src/pages/forgot-password/page.tsx b/client/src/pages/forgot-password/page.tsx index bd4dbe2..ab82818 100644 --- a/client/src/pages/forgot-password/page.tsx +++ b/client/src/pages/forgot-password/page.tsx @@ -1,8 +1,8 @@ -import { SwitchAuth } from "@/components/switch-auth"; -import { ForgotPasswordForm } from "./_components/forgot-password-form"; -import { AppRoutes } from "@/constants/routes"; import { AuthTitle } from "@/components/auth-title"; +import { ForgotPasswordForm } from "./_components/forgot-password-form"; +import { ForgotPasswordBack } from "./_components/forgot-password-back"; + const ForgotPasswordPage = () => { return (
@@ -13,7 +13,7 @@ const ForgotPasswordPage = () => { - +
); }; diff --git a/client/src/pages/resend-verify/page.tsx b/client/src/pages/resend-verify/page.tsx deleted file mode 100644 index 595a57e..0000000 --- a/client/src/pages/resend-verify/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { AuthTitle } from "@/components/auth-title"; - -const ResendVerifyPage = () => { - return ( -
- -
- ); -}; - -export { ResendVerifyPage }; diff --git a/client/src/pages/reset-password/_components/reset-password-back.tsx b/client/src/pages/reset-password/_components/reset-password-back.tsx new file mode 100644 index 0000000..83b0ae0 --- /dev/null +++ b/client/src/pages/reset-password/_components/reset-password-back.tsx @@ -0,0 +1,11 @@ +import { useAuthStore } from "@/lib/stores/auth-store"; +import { SwitchAuth } from "@/components/switch-auth"; +import { AppRoutes } from "@/constants/routes"; + +const ResetPasswordBack = () => { + const { loading } = useAuthStore(); + + return ; +}; + +export { ResetPasswordBack }; diff --git a/client/src/pages/reset-password/_components/reset-password-form.tsx b/client/src/pages/reset-password/_components/reset-password-form.tsx index cfc939b..2e121e2 100644 --- a/client/src/pages/reset-password/_components/reset-password-form.tsx +++ b/client/src/pages/reset-password/_components/reset-password-form.tsx @@ -7,6 +7,7 @@ import { GenericResponse } from "@/lib/classes/generic-response-class"; import { ErrorResponse } from "@/lib/classes/error-response-class"; import { RESET_PASSWORD_INPUTS } from "@/constants/collections"; import { AuthService } from "@/lib/services/auth-service"; +import { useAuthStore } from "@/lib/stores/auth-store"; import { RESETPASSWORDKEY } from "@/constants/keys"; import { Button } from "@/components/text-button"; import { AppRoutes } from "@/constants/routes"; @@ -14,17 +15,23 @@ import { ResetDTO } from "@/lib/DTO/reset-dto"; import { Input } from "@/components/input"; const ResetPasswordForm = () => { + const { setLoading: setGlobalLoading } = useAuthStore(); const navigate = useNavigate(); const { token } = useParams(); const [confirmPassword, setConfirmPassword] = useState(""); + const { mutate, isPending } = useMutation({ mutationKey: [RESETPASSWORDKEY], mutationFn: AuthService.resetPassword, onSuccess: (res: GenericResponse) => { toast.success(res.message); + setGlobalLoading(false); navigate(AppRoutes.SignIn); }, - onError: (error: ErrorResponse) => toast.error(error.message), + onError: (error: ErrorResponse) => { + toast.error(error.message); + setGlobalLoading(false); + }, }); const onSubmit = (e: FormEvent) => { @@ -40,6 +47,8 @@ const ResetPasswordForm = () => { resetPasswordData.token = token; + setGlobalLoading(true); + mutate(resetPasswordData); }; diff --git a/client/src/pages/reset-password/page.tsx b/client/src/pages/reset-password/page.tsx index 91ede2b..621a711 100644 --- a/client/src/pages/reset-password/page.tsx +++ b/client/src/pages/reset-password/page.tsx @@ -1,8 +1,7 @@ -import { SwitchAuth } from "@/components/switch-auth"; import { AuthTitle } from "@/components/auth-title"; -import { AppRoutes } from "@/constants/routes"; import { ResetPasswordForm } from "./_components/reset-password-form"; +import { ResetPasswordBack } from "./_components/reset-password-back"; const ResetPasswordPage = () => { return ( @@ -11,7 +10,7 @@ const ResetPasswordPage = () => { - + ); }; diff --git a/client/src/pages/sign-in/_components/no-account-yet.tsx b/client/src/pages/sign-in/_components/no-account-yet.tsx new file mode 100644 index 0000000..14c0704 --- /dev/null +++ b/client/src/pages/sign-in/_components/no-account-yet.tsx @@ -0,0 +1,18 @@ +import { useAuthStore } from "@/lib/stores/auth-store"; +import { SwitchAuth } from "@/components/switch-auth"; +import { AppRoutes } from "@/constants/routes"; + +const NoAccountYet = () => { + const { loading } = useAuthStore(); + + return ( + + ); +}; + +export { NoAccountYet }; diff --git a/client/src/pages/sign-in/_components/sign-in-form.tsx b/client/src/pages/sign-in/_components/sign-in-form.tsx index 3ed588d..afdb1fa 100644 --- a/client/src/pages/sign-in/_components/sign-in-form.tsx +++ b/client/src/pages/sign-in/_components/sign-in-form.tsx @@ -16,21 +16,29 @@ import { Input } from "@/components/input"; const SignInForm = () => { const navigate = useNavigate(); - const { setEmail, setAuthorized } = useAuthStore(); + const { + setEmail, + setAuthorized, + setLoading: setGlobalLoading, + } = useAuthStore(); + const { mutate, isPending } = useMutation({ mutationKey: [SIGNINKEY], mutationFn: AuthService.signIn, onSuccess: (res: GenericResponse) => { toast.success(res.message); setAuthorized(true); + setGlobalLoading(false); }, onError: (error: ErrorResponse) => { if (error.status == 403) { toast.error("Please verify to sign in"); + setGlobalLoading(false); navigate(AppRoutes.VerifyEmail); } else { toast.error(error.message); setAuthorized(false); + setGlobalLoading(false); } }, }); @@ -43,6 +51,7 @@ const SignInForm = () => { const signInData = Object.fromEntries(formData.entries()) as SignInDTO; setEmail(signInData.email); + setGlobalLoading(true); mutate(signInData); }; diff --git a/client/src/pages/sign-in/page.tsx b/client/src/pages/sign-in/page.tsx index 10837dd..f8d4317 100644 --- a/client/src/pages/sign-in/page.tsx +++ b/client/src/pages/sign-in/page.tsx @@ -1,8 +1,7 @@ -import { SwitchAuth } from "@/components/switch-auth"; import { AuthTitle } from "@/components/auth-title"; -import { AppRoutes } from "@/constants/routes"; import { Or } from "@/components/or"; +import { NoAccountYet } from "./_components/no-account-yet"; import { SignInForm } from "./_components/sign-in-form"; const SignInPage = () => { @@ -14,11 +13,7 @@ const SignInPage = () => { - + ); }; diff --git a/client/src/pages/sign-up/_components/already-have-account.tsx b/client/src/pages/sign-up/_components/already-have-account.tsx new file mode 100644 index 0000000..50e7999 --- /dev/null +++ b/client/src/pages/sign-up/_components/already-have-account.tsx @@ -0,0 +1,18 @@ +import { useAuthStore } from "@/lib/stores/auth-store"; +import { SwitchAuth } from "@/components/switch-auth"; +import { AppRoutes } from "@/constants/routes"; + +const AlreadyHaveAccount = () => { + const { loading } = useAuthStore(); + + return ( + + ); +}; + +export { AlreadyHaveAccount }; diff --git a/client/src/pages/sign-up/_components/sign-up-form.tsx b/client/src/pages/sign-up/_components/sign-up-form.tsx index 338d88e..2c296ca 100644 --- a/client/src/pages/sign-up/_components/sign-up-form.tsx +++ b/client/src/pages/sign-up/_components/sign-up-form.tsx @@ -6,6 +6,7 @@ import { toast } from "sonner"; import { ErrorResponse } from "@/lib/classes/error-response-class"; import { AuthService } from "@/lib/services/auth-service"; import { SIGNUP_INPUTS } from "@/constants/collections"; +import { useAuthStore } from "@/lib/stores/auth-store"; import { SignUpDTO } from "@/lib/DTO/sign-up-dto"; import { Button } from "@/components/text-button"; import { AppRoutes } from "@/constants/routes"; @@ -13,6 +14,7 @@ import { SIGNUPKEY } from "@/constants/keys"; import { Input } from "@/components/input"; const SignUpForm = () => { + const { setLoading: setGlobalLoading } = useAuthStore(); const [confirmPassword, setConfirmPassword] = useState(""); const navigate = useNavigate(); @@ -21,9 +23,13 @@ const SignUpForm = () => { mutationFn: AuthService.signUp, onSuccess: () => { toast.success("Registered successfully"); + setGlobalLoading(false); navigate(AppRoutes.SignIn); }, - onError: (error: ErrorResponse) => toast.error(error.message), + onError: (error: ErrorResponse) => { + toast.error(error.message); + setGlobalLoading(false); + }, }); const onSubmit = (e: FormEvent) => { @@ -33,6 +39,8 @@ const SignUpForm = () => { const signUpData = Object.fromEntries(formData.entries()) as SignUpDTO; + setGlobalLoading(true); + mutate(signUpData); }; diff --git a/client/src/pages/sign-up/page.tsx b/client/src/pages/sign-up/page.tsx index 2cf996c..7aef38e 100644 --- a/client/src/pages/sign-up/page.tsx +++ b/client/src/pages/sign-up/page.tsx @@ -1,8 +1,7 @@ -import { SwitchAuth } from "@/components/switch-auth"; import { AuthTitle } from "@/components/auth-title"; -import { AppRoutes } from "@/constants/routes"; import { Or } from "@/components/or"; +import { AlreadyHaveAccount } from "./_components/already-have-account"; import { SignUpForm } from "./_components/sign-up-form"; const SignUpPage = () => { @@ -14,11 +13,7 @@ const SignUpPage = () => { - + ); }; diff --git a/client/src/pages/verify-email/_components/resend-code.tsx b/client/src/pages/verify-email/_components/resend-code.tsx index 83c8a16..c83dba2 100644 --- a/client/src/pages/verify-email/_components/resend-code.tsx +++ b/client/src/pages/verify-email/_components/resend-code.tsx @@ -8,16 +8,31 @@ import { useAuthStore } from "@/lib/stores/auth-store"; import { RESENDVERIFYKEY } from "@/constants/keys"; const ResendCode = () => { - const { email } = useAuthStore(); + const { + email, + loading: globalLoading, + setLoading: setGlobalLoading, + } = useAuthStore(); const { mutate, isPending } = useMutation({ mutationKey: [RESENDVERIFYKEY], mutationFn: AuthService.resendVerify, - onSuccess: (res: GenericResponse) => toast.success(res.message), - onError: (error: ErrorResponse) => toast.error(error.message), + onSuccess: (res: GenericResponse) => { + toast.success(res.message); + setGlobalLoading(false); + }, + onError: (error: ErrorResponse) => { + toast.error(error.message); + setGlobalLoading(false); + }, }); - const onClick = () => mutate(email); + const onClick = () => { + setGlobalLoading(true); + mutate(email); + }; + + const loading = isPending || globalLoading; return (
{ disabled:text-secondary/50 disabled:pointer-events-none" type="button" onClick={onClick} - disabled={isPending} + disabled={loading} > - {isPending ? "Resending..." : "Resend"} + {loading ? "Resending..." : "Resend"}
); diff --git a/client/src/pages/verify-email/_components/verify-email-form.tsx b/client/src/pages/verify-email/_components/verify-email-form.tsx index fca6e28..2e78950 100644 --- a/client/src/pages/verify-email/_components/verify-email-form.tsx +++ b/client/src/pages/verify-email/_components/verify-email-form.tsx @@ -6,6 +6,7 @@ import { toast } from "sonner"; import { GenericResponse } from "@/lib/classes/generic-response-class"; import { ErrorResponse } from "@/lib/classes/error-response-class"; import { AuthService } from "@/lib/services/auth-service"; +import { useAuthStore } from "@/lib/stores/auth-store"; import { Button } from "@/components/text-button"; import { VERIFYEMAILKEY } from "@/constants/keys"; import { AppRoutes } from "@/constants/routes"; @@ -13,6 +14,8 @@ import { AppRoutes } from "@/constants/routes"; import { SingleInput } from "./single-input"; const VerifyEmailForm = () => { + const { loading: globalLoading, setLoading: setGlobalLoading } = + useAuthStore(); const [code, setCode] = useState(["", "", "", ""]); const inputRef = useRef([]); const formRef = useRef(null); @@ -23,9 +26,13 @@ const VerifyEmailForm = () => { mutationFn: AuthService.verifyEmail, onSuccess: (res: GenericResponse) => { toast.success(res.message); + setGlobalLoading(false); navigate(AppRoutes.SignIn); }, - onError: (error: ErrorResponse) => toast.error(error.message), + onError: (error: ErrorResponse) => { + toast.error(error.message); + setGlobalLoading(false); + }, }); const onChange = (value: string, index: number) => { @@ -73,6 +80,8 @@ const VerifyEmailForm = () => { const verificationCode = code.join(""); + setGlobalLoading(true); + mutate(verificationCode); }; @@ -83,6 +92,8 @@ const VerifyEmailForm = () => { } }, [code]); + const loading = isPending || globalLoading; + return (
{ digit={digit} onChange={(e) => onChange(e.target.value, i)} onKeyDown={(e) => onKeyDown(e, i)} - disabled={isPending} + disabled={loading} /> ))}