Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 210 additions & 0 deletions frontend/app/forgot-password/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
"use client";

import { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import Link from "next/link";
import { useRouter } from "next/navigation";

const forgotPasswordSchema = z.object({
email: z.string().email("Please enter a valid email address"),
});

type ForgotPasswordForm = z.infer<typeof forgotPasswordSchema>;

export default function ForgotPasswordPage() {
const [isLoading, setIsLoading] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [submittedEmail, setSubmittedEmail] = useState("");
const [countdown, setCountdown] = useState(5);
const router = useRouter();

const {
register,
handleSubmit,
formState: { errors },
} = useForm<ForgotPasswordForm>({
resolver: zodResolver(forgotPasswordSchema),
});

const onSubmit = async (data: ForgotPasswordForm) => {
setIsLoading(true);

try {
const response = await fetch("/api/v1/auth/forgot-password", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});

if (!response.ok) {
throw new Error("Failed to send reset email");
}

setSubmittedEmail(data.email);
setIsSuccess(true);

// Start countdown and redirect
let timeLeft = 5;
const timer = setInterval(() => {
timeLeft -= 1;
setCountdown(timeLeft);
if (timeLeft === 0) {
clearInterval(timer);
router.push("/login");
}
}, 1000);
} catch (error) {
alert("Failed to send reset email. Please try again.");
} finally {
setIsLoading(false);
}
};

if (isSuccess) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8 animate-fade-in">
<div className="text-center">
<div className="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-green-100 mb-4">
<svg
className="h-10 w-10 text-green-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
<h2 className="text-3xl font-bold text-gray-900">
Check Your Email
</h2>
<p className="mt-4 text-base text-gray-600">
We've sent password reset instructions to
</p>
<p className="mt-2 text-sm font-medium text-emerald-600">
{submittedEmail}
</p>
<p className="mt-6 text-sm text-gray-500">
Didn't receive the email? Check your spam folder or{" "}
<button
onClick={() => setIsSuccess(false)}
className="text-emerald-600 hover:text-emerald-500 font-medium"
>
try again
</button>
</p>
</div>

<div className="space-y-4">
<Link
href="/login"
className="w-full flex justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-emerald-600 hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-emerald-500 transition-colors"
>
Back to Login
</Link>
<p className="text-center text-sm text-gray-500">
Redirecting in {countdown} seconds...
</p>
</div>
</div>
</div>
);
}

return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div className="text-center">
<h2 className="text-3xl font-bold text-gray-900">
Reset Your Password
</h2>
<p className="mt-2 text-sm text-gray-600">
Enter your email and we'll send you a reset link
</p>
</div>

<form className="mt-8 space-y-6" onSubmit={handleSubmit(onSubmit)}>
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700"
>
Email address
</label>
<div className="mt-1">
<input
{...register("email")}
id="email"
type="email"
autoComplete="email"
className={`appearance-none block w-full px-3 py-3 border ${
errors.email ? "border-red-300" : "border-gray-300"
} rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-emerald-500 focus:border-emerald-500 sm:text-sm`}
placeholder="you@example.com"
/>
{errors.email && (
<p className="mt-2 text-sm text-red-600">
{errors.email.message}
</p>
)}
</div>
</div>

<div>
<button
type="submit"
disabled={isLoading}
className="w-full flex justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-emerald-600 hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-emerald-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isLoading ? (
<div className="flex items-center">
<svg
className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Sending...
</div>
) : (
"Send Reset Link"
)}
</button>
</div>

<div className="text-center">
<Link
href="/login"
className="text-sm font-medium text-emerald-600 hover:text-emerald-500"
>
← Back to Login
</Link>
</div>
</form>
</div>
</div>
);
}
Loading