Skip to content

Commit

Permalink
Forget password #147 (#178)
Browse files Browse the repository at this point in the history
* fogetpassword

* ft-forget password

* reset and forget password

* reset and forget password

* reset and forget password

* reset and forget password

* reset password

* reset update background

* reset update background

* reset update background

* reset password

* reset password

* reset password

* reset password

* yarn.lock

* Ft search job post (#187)

* ft(Add search): Add entry search

* Will add user seach Job Functionalities --squashed commits

* single page to view and search Jobs

* will make jobs and search be on same page

* updating functionality

* Real time searching

---------

Co-authored-by: Jmukakalisa <[email protected]>

* 154 crud application form (#167)

* Will add create application feature

* Will add update application form feature

* Will add view application form feature

* Will add dark mode card

* Will add create application feature

* Will add update application form feature

* Will add view application form feature

* Will add dark mode card

* Add Verify Email (#179)

Co-authored-by: vegetason <[email protected]>

* resolve comflit

* resolve comflit

* resolve comflit

* resolve comflit

---------

Co-authored-by: Aime Brues <[email protected]>
Co-authored-by: Jmukakalisa <[email protected]>
Co-authored-by: MUGISHA Joseph <[email protected]>
Co-authored-by: Irakoze Paulin <[email protected]>
Co-authored-by: vegetason <[email protected]>
  • Loading branch information
6 people authored Oct 8, 2024
1 parent b2acedd commit 8eecb3c
Show file tree
Hide file tree
Showing 14 changed files with 20,527 additions and 6 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ buildcoverage
package-lock.json
yarn.lock
.DS_Store
build/
build/
yarn.lock
69 changes: 69 additions & 0 deletions src/components/form/ForgotPasswordForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from "react";
import { Link } from "react-router-dom";
import { Formik, Field, Form, ErrorMessage } from 'formik';
import { validationSchema } from "../validation/forgetvalidation";

interface ForgotPasswordFormProps {
emailSent: boolean;
error: string;
onSubmit: (values: { email: string }, { setSubmitting }) => Promise<void>;
}

export const ForgotPasswordForm: React.FC<ForgotPasswordFormProps> = ({ emailSent, error, onSubmit }) => {
if (emailSent) {
return (
<p className="text-white dark:text-green-400 text-center">
An email with password reset instructions has been sent to your email address.
</p>
);
}

return (
<>
<Formik
initialValues={{ email: '' }}
validationSchema={validationSchema}
onSubmit={onSubmit}
>
{({ isSubmitting }) => (
<Form className="w-full max-w-md px-4">
<div className="mb-4">
<Field
name="email"
type="email"
className="w-full rounded-md px-2 py-3 border border-white placeholder:text-gray-400 text-black dark:text-white sm:text-[12px] outline-none bg-gray-100 dark:bg-[#1F2A37]"
placeholder="Enter your email"
/>
<ErrorMessage
name="email"
component="div"
className="text-red-500 text-xs mt-1"
/>
</div>

{error && (
<div className="text-red-500 text-sm mb-4 text-center">
{error}
</div>
)}

<button
type="submit"
disabled={isSubmitting}
className="w-full py-2 px-4 text-white rounded-md bg-[#56C870] focus:outline-none disabled:opacity-50"
>
{isSubmitting ? 'Submitting...' : 'Send Reset Link'}
</button>
</Form>
)}
</Formik>

<p className="mt-4 text-sm dark:text-white text-center">
Remembered your password?{" "}
<Link to="/login" className="text-blue-600 dark:text-blue-400">
Login here
</Link>
</p>
</>
);
};
56 changes: 56 additions & 0 deletions src/components/form/ResetPasswordForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from "react";
import { Formik, Field, Form, ErrorMessage } from "formik";
import { validationSchema } from "../validation/resetvalidation";

interface ResetPasswordFormProps {
onSubmit: (values: { password: string }, { setSubmitting }) => Promise<void>;
error: string;
}

export const ResetPasswordForm: React.FC<ResetPasswordFormProps> = ({ onSubmit, error }) => (
<Formik
initialValues={{ password: "", confirmPassword: "" }}
validationSchema={validationSchema}
onSubmit={onSubmit}
>
{({ isSubmitting }) => (
<Form className="w-full max-w-md">
<PasswordField name="password" placeholder="Enter your new password" />
<PasswordField name="confirmPassword" placeholder="Confirm your new password" />
{error && (
<div className="mb-4 text-red-500 text-sm text-center">
{error}
</div>
)}
<SubmitButton isSubmitting={isSubmitting} />
</Form>
)}
</Formik>
);

const PasswordField: React.FC<{ name: string; placeholder: string }> = ({ name, placeholder }) => (
<div className="mb-4">
<Field
name={name}
type="password"
className="w-full rounded-md px-2 py-3 border border-white placeholder:text-gray-400 text-black dark:text-white sm:text-[12px] outline-none bg-gray-100 dark:bg-[#1F2A37]"
placeholder={placeholder}
/>
<ErrorMessage
name={name}
component="div"
className="text-red-500 text-xs mt-1"
/>
</div>
);

const SubmitButton: React.FC<{ isSubmitting: boolean }> = ({ isSubmitting }) => (
<button
type="submit"
disabled={isSubmitting}
className="w-full py-2 px-4 text-white rounded-md bg-[#56C870] focus:outline-none disabled:opacity-50"
>
{isSubmitting ? "Submitting..." : "New Password"}
</button>
);

22 changes: 17 additions & 5 deletions src/components/form/SignInForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,23 @@ const LoginForm = () => {
});

const redirectAfterLogin = async () => {
const lastAttemptedRoute = localStorage.getItem('lastAttemptedRoute');
if (lastAttemptedRoute) {
localStorage.removeItem('lastAttemptedRoute');
navigate(lastAttemptedRoute);
} else {
await Token();
const role = localStorage.getItem("roleName") as string;

if (role === "applicant") {
navigate("/applicant");
} else if (role === "superAdmin" || "Admin") {
} else if (role === "superAdmin") {
navigate("/admin");
} else {
const searchParams = new URLSearchParams(location.search);
const returnUrl = searchParams.get('returnUrl') || '/';
navigate(returnUrl);
}
}
}

const onSubmit = async (data: loginFormData) => {
Expand Down Expand Up @@ -150,7 +159,8 @@ const LoginForm = () => {
<Button
type="submit"
label=""
className="sm:w-full w-5/6 rounded-md mb-4 px-2 py-3 text-white focus:bg-[#56C870] bg-[#56C870]"
className="sm:w-full w-5/6 rounded-md mb-4 px-2 py-3 text-white focus:bg-[#56C870] bg-[#56C870]"

disabled={true}
>
<svg
Expand Down Expand Up @@ -179,12 +189,14 @@ const LoginForm = () => {
</div>
<p className="text-sm mt-3 mb-2 text-[#616161] dark:text-gray-300">
Don't have an account?{" "}
<Link to={'/signup'} className="text-[#56C870]">
<Link to="/signup" className="text-[#56C870]">

Sign up
</Link>
</p>
<p className="text-sm mt-3 mb-2 text-[#616161] dark:text-gray-300">
<Link to="/forgot-password" className="text-blue-500 hover:underline">
<Link to="/forget" className="text-blue-500 hover:underline">

Forgot your password?
</Link>
</p>
Expand Down
28 changes: 28 additions & 0 deletions src/components/form/SuccessMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React, { useEffect } from "react";
import { useNavigate } from "react-router-dom";

interface SuccessMessageProps {
message: string;
onNavigate: () => void;
}

export const SuccessMessage: React.FC<SuccessMessageProps> = ({ message, onNavigate }) => {
const navigate = useNavigate();

useEffect(() => {
const timer = setTimeout(() => {
onNavigate();
}, 3000);

return () => clearTimeout(timer);
}, [onNavigate]);

return (
<div className="text-center">
<p className="text-xl font-bold text-green-600 dark:text-gray-300">{message}</p>
<p className="mt-2 text-gray-200 dark:text-gray-300">
Redirecting to login in 3 seconds...
</p>
</div>
);
};
30 changes: 30 additions & 0 deletions src/components/form/useForgotPassword.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useState } from "react";
import { request } from "graphql-request";

const RESET_PASSWORD_MUTATION = `
mutation ForgetPassword($email: String!) {
forgetPassword(email: $email)
}
`;

export const useForgotPassword = () => {
const [emailSent, setEmailSent] = useState(false);
const [error, setError] = useState("");

const handleSubmit = async (values: { email: string }, { setSubmitting }) => {
try {
const API_URL = process.env.BACKEND_URL;
await request(`${API_URL}/graphql`, RESET_PASSWORD_MUTATION, {
email: values.email,
});
setEmailSent(true);
setError("");
} catch (error: any) {
setError(error.response?.errors?.[0]?.message || "An error occurred. Please try again.");
} finally {
setSubmitting(false);
}
};

return { emailSent, error, handleSubmit };
};
112 changes: 112 additions & 0 deletions src/components/form/useResetPassword.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { useState } from "react";
import { request, ClientError } from "graphql-request";

interface ResetPasswordResponse {
resetPassword: string
}

interface ResetPasswordValues {
password: string;
}

interface SubmittingHelpers {
setSubmitting: (isSubmitting: boolean) => void;
}

interface ResetState {
successMessage: string;
error: string;
}

const RESET_PASSWORD_MUTATION = `
mutation ResetPassword($token: String!, $newPassword: String!) {
resetPassword(token: $token, newPassword: $newPassword)
}
`;

const extractSuccessMessage = (error: ClientError): string | null => {
const match = error.response.errors?.[0]?.message.match(/message: "(.*?)"/);
return match?.[1] || null;
};

const getErrorMessage = (error: unknown): string => {
if (error instanceof ClientError) {
return error.response.errors?.[0]?.message || "An error occurred while resetting your password.";
}
if (error instanceof Error) {
return error.message;
}
return "An unexpected error occurred";
};

const processResponse = (response: ResetPasswordResponse): ResetState => ({
successMessage: response.resetPassword || "",
error: "",
});

const processError = (error: unknown): ResetState => {
if (error instanceof ClientError) {
const successMessage = extractSuccessMessage(error);
if (successMessage) {
return { successMessage, error: "" };
}
}
return { successMessage: "", error: getErrorMessage(error) };
};

const makeRequest = async (url: string, token: string, password: string): Promise<ResetPasswordResponse> => {
return request<ResetPasswordResponse>(
url,
RESET_PASSWORD_MUTATION,
{
token,
newPassword: password,
}
);
};

const usePasswordResetRequest = () => {
const sendRequest = async (token: string, password: string): Promise<ResetState> => {
const API_URL = process.env.BACKEND_URL;
if (!API_URL) {
return { successMessage: "", error: "Backend URL is not defined" };
}

try {
const response = await makeRequest(`${API_URL}/graphql`, token, password);
return processResponse(response);
} catch (error) {
return processError(error);
}
};

return { sendRequest };
};

export const useResetPassword = (token: string | null) => {
const [successMessage, setSuccessMessage] = useState("");
const [error, setError] = useState("");
const { sendRequest } = usePasswordResetRequest();

const handleSubmit = async (
values: ResetPasswordValues,
{ setSubmitting }: SubmittingHelpers
) => {
if (!token) {
setSuccessMessage("");
setError("Invalid or missing token");
setSubmitting(false);
return;
}

try {
const result = await sendRequest(token, values.password);
setSuccessMessage(result.successMessage);
setError(result.error);
} finally {
setSubmitting(false);
}
};

return { successMessage, error, handleSubmit };
};
Loading

0 comments on commit 8eecb3c

Please sign in to comment.