diff --git a/client/src/Pages/Auth/Login/index.jsx b/client/src/Pages/Auth/Login/index.jsx index d6b281c27..ed9f2e0ff 100644 --- a/client/src/Pages/Auth/Login/index.jsx +++ b/client/src/Pages/Auth/Login/index.jsx @@ -3,84 +3,35 @@ import Stack from "@mui/material/Stack"; import Button from "@mui/material/Button"; import TextInput from "../../../Components/Inputs/TextInput"; import { PasswordEndAdornment } from "../../../Components/Inputs/TextInput/Adornments"; -import { loginCredentials } from "../../../Validation/validation"; import TextLink from "../../../Components/TextLink"; import AuthPageWrapper from "../components/AuthPageWrapper"; // Utils -import { login } from "../../../Features/Auth/authSlice"; -import { useNavigate } from "react-router-dom"; -import { useDispatch } from "react-redux"; -import { useState } from "react"; import { useTheme } from "@mui/material/styles"; import { useTranslation } from "react-i18next"; -import { createToast } from "../../../Utils/toastUtils"; +import useLoginForm from "../hooks/useLoginForm"; +import useValidateLoginForm from "../hooks/useValidateLoginForm"; +import useLoginSubmit from "../hooks/useLoginSubmit"; +import useLoadingSubmit from "../hooks/useLoadingSubmit"; const Login = () => { - // Local state - const [form, setForm] = useState({ - email: "", - password: "", - }); - const [errors, setErrors] = useState({ - email: "", - password: "", - }); - // Hooks const { t } = useTranslation(); const theme = useTheme(); - const dispatch = useDispatch(); - const navigate = useNavigate(); + const [form, onChange] = useLoginForm(); + const [errors, setErrors, validateField, validateForm] = useValidateLoginForm(); + const [handleLoginSubmit] = useLoginSubmit(); + const { submitting, executeSubmit } = useLoadingSubmit(); // Handlers - const onChange = (e) => { - let { name, value } = e.target; - if (name === "email") { - value = value.toLowerCase(); - } - const updatedForm = { ...form, [name]: value }; - const { error } = loginCredentials.validate({ [name]: value }); - setForm(updatedForm); - setErrors((prev) => ({ - ...prev, - [name]: error?.details?.[0]?.message || "", - })); + const handleChange = (e) => { + onChange(e); + validateField(e.target.name, e.target.value); }; const onSubmit = async (e) => { e.preventDefault(); - const toSubmit = { ...form }; - const { error } = loginCredentials.validate(toSubmit, { abortEarly: false }); - if (error) { - const formErrors = {}; - for (const err of error.details) { - formErrors[err.path[0]] = err.message; - } - setErrors(formErrors); - return; - } - const action = await dispatch(login(form)); - if (action.payload.success) { - navigate("/uptime"); - createToast({ - body: t("auth.login.toasts.success"), - }); - } else { - if (action.payload) { - if (action.payload.msg === "Incorrect password") - setErrors({ - password: t("auth.login.errors.password.incorrect"), - }); - // dispatch errors - createToast({ - body: t("auth.login.toasts.incorrectPassword"), - }); - } else { - // unknown errors - createToast({ - body: t("common.toasts.unknownError"), - }); - } - } + const isValid = validateForm(form); + if (!isValid) return; + await executeSubmit(() => handleLoginSubmit(form, setErrors)); }; return ( { placeholder={t("auth.common.inputs.email.placeholder")} autoComplete="email" value={form.email} - onChange={onChange} + onChange={handleChange} error={errors.email ? true : false} helperText={errors.email ? t(errors.email) : ""} // Localization keys are in validation.js /> @@ -120,7 +71,7 @@ const Login = () => { placeholder="••••••••••" autoComplete="current-password" value={form.password} - onChange={onChange} + onChange={handleChange} error={errors.password ? true : false} helperText={errors.password ? t(errors.password) : ""} // Localization keys are in validation.js endAdornment={} @@ -130,6 +81,7 @@ const Login = () => { color="accent" type="submit" sx={{ width: "100%", alignSelf: "center", fontWeight: 700 }} + loading={submitting} > Login @@ -147,5 +99,4 @@ const Login = () => { ); }; - export default Login; diff --git a/client/src/Pages/Auth/hooks/useLoadingSubmit.jsx b/client/src/Pages/Auth/hooks/useLoadingSubmit.jsx new file mode 100644 index 000000000..a779165b6 --- /dev/null +++ b/client/src/Pages/Auth/hooks/useLoadingSubmit.jsx @@ -0,0 +1,22 @@ +// Hook to avoid double submits and manage loading state +import { useState, useCallback } from "react"; + +const useLoadingSubmit = () => { + const [submitting, setSubmitting] = useState(false); + const executeSubmit = useCallback( + async (submitFunction) => { + if (submitting) return; + setSubmitting(true); + try { + return await submitFunction(); + } finally { + setSubmitting(false); + } + }, + [submitting] + ); + + return { submitting, executeSubmit }; +}; + +export default useLoadingSubmit; diff --git a/client/src/Pages/Auth/hooks/useLoginForm.jsx b/client/src/Pages/Auth/hooks/useLoginForm.jsx new file mode 100644 index 000000000..61814d723 --- /dev/null +++ b/client/src/Pages/Auth/hooks/useLoginForm.jsx @@ -0,0 +1,19 @@ +import { useState } from "react"; + +const useLoginForm = () => { + const [form, setForm] = useState({ + email: "", + password: "", + }); + const onChange = (e) => { + let { name, value } = e.target; + if (name === "email") { + value = value.toLowerCase(); + } + const updatedForm = { ...form, [name]: value }; + setForm(updatedForm); + }; + return [form, onChange]; +}; + +export default useLoginForm; diff --git a/client/src/Pages/Auth/hooks/useLoginSubmit.jsx b/client/src/Pages/Auth/hooks/useLoginSubmit.jsx new file mode 100644 index 000000000..e15cd98a0 --- /dev/null +++ b/client/src/Pages/Auth/hooks/useLoginSubmit.jsx @@ -0,0 +1,41 @@ +import { useDispatch } from "react-redux"; +import { login } from "../../../Features/Auth/authSlice"; +import { useNavigate } from "react-router-dom"; +import { createToast } from "../../../Utils/toastUtils"; +import { useTranslation } from "react-i18next"; + +const useLoginSubmit = () => { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const { t } = useTranslation(); + + const handleLoginSubmit = async (form, setErrors) => { + const action = await dispatch(login(form)); + if (action.payload.success) { + navigate("/uptime"); + createToast({ + body: t("auth.login.toasts.success"), + }); + } else { + if (action.payload) { + console.log(action.payload); + if (action.payload.msg === "Incorrect password") + setErrors({ + password: t("auth.login.errors.password.incorrect"), + }); + // dispatch errors + createToast({ + body: t("auth.login.toasts.incorrectPassword"), + }); + } else { + // unknown errors + createToast({ + body: t("common.toasts.unknownError"), + }); + } + } + }; + return [handleLoginSubmit]; +}; + +export default useLoginSubmit; diff --git a/client/src/Pages/Auth/hooks/useValidateLoginForm.jsx b/client/src/Pages/Auth/hooks/useValidateLoginForm.jsx new file mode 100644 index 000000000..f50c777f3 --- /dev/null +++ b/client/src/Pages/Auth/hooks/useValidateLoginForm.jsx @@ -0,0 +1,31 @@ +import { useState } from "react"; +import { loginCredentials } from "../../../Validation/validation"; + +const useValidateLoginForm = () => { + const [errors, setErrors] = useState({ + email: "", + password: "", + }); + const validateField = (name, value) => { + const { error } = loginCredentials.validate({ [name]: value }); + setErrors((prev) => ({ + ...prev, + [name]: error?.details?.[0]?.message || "", + })); + }; + const validateForm = (form) => { + const { error } = loginCredentials.validate(form, { abortEarly: false }); + if (error) { + const formErrors = {}; + for (const err of error.details) { + formErrors[err.path[0]] = err.message; + } + setErrors(formErrors); + return false; + } + return true; + }; + return [errors, setErrors, validateField, validateForm]; +}; + +export default useValidateLoginForm;