diff --git a/src/app/(public)/forgot-password/components/SuccessModal.tsx b/src/app/(public)/forgot-password/components/SuccessModal.tsx
new file mode 100644
index 0000000..74347b3
--- /dev/null
+++ b/src/app/(public)/forgot-password/components/SuccessModal.tsx
@@ -0,0 +1,89 @@
+'use client';
+
+import styled from 'styled-components';
+
+const Overlay = styled.div`
+ position: fixed;
+ inset: 0;
+ background: rgba(80, 80, 80, 0.6);
+ z-index: 1000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+`;
+
+const ModalBox = styled.div`
+ background: #fff;
+ border-radius: 16px;
+ padding: 36px 32px 28px 32px;
+ box-shadow: 0 4px 32px rgba(0, 0, 0, 0.12);
+ min-width: 340px;
+ max-width: 400px;
+ text-align: center;
+ position: relative;
+`;
+
+const CloseBtn = styled.button`
+ position: absolute;
+ top: 16px;
+ right: 16px;
+ background: none;
+ border: none;
+ font-size: 22px;
+ color: #888;
+ cursor: pointer;
+`;
+
+const SuccessIcon = () => (
+
+);
+
+interface SuccessModalProps {
+ email?: string;
+ title?: string;
+ description?: React.ReactNode;
+ onClose: () => void;
+}
+
+export default function SuccessModal({
+ email,
+ title,
+ description,
+ onClose,
+}: SuccessModalProps) {
+ return (
+
+
+
+ ×
+
+
+
+ {title}
+
+
+ {description}
+
+
+
+ );
+}
diff --git a/src/app/(public)/forgot-password/page.tsx b/src/app/(public)/forgot-password/page.tsx
new file mode 100644
index 0000000..5cfe902
--- /dev/null
+++ b/src/app/(public)/forgot-password/page.tsx
@@ -0,0 +1,272 @@
+'use client';
+
+import { zodResolver } from '@hookform/resolvers/zod';
+import Image from 'next/image';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { useForm } from 'react-hook-form';
+import styled from 'styled-components';
+
+import FormField from '@/app/(public)/login/component/FormField';
+import Button from '@/app/(public)/login/ui/Button';
+import ControllerInput from '@/app/(public)/login/ui/controller/ControllerInput';
+
+import SuccessModal from './components/SuccessModal';
+import { forgotPasswordSchema } from './schemas/forgotPasswordSchema';
+
+const PageContainer = styled.div`
+ min-height: 100vh;
+ background-color: #fafafa;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 20px;
+`;
+
+const FormContainer = styled.div`
+ width: 100%;
+ max-width: 700px;
+ margin: 0 auto;
+ padding: 40px;
+ padding-bottom: 100px;
+ border-radius: 24px;
+ box-shadow: 0 0 24px 0 rgba(0, 0, 0, 0.03);
+ background-color: white;
+
+ @media (max-width: 600px) {
+ padding: 20px;
+ padding-bottom: 60px;
+ }
+`;
+
+const IconWrapper = styled.div`
+ position: absolute;
+ top: 24px;
+ left: 24px;
+ z-index: 2;
+ display: none;
+
+ @media (max-width: 600px) {
+ display: block;
+ }
+`;
+
+const RelativeContainer = styled.div`
+ position: relative;
+`;
+
+const LogoContainer = styled.div`
+ display: flex;
+ justify-content: center;
+ margin-bottom: 32px;
+
+ @media (max-width: 600px) {
+ margin-bottom: 24px;
+ }
+`;
+
+const LogoImageWrapper = styled.div`
+ width: 200px;
+ height: 100px;
+
+ @media (max-width: 600px) {
+ margin-top: 32px;
+ width: 105px;
+ height: 25px;
+ }
+
+ img {
+ width: 100% !important;
+ height: 100% !important;
+ object-fit: contain;
+ }
+`;
+
+const Title = styled.h2`
+ text-align: center;
+ font-size: 22px;
+ font-weight: 600;
+ margin-bottom: 24px;
+`;
+
+const Subtitle = styled.p`
+ text-align: center;
+ color: #444;
+ font-size: 16px;
+ margin-bottom: 50px;
+ margin-top: -12px;
+`;
+
+const TopRightWrapper = styled.div`
+ position: absolute;
+ top: 56px;
+ right: 80px;
+ z-index: 10;
+`;
+
+const BackButton = styled.button`
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ background: #fafafa;
+ border: 1px solid #e0e0e0;
+ border-radius: 12px;
+ font-size: 14px;
+ padding: 10px 16px;
+ cursor: pointer;
+ transition: background 0.2s;
+
+ &:hover {
+ background: #f0f0f0;
+ }
+`;
+
+export default function ForgotPasswordPage() {
+ const [mounted, setMounted] = useState(false);
+ const router = useRouter();
+ const [showSuccess, setShowSuccess] = useState(false);
+ const [sentEmail, setSentEmail] = useState('');
+ const { control, handleSubmit } = useForm<{ email: string }>({
+ resolver: zodResolver(forgotPasswordSchema),
+ defaultValues: { email: '' },
+ });
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ const onSubmit = async (data: { email: string }) => {
+ try {
+ const res = await fetch(
+ 'http://localhost:4000/api/auth/forgot-password',
+ {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email: data.email }),
+ },
+ );
+
+ // Optionally handle error response
+ if (!res.ok) {
+ const result = (await res.json()) as { message?: string };
+ alert(result.message ?? 'Failed to send reset email');
+ return;
+ }
+
+ setSentEmail(data.email);
+ setShowSuccess(true);
+ } catch {
+ alert('Network error. Please try again.');
+ }
+ };
+
+ if (!mounted) {
+ return (
+
+ Loading...
+
+ );
+ }
+
+ return (
+
+ {showSuccess && (
+
+ An email has been sent to{' '}
+ {sentEmail} with
+ instructions for resetting your password. This email may take a
+ few minutes to arrive in your inbox.
+ >
+ }
+ onClose={() => setShowSuccess(false)}
+ />
+ )}
+
+ router.back()}>
+
+ Back
+
+
+
+
+
+
+
+
+
+
+
+ Forgot Password
+
+ Fill in your email and we'll send you a link to reset your password.
+
+
+
+
+ );
+}
diff --git a/src/app/(public)/forgot-password/schemas/forgotPasswordSchema.ts b/src/app/(public)/forgot-password/schemas/forgotPasswordSchema.ts
new file mode 100644
index 0000000..ec75d95
--- /dev/null
+++ b/src/app/(public)/forgot-password/schemas/forgotPasswordSchema.ts
@@ -0,0 +1,7 @@
+import { z } from 'zod';
+
+import { emailSchema } from '@/app/(public)/login/schemas/loginSchema';
+
+export const forgotPasswordSchema = z.object({
+ email: emailSchema,
+});
diff --git a/src/app/(public)/login/component/FormField.tsx b/src/app/(public)/login/component/FormField.tsx
index 301273f..a48c97f 100644
--- a/src/app/(public)/login/component/FormField.tsx
+++ b/src/app/(public)/login/component/FormField.tsx
@@ -1,8 +1,7 @@
import React from 'react';
import styled from 'styled-components';
-
interface FormFieldProps {
- label?: string;
+ label?: React.ReactNode;
children: React.ReactNode;
size?: 'small' | 'normal' | 'large';
mb?: number;
diff --git a/src/app/(public)/login/component/LoginForm.tsx b/src/app/(public)/login/component/LoginForm.tsx
index 38c0562..6d3320c 100644
--- a/src/app/(public)/login/component/LoginForm.tsx
+++ b/src/app/(public)/login/component/LoginForm.tsx
@@ -99,6 +99,22 @@ const ErrorMessage = styled.div`
margin-bottom: 16px;
`;
+const ForgotPasswordWrapper = styled.div`
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 6px;
+`;
+
+const ForgotPasswordLink = styled.a`
+ font-family: Roboto;
+ font-size: 14px;
+ font-weight: bold;
+ color: #0687ff;
+ text-decoration: none;
+`;
+
export default function LoginForm() {
const { control, handleSubmit } = useForm({
resolver: zodResolver(loginSchema),
@@ -133,7 +149,18 @@ export default function LoginForm() {
disabled={isLoading}
/>
-
+
+
+ Password
+
+ Forgot password?
+
+
+ }
+ mb={0}
+ >
{
type?: string;
fullWidth?: boolean;
disabled?: boolean;
+ hideError?: boolean;
}
const StyledBox = styled(Box)<{ $fullWidth?: boolean }>`
@@ -52,6 +53,7 @@ export default function ControllerInput({
type = 'text',
fullWidth = true,
disabled,
+ hideError = false,
}: ControllerInputProps) {
return (
({
$hasError={!!error}
disabled={disabled}
/>
- {error && {error.message}}
+ {!hideError && error && {error.message}}
)}
/>
diff --git a/src/app/(public)/reset-password/components/ResetPasswordForm.tsx b/src/app/(public)/reset-password/components/ResetPasswordForm.tsx
new file mode 100644
index 0000000..262fe81
--- /dev/null
+++ b/src/app/(public)/reset-password/components/ResetPasswordForm.tsx
@@ -0,0 +1,209 @@
+'use client';
+
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+import { useForm } from 'react-hook-form';
+
+import SuccessModal from '@/app/(public)/forgot-password/components/SuccessModal';
+import FormField from '@/app/(public)/login/component/FormField';
+import Button from '@/app/(public)/login/ui/Button';
+import ControllerInput from '@/app/(public)/login/ui/controller/ControllerInput';
+
+import { resetPasswordSchema } from '../schemas/resetPasswordSchema';
+
+const EyeIcon = () => (
+
+);
+
+const EyeOffIcon = () => (
+
+);
+
+interface ResetPasswordFormProps {
+ token: string;
+}
+
+export default function ResetPasswordForm({ token }: ResetPasswordFormProps) {
+ const [mounted, setMounted] = useState(false);
+ const [showSuccess, setShowSuccess] = useState(false);
+ const [showPassword, setShowPassword] = useState(false);
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false);
+ const router = useRouter();
+ const { control, handleSubmit, formState } = useForm<{
+ password: string;
+ confirmPassword: string;
+ }>({
+ resolver: zodResolver(resetPasswordSchema),
+ defaultValues: { password: '', confirmPassword: '' },
+ });
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ const onSubmit = async (data: {
+ password: string;
+ confirmPassword: string;
+ }) => {
+ try {
+ // Call the backend API to reset password (mocked for now)
+ const res = await fetch('http://localhost:4000/api/auth/reset-password', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ token,
+ password: data.password,
+ confirmPassword: data.confirmPassword,
+ }),
+ });
+
+ if (!res.ok) {
+ // Optionally handle error response
+ const data = (await res.json()) as unknown;
+ let errorMessage: string | undefined;
+ if (typeof data === 'object' && data !== null && 'message' in data) {
+ const msg = (data as Record).message;
+ if (typeof msg === 'string') errorMessage = msg;
+ }
+ alert(errorMessage ?? 'Failed to reset password');
+ return;
+ }
+
+ setShowSuccess(true);
+ } catch (err) {
+ alert('Network error. Please try again.');
+ }
+ };
+
+ if (!mounted) return null;
+
+ return (
+ <>
+ {showSuccess && (
+ router.push('/login')}
+ />
+ )}
+
+ >
+ );
+}
diff --git a/src/app/(public)/reset-password/page.tsx b/src/app/(public)/reset-password/page.tsx
new file mode 100644
index 0000000..87d5ae3
--- /dev/null
+++ b/src/app/(public)/reset-password/page.tsx
@@ -0,0 +1,125 @@
+'use client';
+
+import Image from 'next/image';
+import { useRouter, useSearchParams } from 'next/navigation';
+import styled from 'styled-components';
+
+import ResetPasswordForm from './components/ResetPasswordForm';
+
+const PageContainer = styled.div`
+ min-height: 100vh;
+ background-color: #fafafa;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 20px;
+`;
+
+const FormContainer = styled.div`
+ width: 100%;
+ max-width: 700px;
+ margin: 0 auto;
+ padding: 40px;
+ padding-bottom: 100px;
+ border-radius: 24px;
+ box-shadow: 0 0 24px 0 rgba(0, 0, 0, 0.03);
+ background-color: white;
+
+ @media (max-width: 600px) {
+ padding: 20px;
+ padding-bottom: 60px;
+ }
+`;
+
+const TopRightWrapper = styled.div`
+ position: absolute;
+ top: 56px;
+ right: 80px;
+ z-index: 10;
+`;
+
+const BackButton = styled.button`
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ background: #fafafa;
+ border: 1px solid #e0e0e0;
+ border-radius: 12px;
+ font-size: 14px;
+ padding: 10px 16px;
+ cursor: pointer;
+ transition: background 0.2s;
+
+ &:hover {
+ background: #f0f0f0;
+ }
+`;
+
+const LogoContainer = styled.div`
+ display: flex;
+ justify-content: center;
+ margin-bottom: 32px;
+
+ @media (max-width: 600px) {
+ margin-bottom: 24px;
+ }
+`;
+
+const LogoImageWrapper = styled.div`
+ width: 200px;
+ height: 100px;
+
+ @media (max-width: 600px) {
+ margin-top: 32px;
+ width: 105px;
+ height: 25px;
+ }
+
+ img {
+ width: 100% !important;
+ height: 100% !important;
+ object-fit: contain;
+ }
+`;
+
+const Title = styled.h2`
+ text-align: center;
+ font-size: 22px;
+ font-weight: 600;
+ margin-bottom: 24px;
+`;
+
+export default function ResetPasswordPage() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const token = searchParams.get('token') ?? '';
+
+ return (
+
+
+ router.back()}>
+
+ Back
+
+
+
+
+
+
+
+
+ Reset Your Password
+
+
+
+ );
+}
diff --git a/src/app/(public)/reset-password/schemas/resetPasswordSchema.ts b/src/app/(public)/reset-password/schemas/resetPasswordSchema.ts
new file mode 100644
index 0000000..a32c18c
--- /dev/null
+++ b/src/app/(public)/reset-password/schemas/resetPasswordSchema.ts
@@ -0,0 +1,11 @@
+import * as z from 'zod';
+
+export const resetPasswordSchema = z
+ .object({
+ password: z.string().min(6, 'Password must be at least 6 characters'),
+ confirmPassword: z.string(),
+ })
+ .refine(data => data.password === data.confirmPassword, {
+ message: "Passwords don't match",
+ path: ['confirmPassword'],
+ });