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 + + + + + + + + + Logo + + + Forgot Password + + Fill in your email and we'll send you a link to reset your password. + +
void handleSubmit(onSubmit)(e)} noValidate> + + + + +
+
+
+ ); +} 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')} + /> + )} +
void handleSubmit(onSubmit)(e)} noValidate> + +
+ + +
+ {formState.errors.password && ( +
+ {formState.errors.password.message} +
+ )} +
+ +
+ + +
+ {formState.errors.confirmPassword && ( +
+ {formState.errors.confirmPassword.message} +
+ )} +
+ +
+ + ); +} 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 + + + + + + Logo + + + 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'], + });