Skip to content
Open
Show file tree
Hide file tree
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
89 changes: 89 additions & 0 deletions src/app/(public)/forgot-password/components/SuccessModal.tsx
Original file line number Diff line number Diff line change
@@ -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 = () => (
<svg
width="60"
height="60"
viewBox="0 0 60 60"
xmlns="http://www.w3.org/2000/svg"
>
<g fill="none" fillRule="evenodd">
<path d="M0 0h60v60H0z" />
<g fillRule="nonzero">
<path
d="M50.938 27.527c0 11.683-9.662 21.345-21.856 21.345-11.683 0-21.345-9.662-21.345-21.345 0-12.194 9.662-21.856 21.345-21.856 12.194 0 21.856 9.662 21.856 21.856zm-32.331 1.162 7.107 7.363a.225.225 0 0 0 .325 0L41.392 20.93a.225.225 0 0 0 0-.325l-.93-.906c-.092-.093-.254-.116-.347-.023L26.039 31.406c-.116.07-.278.07-.371 0l-5.784-4.437c-.093-.069-.255-.046-.325.047l-.975 1.324a.28.28 0 0 0 .022.348zM43.94 5.965l2.546-2.545a1.146 1.146 0 1 1 1.62 1.62L45.56 7.585a1.146 1.146 0 0 1-1.62-1.62zm-3.417-1.638.932-3.478a1.146 1.146 0 1 1 2.213.593l-.932 3.478a1.146 1.146 0 0 1-2.213-.593zm11.078 4.81-3.263 1.523a1.146 1.146 0 1 1-.968-2.077l3.263-1.521a1.145 1.145 0 1 1 .968 2.076z"
fill="#58C112"
/>
<path
d="M15.919 57.709c0 1.265 6.008 2.291 13.418 2.291 7.411 0 13.42-1.026 13.42-2.291 0-.818-2.558-1.575-6.71-1.984-4.152-.41-9.267-.41-13.419 0-4.152.41-6.71 1.166-6.71 1.984z"
fill="#EEE"
/>
</g>
</g>
</svg>
);

interface SuccessModalProps {
email?: string;
title?: string;
description?: React.ReactNode;
onClose: () => void;
}

export default function SuccessModal({
email,
title,
description,
onClose,
}: SuccessModalProps) {
return (
<Overlay>
<ModalBox>
<CloseBtn onClick={onClose} aria-label="Close">
&times;
</CloseBtn>
<SuccessIcon />
<h3 style={{ margin: '20px 0 16px 0', fontWeight: 700, fontSize: 18 }}>
{title}
</h3>
<div style={{ color: '#444', fontSize: 14, margin: '16px 11px 0 0' }}>
{description}
</div>
</ModalBox>
</Overlay>
);
}
272 changes: 272 additions & 0 deletions src/app/(public)/forgot-password/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
style={{
minHeight: '100vh',
backgroundColor: '#fafafa',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
visibility: 'hidden',
}}
>
Loading...
</div>
);
}

return (
<PageContainer>
{showSuccess && (
<SuccessModal
title="Email sent successfully!"
description={
<>
An email has been sent to{' '}
<span style={{ color: '#0687ff' }}>{sentEmail}</span> with
instructions for resetting your password. This email may take a
few minutes to arrive in your inbox.
</>
}
onClose={() => setShowSuccess(false)}
/>
)}
<TopRightWrapper>
<BackButton onClick={() => router.back()}>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
<g fill="none" fillRule="evenodd">
<path d="M0 0h16v16H0z" />
<path
d="M6.424 3.576a.6.6 0 0 1 0 .848L3.448 7.4H14a.6.6 0 0 1 0 1.2H3.448l2.976 2.976a.6.6 0 0 1 .07.765l-.07.083a.6.6 0 0 1-.848 0l-4-4-.06-.07a.602.602 0 0 1-.006-.008l.066.078A.602.602 0 0 1 1.4 8v-.027l.004-.042L1.4 8a.602.602 0 0 1 .176-.424l4-4a.6.6 0 0 1 .848 0z"
fill="#5A5A5A"
fillRule="nonzero"
/>
</g>
</svg>
Back
</BackButton>
</TopRightWrapper>
<FormContainer as={RelativeContainer}>
<IconWrapper>
<button
type="button"
onClick={() => router.back()}
style={{
background: 'none',
border: 'none',
padding: 0,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
}}
aria-label="Go back"
>
<svg
width="20"
height="20"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<g fill="none" fillRule="evenodd">
<path d="M0 0h20v20H0z" />
<path
d="M12.47 3.47a.75.75 0 0 1 1.06 1.06L8.061 10l5.47 5.47a.75.75 0 0 1 .072.976l-.073.084a.75.75 0 0 1-1.06 0l-6-6a.75.75 0 0 1 0-1.06l6-6z"
fill="#060606"
fillRule="nonzero"
/>
</g>
</svg>
</button>
</IconWrapper>
<LogoContainer>
<LogoImageWrapper>
<Image src="/logo.svg" alt="Logo" width={200} height={100} />
</LogoImageWrapper>
</LogoContainer>
<Title>Forgot Password</Title>
<Subtitle>
Fill in your email and we'll send you a link to reset your password.
</Subtitle>
<form onSubmit={e => void handleSubmit(onSubmit)(e)} noValidate>
<FormField label="Email address" mb={0}>
<ControllerInput
name="email"
control={control}
placeholder="Email address"
/>
</FormField>
<Button type="submit" fullWidth sx={{ mt: 2 }}>
Send
</Button>
</form>
</FormContainer>
</PageContainer>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { z } from 'zod';

import { emailSchema } from '@/app/(public)/login/schemas/loginSchema';

export const forgotPasswordSchema = z.object({
email: emailSchema,
});
3 changes: 1 addition & 2 deletions src/app/(public)/login/component/FormField.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Loading