Skip to content

Commit

Permalink
Merge pull request #1 from Fingertips18/development
Browse files Browse the repository at this point in the history
Create forgot password form.
  • Loading branch information
Fingertips18 authored Sep 22, 2024
2 parents a397ccd + a9a085f commit 01c27ae
Show file tree
Hide file tree
Showing 20 changed files with 190 additions and 39 deletions.
5 changes: 5 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Route, Routes } from "react-router-dom";

import { ForgotPasswordPage } from "@/pages/forgot-password/page";
import VerifyEmailPage from "@/pages/verify-email/page";
import PrivateGuard from "@/guards/private-guard";
import { AppRoutes } from "@/constants/routes";
Expand All @@ -20,6 +21,10 @@ function App() {
<Route path={AppRoutes.SignIn} element={<SignInPage />} />
<Route path={AppRoutes.VerifyEmail} element={<VerifyEmailPage />} />
</Route>
<Route
path={AppRoutes.ForgotPassword}
element={<ForgotPasswordPage />}
/>
</Routes>
</main>
);
Expand Down
9 changes: 5 additions & 4 deletions client/src/components/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const Input = ({
const [obscure, setObscure] = useState(true);
const [valid, setValid] = useState(false);

const isPasswod = type === "password";
const isPassword = type === "password";
const hasInput = value.length > 0;

useEffect(() => {
Expand Down Expand Up @@ -75,7 +75,7 @@ const Input = ({
<input
value={value}
onChange={(e) => setValue(e.target.value)}
type={isPasswod ? (obscure ? "password" : "text") : type}
type={isPassword ? (obscure ? "password" : "text") : type}
placeholder={placeholder}
autoComplete={autoComplete}
id={name?.toLowerCase() ?? label.toLowerCase()}
Expand All @@ -84,7 +84,8 @@ const Input = ({
required={required}
maxLength={maxLength}
className={`w-full md:w-[400px] bg-background py-2.5 rounded-lg outline-none border ring-1 focus:ring-2 transition-all
placeholder-foreground/50 px-11 disabled:bg-opacity-25 disabled:border-primary/25 disabled:text-foreground/50 disabled:pointer-events-none
placeholder-foreground/50 disabled:bg-opacity-25 disabled:border-primary/25 disabled:text-foreground/50 disabled:pointer-events-none
${isPassword ? "px-11" : "pl-11"}
${
hasInput
? valid
Expand All @@ -96,7 +97,7 @@ const Input = ({
<div className="absolute inset-y-0 left-0 flex-center pl-3 pointer-events-none">
<SuffixIcon size={24} className="text-primary" />
</div>
{isPasswod && (
{isPassword && (
<button
type="button"
onClick={() => setObscure(!obscure)}
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/or.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const Or = () => {
return (
<div className="flex-between gap-x-2 w-full md:w-[400px] px-6 lg:px-0">
<div className="flex-between gap-x-2 w-full px-6">
<span className="h-px w-full flex-1 bg-foreground/40 rounded-full" />
<p className="text-sm lg:text-lg font-semibold text-foreground/40">Or</p>
<span className="h-px flex-1 bg-foreground/40 rounded-full" />
Expand Down
9 changes: 6 additions & 3 deletions client/src/components/switch-auth.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { Link } from "react-router-dom";

interface SwitchAuthProps {
label: string;
label?: string;
tag: string;
href: string;
}

const SwitchAuth = ({ label, tag, href }: SwitchAuthProps) => {
return (
<div className="p-4 w-full md:w-[400px] text-sm lg:text-base rounded-md border border-secondary/50 bg-secondary/15 drop-shadow-2xl flex-center gap-x-2">
<p className="font-medium">{label}</p>
<div
className="p-4 w-full text-sm lg:text-base rounded-md border
border-secondary/50 bg-secondary/15 drop-shadow-2xl flex-center gap-x-2"
>
{label && <p className="font-medium">{label}</p>}
<Link
to={href}
className="font-bold underline-offset-4 hover:underline text-secondary transition-all hover:drop-shadow-secondary-glow"
Expand Down
1 change: 1 addition & 0 deletions client/src/constants/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export const SIGNOUTKEY = "sign-out";
export const VERIFYEMAILKEY = "verify-email";
export const RESENDVERIFYKEY = "resend-verify";
export const VERIFYTOKENKEY = "verify-token";
export const FORGOTPASSWORDKEY = "forgot-password";
8 changes: 4 additions & 4 deletions client/src/lib/providers/theme-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
const ThemeProvider = ({
children,
defaultTheme = "system",
storageKey = "vite-ui-theme",
storageKey = "gra-theme",
...props
}: ThemeProviderProps) => {
const [theme, setTheme] =
useState<Theme>(() => localStorage.getItem(storageKey) as Theme) ||
defaultTheme;
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
);

useEffect(() => {
const root = window.document.documentElement;
Expand Down
24 changes: 24 additions & 0 deletions client/src/lib/services/auth-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,28 @@ export const AuthService = {
user: data.user,
});
},
forgotPassword: async (email: string) => {
const res = await fetch(`${baseURL}${AppRoutes.ForgotPassword}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
email: email,
}),
});

const data = await res.json();

if (!res.ok) {
throw new ErrorResponse({
status: res.status,
message: data.error,
});
}

return new GenericResponse({
message: data.message,
});
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { useMutation } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
import { Mail } from "lucide-react";
import { FormEvent } from "react";
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 { ValidateEmail } from "@/lib/utils/validations";
import { FORGOTPASSWORDKEY } from "@/constants/keys";
import { Button } from "@/components/text-button";
import { AppRoutes } from "@/constants/routes";
import { Input } from "@/components/input";

const ForgotPasswordForm = () => {
const navigate = useNavigate();

const { mutate, isPending } = useMutation({
mutationKey: [FORGOTPASSWORDKEY],
mutationFn: AuthService.forgotPassword,
onSuccess: (res: GenericResponse) => {
toast.success(res.message);
navigate(AppRoutes.ResetPassword);
},
onError: (error: ErrorResponse) => toast.error(error.message),
});

const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();

const formData = new FormData(e.currentTarget);

const forgotPasswordData = Object.fromEntries(formData.entries());

mutate(forgotPasswordData["email"] as string);
};

return (
<form
onSubmit={onSubmit}
className="p-4 lg:p-6 w-full md:w-fit rounded-md border border-primary/50 bg-primary/15 drop-shadow-2xl space-y-4 lg:space-y-6"
>
<Input
required
name="email"
label="Email Address"
tooltip="Please enter a valid email address. Ensure it includes an '@' symbol and a domain name."
placeholder="e.g. [email protected]"
type="email"
autoComplete="email"
suffixIcon={Mail}
disabled={isPending}
validation={ValidateEmail}
maxLength={320}
/>

<Button
label="Send Reset Link"
disabled={isPending}
loading={isPending}
type="submit"
/>
</form>
);
};

export { ForgotPasswordForm };
27 changes: 27 additions & 0 deletions client/src/pages/forgot-password/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { SwitchAuth } from "@/components/switch-auth";
import { ForgotPasswordForm } from "./_components/forgot-password-form";
import { AppRoutes } from "@/constants/routes";

const ForgotPasswordPage = () => {
return (
<section className="px-4 lg:px-0 h-full flex-center flex-col gap-y-6 w-fit mx-auto">
<div className="w-full rounded-md border border-primary/50 bg-primary/15 drop-shadow-2xl">
<h2 className="text-lg lg:text-2xl font-extrabold text-center uppercase pt-4 pb-2">
Forgot Password
</h2>
<div className="w-full bg-primary/25 p-2.5 flex-center border-t border-primary/50">
<p className="text-xs lg:text-base text-center lg:max-w-[324px] text-foreground/60 font-medium">
Enter your email address and wait for a reset password link to be
sent.
</p>
</div>
</div>

<ForgotPasswordForm />

<SwitchAuth href={AppRoutes.Root} tag="Back" />
</section>
);
};

export { ForgotPasswordPage };
14 changes: 9 additions & 5 deletions client/src/pages/sign-in/_components/sign-in-form.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Link, useNavigate } from "react-router-dom";
import { useMutation } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
import { FormEvent } from "react";
import { toast } from "sonner";

Expand Down Expand Up @@ -52,10 +52,6 @@ const SignInForm = () => {
onSubmit={onSubmit}
className="p-4 lg:p-6 w-full md:w-fit rounded-md border border-primary/50 bg-primary/15 drop-shadow-2xl space-y-4 lg:space-y-6"
>
<h2 className="text-lg lg:text-2xl font-extrabold text-center uppercase">
Access Your Account
</h2>

{SIGNININPUTS.map((s) => (
<Input
key={s.label}
Expand All @@ -66,6 +62,14 @@ const SignInForm = () => {
/>
))}

<Link
to={AppRoutes.ForgotPassword}
className="text-sm font-medium hover:underline underline-offset-4 transition-all text-foreground
hover:text-primary px-1.5 inline-block text-end w-full"
>
Forgot Password?
</Link>

<Button
label="Sign In"
disabled={isPending}
Expand Down
9 changes: 8 additions & 1 deletion client/src/pages/sign-in/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ import { SignInForm } from "./_components/sign-in-form";

const SignInPage = () => {
return (
<section className="px-4 lg:px-0 h-full flex-center flex-col gap-y-6">
<section className="px-4 lg:px-0 h-full flex-center flex-col gap-y-6 w-fit mx-auto">
<h2
className="text-lg lg:text-2xl font-extrabold text-center uppercase w-full
rounded-md border border-primary/50 bg-primary/15 drop-shadow-2xl p-4"
>
Access Your Account
</h2>

<SignInForm />

<Or />
Expand Down
4 changes: 0 additions & 4 deletions client/src/pages/sign-up/_components/sign-up-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,6 @@ const SignUpForm = () => {
onSubmit={onSubmit}
className="p-4 lg:p-6 w-full md:w-fit rounded-md border border-primary/50 bg-primary/15 drop-shadow-2xl space-y-4 lg:space-y-6"
>
<h2 className="text-lg lg:text-2xl font-extrabold text-center uppercase">
Create Account
</h2>

{SIGNUPINPUTS.map((s) => (
<Input
key={s.label}
Expand Down
9 changes: 8 additions & 1 deletion client/src/pages/sign-up/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ import { SignUpForm } from "./_components/sign-up-form";

const SignUpPage = () => {
return (
<section className="px-4 lg:px-0 h-full flex-center flex-col gap-y-6">
<section className="px-4 lg:px-0 h-full flex-center flex-col gap-y-6 w-fit mx-auto">
<h2
className="text-lg lg:text-2xl font-extrabold text-center uppercase w-full
rounded-md border border-primary/50 bg-primary/15 drop-shadow-2xl p-4"
>
Create Account
</h2>

<SignUpForm />

<Or />
Expand Down
5 changes: 4 additions & 1 deletion client/src/pages/verify-email/_components/resend-code.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ const ResendCode = () => {
const onClick = () => mutate(email);

return (
<div className="flex-center gap-x-2 text-sm">
<div
className="p-4 w-full text-sm lg:text-base rounded-md border
border-secondary/50 bg-secondary/15 drop-shadow-2xl flex-center gap-x-2"
>
<p className="font-medium">Didn't receive a code?</p>
<button
className="font-bold underline-offset-4 hover:underline text-secondary transition-all hover:drop-shadow-secondary-glow"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,11 @@ const VerifyEmailForm = () => {
}, [code]);

return (
<form ref={formRef} onSubmit={onSubmit} className="space-y-4 lg:space-y-6">
<form
ref={formRef}
onSubmit={onSubmit}
className="p-4 lg:p-6 w-full md:w-fit rounded-md border border-primary/50 bg-primary/15 drop-shadow-2xl space-y-4 lg:space-y-6"
>
<div className="flex-center gap-x-4 lg:gap-x-6">
{code.map((digit, i) => (
<SingleInput
Expand Down
17 changes: 9 additions & 8 deletions client/src/pages/verify-email/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@ import { ResendCode } from "./_components/resend-code";

const VerifyEmailPage = () => {
return (
<section className="px-4 lg:px-0 h-full flex-center">
<div className="p-4 lg:p-6 w-full md:w-fit rounded-md border border-primary/50 bg-primary/15 drop-shadow-2xl space-y-4 lg:space-y-6">
<h2 className="text-lg lg:text-2xl font-extrabold text-center uppercase">
Verify Email
</h2>
<section className="px-4 lg:px-0 h-full flex-center flex-col gap-y-6 w-fit mx-auto">
<h2
className="text-lg lg:text-2xl font-extrabold text-center uppercase w-full
rounded-md border border-primary/50 bg-primary/15 drop-shadow-2xl p-4"
>
Verify Email
</h2>

<VerifyEmailForm />
<VerifyEmailForm />

<ResendCode />
</div>
<ResendCode />
</section>
);
};
Expand Down
4 changes: 2 additions & 2 deletions templates/request_reset_password.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ const _REQUEST_RESET_PASSWORD_HTML = `
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: %s; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: linear-gradient(to right, %s, %s); padding: 20px; text-align: center;">
<h1 style="color: #DCF9F5; margin: 0;">Password Request Reset</h1>
<h1 style="color: #DCF9F5 !important; margin: 0;">Password Request Reset</h1>
</div>
<div style="background-color: %s; padding: 20px; border-radius: 0 0 5px 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
<p>Hello,</p>
<p>We received a request to reset your password. If you didn't make this request, please ignore this email.</p>
<p>To reset your password, click the button below:</p>
<div style="text-align: center; margin: 30px 0;">
<a href="{resetURL}" style="background-color: %s; color: #DCF9F5; padding: 12px 20px; text-decoration: none; border-radius: 5px; font-weight: bold;">Reset Password</a>
<a href="{resetURL}" style="background-color: %s; color: #DCF9F5 !important; padding: 12px 20px; text-decoration: none; border-radius: 5px; font-weight: bold;">Reset Password</a>
</div>
<p>This link will expire in 15 minutes for security reasons.</p>
<p>Best regards,<br>Fingertips</p>
Expand Down
4 changes: 2 additions & 2 deletions templates/success_reset_password.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ const _RESET_PASSWORD_SUCCESS_HTML = `
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: %s; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="background: linear-gradient(to right, %s, %s); padding: 20px; text-align: center;">
<h1 style="color: #DCF9F5; margin: 0;">Password Reset Successful</h1>
<h1 style="color: #DCF9F5 !important; margin: 0;">Password Reset Successful</h1>
</div>
<div style="background-color: %s; padding: 20px; border-radius: 0 0 5px 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">
<p>Hello {username},</p>
<p>We're writing to confirm that your password has been successfully reset.</p>
<div style="text-align: center; margin: 30px 0;">
<div style="background-color: %s; color: #DCF9F5; width: 50px; height: 50px; line-height: 50px; border-radius: 50%%; display: inline-block; font-size: 30px;">
<div style="background-color: %s; color: #DCF9F5 !important; width: 50px; height: 50px; line-height: 50px; border-radius: 50%%; display: inline-block; font-size: 30px;">
</div>
</div>
Expand Down
Loading

0 comments on commit 01c27ae

Please sign in to comment.