diff --git a/src/App.tsx b/src/App.tsx index 6e212ac..300c26e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -21,6 +21,8 @@ import Messages from './pages/admin/Messages'; import Settings from './pages/admin/Settings'; import CategoriesPage from './pages/CategoriesPage'; +import ResetPassword from './pages/ResetPassword'; +import NewPassword from './pages/NewPassword'; const App = () => { const { data, error, isLoading } = useGetProductsQuery(); const dispatch = useDispatch(); @@ -69,7 +71,17 @@ const App = () => { { path: 'categories/:categoryId', element: , - } + }, + { + path: '/reset-password', + children: [ + { path: '', element: }, + { + path: ':token', + element: , + }, + ], + }, ], }, { diff --git a/src/components/authentication/LoginComponent.tsx b/src/components/authentication/LoginComponent.tsx index 38cc55c..09b179c 100644 --- a/src/components/authentication/LoginComponent.tsx +++ b/src/components/authentication/LoginComponent.tsx @@ -6,7 +6,7 @@ import GoogleIcon from '../../assets/googleIcon.svg'; import Button from '../common/Button'; import Input from '../common/Input'; import { loginSchema, LoginData } from '../../utils/schemas'; -import { useNavigate, useLocation, useSearchParams } from 'react-router-dom'; +import { useNavigate, useLocation, useSearchParams, Link } from 'react-router-dom'; import { useSelector, useDispatch } from 'react-redux'; import { setToken, setUser, setRole } from '../../redux/slices/userSlice'; import { useLoginUserMutation } from '../../services/authAPI'; @@ -120,7 +120,9 @@ const LoginComponent = () => { {...register('password')} error={errors.password && errors.password.message} /> -

Forget password

+ +

Forget password

+ - - Email is not valid - - - -

© 2024 Mavericks Shop. All rights reserved.

+ return ( + <> +
+
+
+ +
+

K309 St , Makuza plaza, Nyarugenge , Kigali, Rwanda

+

andela.mavericks@gmail.com

+

+250 788888888

- - ) + +
+
+ +
+ + +
+
+
+ +
+ + + + + +
+
+
+ +
+ + + + +
+
+
+ +

+ Be the first to get latest news about trends,Promotions and many more. +

+
+
+ + + +
+ Email is not valid +
+
+
+

+ © 2024 Mavericks Shop. All rights reserved. +

+
+ + ); } -export default Footer +export default Footer; diff --git a/src/components/passwordReset/NewPasswordComponent.tsx b/src/components/passwordReset/NewPasswordComponent.tsx new file mode 100644 index 0000000..857f564 --- /dev/null +++ b/src/components/passwordReset/NewPasswordComponent.tsx @@ -0,0 +1,81 @@ +import 'react-toastify/dist/ReactToastify.css'; +import { toast } from 'react-toastify'; + +import { cn } from '../../utils'; +import Button from '../common/Button'; +import Input from '../common/Input'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { resetPasswordSchema } from '../../utils/schemas'; +import { useParams, useNavigate } from 'react-router-dom'; + +import { useNewPasswordMutation } from '../../services/resetPassword'; + +interface PasswordData { + newPassword: string; + passwordConfirm: string; +} +const NewPasswordComponent = () => { + const navigate = useNavigate(); + const { token } = useParams<{ token: string }>(); + const [mutate, { isLoading }] = useNewPasswordMutation(); + const { + register, + handleSubmit, + reset, + formState: { errors }, + } = useForm({ resolver: zodResolver(resetPasswordSchema) }); + + const onSubmit = async (data: PasswordData) => { + try { + const { newPassword } = data; + const res = await mutate({ newPassword, token }); + if (res.error) { + const { data } = res.error as any; + return toast.error(data.error || 'Failed, please try again later!'); + } else { + reset(); + const { + data: { message }, + } = res; + toast.success(message || 'Password reset successfully!'); + setTimeout(() => navigate('/login'), 3000); + } + } catch (err) { + toast.error('An unexpected error occurred'); + } + }; + return ( +
+
+ + {errors.newPassword &&

{errors.newPassword.message}

} + + {errors.passwordConfirm && ( +

{errors.passwordConfirm.message}

+ )} +
+ ); +}; + +export default NewPasswordComponent; diff --git a/src/components/passwordReset/PasswordResetComponent.tsx b/src/components/passwordReset/PasswordResetComponent.tsx new file mode 100644 index 0000000..44348bd --- /dev/null +++ b/src/components/passwordReset/PasswordResetComponent.tsx @@ -0,0 +1,63 @@ +import 'react-toastify/dist/ReactToastify.css'; +import { toast } from 'react-toastify'; +import { useEffect } from 'react'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { useNavigate } from 'react-router-dom'; + +import { cn } from '../../utils'; +import Button from '../common/Button'; +import Input from '../common/Input'; +import { emailSchema } from '../../utils/schemas'; +import { useResetPasswordMutation } from '../../services/resetPassword'; +import { useAppSelector } from '../../hooks/customHooks'; +interface UserData { + email: string; +} + +const PasswordResetComponent = () => { + const token: string | undefined = useAppSelector(state => state.user.token)?.replace(/"/g, ''); + const navigate = useNavigate(); + useEffect(() => { + if (token !== undefined) { + navigate('/login'); + } + }, []); + const { + register, + handleSubmit, + reset, + formState: { errors }, + } = useForm({ + resolver: zodResolver(emailSchema), + }); + const [resetPassword, { isLoading }] = useResetPasswordMutation(); + + const onSubmit = async (data: UserData) => { + try { + const res = await resetPassword(data.email).unwrap(); + toast.success(res.message || 'Password reset email sent, Please verify your email!'); + reset(); + } catch (err) { + toast.error('Failed to send email, check your provided email and try again!'); + } + }; + return ( +
+

Forgot Password?

+
+ + {errors.email &&

{errors.email.message}

} +
+ ); +}; + +export default PasswordResetComponent; diff --git a/src/hooks/customHooks.ts b/src/hooks/customHooks.ts new file mode 100644 index 0000000..fb06a67 --- /dev/null +++ b/src/hooks/customHooks.ts @@ -0,0 +1,5 @@ +import { TypedUseSelectorHook, useSelector, useDispatch } from 'react-redux'; +import { RootState, AppDispatch } from '../redux/store'; + +export const useAppDispatch = () => useDispatch(); +export const useAppSelector: TypedUseSelectorHook = useSelector; diff --git a/src/pages/NewPassword.tsx b/src/pages/NewPassword.tsx new file mode 100644 index 0000000..13ecdb5 --- /dev/null +++ b/src/pages/NewPassword.tsx @@ -0,0 +1,15 @@ +import Footer from '../components/footer/Footer'; +import Navbar from '../components/navbar/Navbar'; +import NewPasswordComponent from '../components/passwordReset/NewPasswordComponent'; + +const NewPassword = () => { + return ( +
+ + +
+
+ ); +}; + +export default NewPassword; diff --git a/src/pages/ResetPassword.tsx b/src/pages/ResetPassword.tsx new file mode 100644 index 0000000..9544b4c --- /dev/null +++ b/src/pages/ResetPassword.tsx @@ -0,0 +1,14 @@ +import PasswordResetComponent from '../components/passwordReset/PasswordResetComponent'; +import Navbar from '../components/navbar/Navbar'; +import Footer from '../components/footer/Footer'; +const ResetPassword = () => { + return ( +
+ + +
+
+ ); +}; + +export default ResetPassword; diff --git a/src/services/resetPassword.ts b/src/services/resetPassword.ts new file mode 100644 index 0000000..767e23e --- /dev/null +++ b/src/services/resetPassword.ts @@ -0,0 +1,22 @@ +import { mavericksApi } from '.'; + +export const resetPasswordAPI = mavericksApi.injectEndpoints({ + endpoints: builder => ({ + resetPassword: builder.mutation({ + query: email => ({ + url: '/auth/forgot-password', + method: 'POST', + body: { email }, + }), + }), + newPassword: builder.mutation({ + query: ({ newPassword, token }) => ({ + url: `/auth/reset-password/${token}`, + method: 'POST', + body: { newPassword }, + }), + }), + }), +}); + +export const { useResetPasswordMutation, useNewPasswordMutation } = resetPasswordAPI; diff --git a/src/utils/schemas.ts b/src/utils/schemas.ts index 30eb47a..6588cc3 100644 --- a/src/utils/schemas.ts +++ b/src/utils/schemas.ts @@ -40,5 +40,23 @@ export const loginSchema = z.object({ email: z.string().email({ message: 'A valid email is required' }), password: z.string().min(5, { message: 'Password must be 5 characters or more' }), }); +export const emailSchema = z.object({ + email: z.string().email({ message: 'Please Provide a valid email address' }), +}); +export const resetPasswordSchema = z + .object({ + newPassword: z + .string() + .min(8, { message: 'Password must be at least 8 characters long' }) + .regex(/[A-Z]/, 'Password must contain at least one uppercase letter') + .regex(/[a-z]/, 'Password must contain at least one lowercase letter') + .regex(/[0-9]/, 'Password must contain at least one digit') + .regex(/[!@#$%^&*(),.?":{}|<>]/, 'Password must contain at least one special character'), + passwordConfirm: z.string().min(8, { message: 'Password confirmation must be at least 8 characters long' }), + }) + .refine(data => data.newPassword === data.passwordConfirm, { + message: 'Passwords do not match', + path: ['passwordConfirm'], + }); export type LoginData = z.infer;