Add Forgot Password functionality and related components

- Introduced new components for the Forgot Password flow: EmailInput, OTPInput, OTPVerification, PasswordSetup, and ConfirmationMessage.
- Implemented navigation updates in DashboardCard for project links.
- Added a new decal image asset for the categories.
- Updated sidebar assets to include decals.
- Enhanced UserAuth page to include a link for forgotten passwords.
- Created a dedicated ForgotPassword page to manage the entire password recovery process.
- Added styles for the new Forgot Password components and updated existing styles for consistency.
This commit is contained in:
Nalvazhuthi
2025-08-18 10:07:47 +05:30
parent 5d17c1125b
commit cd465edc56
20 changed files with 500 additions and 44 deletions

View File

@@ -75,7 +75,7 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
setLoadingProgress(1);
setProjectName(projectName);
navigate(`/${projectId}`);
navigate(`/projects/${projectId}`);
} catch {}
};
@@ -108,7 +108,7 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
setIsKebabOpen(false);
}
} catch (error) {}
window.open(`/${projectId}`, "_blank");
window.open(`/projects/${projectId}`, "_blank");
break;
case "rename":
setIsRenaming(true);

View File

@@ -0,0 +1,15 @@
import React from 'react';
import { SuccessIcon } from '../icons/ExportCommonIcons';
const ConfirmationMessage: React.FC = () => {
return (
<div className='request-container'>
<div className="icon"><SuccessIcon /></div>
<h1 className='header'>Successfully</h1>
<p className='sub-header'>Your password has been reset successfully</p>
<a href='/' className='login'>Login</a>
</div>
);
};
export default ConfirmationMessage;

View File

@@ -0,0 +1,30 @@
import React from 'react';
interface Props {
email: string;
setEmail: (value: string) => void;
onSubmit: () => void;
}
const EmailInput: React.FC<Props> = ({ email, setEmail, onSubmit }) => {
return (
<div className='request-container'>
<h1 className='header'>Forgot password</h1>
<p className='sub-header'>
Enter your email for the verification process, we will send a 4-digit code to your email.
</p>
<form className='auth-form' onSubmit={(e) => { e.preventDefault(); onSubmit(); }}>
<input
type='email'
placeholder='Email'
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<button type='submit' className='continue-button'>Continue</button>
</form>
</div>
);
};
export default EmailInput;

View File

@@ -0,0 +1,52 @@
import React, { useState, useRef, useEffect } from 'react';
const OTPInput: React.FC<{ length?: number; onComplete: (otp: string) => void }> = ({ length = 4, onComplete }) => {
const [otpValues, setOtpValues] = useState<string[]>(Array(length).fill(''));
const inputsRef = useRef<(HTMLInputElement | null)[]>([]);
// Auto focus first input on mount
useEffect(() => {
inputsRef.current[0]?.focus();
}, []);
const handleChange = (value: string, index: number) => {
if (/^[0-9]?$/.test(value)) {
const newOtp = [...otpValues];
newOtp[index] = value;
setOtpValues(newOtp);
if (value && index < length - 1) {
inputsRef.current[index + 1]?.focus();
}
if (newOtp.every((digit) => digit !== '')) {
onComplete(newOtp.join(''));
}
}
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>, index: number) => {
if (e.key === 'Backspace' && !otpValues[index] && index > 0) {
inputsRef.current[index - 1]?.focus();
}
};
return (
<div className="otp-container">
{otpValues.map((value, index) => (
<input
key={index}
type="text"
className="otp-input"
maxLength={1}
value={value}
onChange={(e) => handleChange(e.target.value, index)}
onKeyDown={(e) => handleKeyDown(e, index)}
ref={(el) => (inputsRef.current[index] = el)}
/>
))}
</div>
);
};
export default OTPInput;

View File

@@ -0,0 +1,57 @@
import React, { useState } from 'react';
import OTPInput from './OTPInput';
interface Props {
email: string;
timer: number;
setCode: (value: string) => void;
onSubmit: () => void;
resendCode: () => void;
}
const OTPVerification: React.FC<Props> = ({ email, timer, setCode, onSubmit, resendCode }) => {
const [otp, setOtp] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log('otp.length: ', otp.length);
if (otp.length === 4) {
onSubmit();
} else {
alert('Please enter the 4-digit code');
}
};
return (
<div className='request-container'>
<h1 className='header'>Verification</h1>
<p className='sub-header'>
Enter the 4-digit code sent to <strong>{email}</strong>.
</p>
<form className='auth-form' onSubmit={handleSubmit}>
<OTPInput length={4} onComplete={(code) => { setOtp(code); setCode(code); }} />
<div className="timing">
{timer > 0
? `${String(Math.floor(timer / 60)).padStart(2, '0')}:${String(timer % 60).padStart(2, '0')}`
: ''}
</div>
<button
type='submit'
className='continue-button'
disabled={otp.length < 4} // prevent clicking if not complete
>
Verify
</button>
</form>
<div
className={`resend ${timer > 0 ? 'disabled' : ''}`}
onClick={timer === 0 ? resendCode : undefined}
style={{ cursor: timer === 0 ? 'pointer' : 'not-allowed', opacity: timer === 0 ? 1 : 0.5 }}
>
If you didnt receive a code, <span>Resend</span>
</div>
</div>
);
};
export default OTPVerification;

View File

@@ -0,0 +1,77 @@
import React, { useState } from 'react';
import { EyeIcon } from '../icons/ExportCommonIcons';
interface Props {
newPassword: string;
confirmPassword: string;
setNewPassword: (value: string) => void;
setConfirmPassword: (value: string) => void;
onSubmit: () => void;
}
const PasswordSetup: React.FC<Props> = ({
newPassword,
confirmPassword,
setNewPassword,
setConfirmPassword,
onSubmit
}) => {
const [showNewPassword, setShowNewPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
return (
<div className='request-container'>
<h1 className='header'>New Password</h1>
<p className='sub-header'>Set the new password for your account so you can login and access all features.</p>
<form
className='auth-form'
onSubmit={(e) => {
e.preventDefault();
if (newPassword !== confirmPassword) {
alert('Passwords do not match');
return;
}
onSubmit();
}}
>
<div className="password-container">
<input
type={showNewPassword ? 'text' : 'password'}
placeholder='Enter new password'
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
required
/>
<button
type="button"
className="toggle-password"
onClick={() => setShowNewPassword(prev => !prev)}
>
<EyeIcon isClosed={!showNewPassword} />
</button>
</div>
<div className="password-container">
<input
type={showConfirmPassword ? 'text' : 'password'}
placeholder='Confirm password'
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
/>
<button
type="button"
className="toggle-password"
onClick={() => setShowConfirmPassword(prev => !prev)}
>
<EyeIcon isClosed={!showConfirmPassword} />
</button>
</div>
<button type='submit' className='continue-button'>Update password</button>
</form>
</div>
);
};
export default PasswordSetup;

View File

@@ -192,7 +192,7 @@ export function DublicateIcon() {
<g clip-path="url(#clip0_1_190)">
<path d="M9 1.5H2C1.72386 1.5 1.5 1.72386 1.5 2V9C1.5 9.27615 1.27614 9.5 1 9.5C0.72386 9.5 0.5 9.27615 0.5 9V2C0.5 1.17158 1.17158 0.5 2 0.5H9C9.27615 0.5 9.5 0.72386 9.5 1C9.5 1.27614 9.27615 1.5 9 1.5Z" fill="white" />
<path d="M6.5 5.5C6.5 5.22385 6.72385 5 7 5C7.27615 5 7.5 5.22385 7.5 5.5V6.5H8.5C8.77615 6.5 9 6.72385 9 7C9 7.27615 8.77615 7.5 8.5 7.5H7.5V8.5C7.5 8.77615 7.27615 9 7 9C6.72385 9 6.5 8.77615 6.5 8.5V7.5H5.5C5.22385 7.5 5 7.27615 5 7C5 6.72385 5.22385 6.5 5.5 6.5H6.5V5.5Z" fill="white" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 2.5C10.8285 2.5 11.5 3.17158 11.5 4V10C11.5 10.8285 10.8285 11.5 10 11.5H4C3.17158 11.5 2.5 10.8285 2.5 10V4C2.5 3.17158 3.17158 2.5 4 2.5H10ZM10 3.5C10.2761 3.5 10.5 3.72386 10.5 4V10C10.5 10.2761 10.2761 10.5 10 10.5H4C3.72386 10.5 3.5 10.2761 3.5 10V4C3.5 3.72386 3.72386 3.5 4 3.5H10Z" fill="white" />
<path fillRule="evenodd" clipRule="evenodd" d="M10 2.5C10.8285 2.5 11.5 3.17158 11.5 4V10C11.5 10.8285 10.8285 11.5 10 11.5H4C3.17158 11.5 2.5 10.8285 2.5 10V4C2.5 3.17158 3.17158 2.5 4 2.5H10ZM10 3.5C10.2761 3.5 10.5 3.72386 10.5 4V10C10.5 10.2761 10.2761 10.5 10 10.5H4C3.72386 10.5 3.5 10.2761 3.5 10V4C3.5 3.72386 3.72386 3.5 4 3.5H10Z" fill="white" />
</g>
<defs>
<clipPath id="clip0_1_190">
@@ -278,7 +278,7 @@ export function MoveIcon() {
export function RotateIcon() {
return (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.99998 1.31111H4.7251L3.88539 0.471406L4.3568 0L6.00124 1.64444L4.37502 3.27067L3.90361 2.79927L4.72511 1.97777H3.99998C2.89541 1.97777 1.99998 2.87321 1.99998 3.97777H1.33331C1.33331 2.50501 2.52722 1.31111 3.99998 1.31111ZM3.99998 5.33333C3.99998 4.59696 4.59693 4 5.33331 4H10.6667C11.4031 4 12 4.59696 12 5.33333V10.6667C12 11.4031 11.4031 12 10.6667 12H5.33331C4.59693 12 3.99998 11.4031 3.99998 10.6667V5.33333ZM5.33331 4.66667H10.6667C11.0349 4.66667 11.3333 4.96514 11.3333 5.33333V10.6667C11.3333 11.0349 11.0349 11.3333 10.6667 11.3333H5.33331C4.96513 11.3333 4.66664 11.0349 4.66664 10.6667V5.33333C4.66664 4.96514 4.96513 4.66667 5.33331 4.66667Z" fill="#FCFDFD" />
<path fillRule="evenodd" clipRule="evenodd" d="M3.99998 1.31111H4.7251L3.88539 0.471406L4.3568 0L6.00124 1.64444L4.37502 3.27067L3.90361 2.79927L4.72511 1.97777H3.99998C2.89541 1.97777 1.99998 2.87321 1.99998 3.97777H1.33331C1.33331 2.50501 2.52722 1.31111 3.99998 1.31111ZM3.99998 5.33333C3.99998 4.59696 4.59693 4 5.33331 4H10.6667C11.4031 4 12 4.59696 12 5.33333V10.6667C12 11.4031 11.4031 12 10.6667 12H5.33331C4.59693 12 3.99998 11.4031 3.99998 10.6667V5.33333ZM5.33331 4.66667H10.6667C11.0349 4.66667 11.3333 4.96514 11.3333 5.33333V10.6667C11.3333 11.0349 11.0349 11.3333 10.6667 11.3333H5.33331C4.96513 11.3333 4.66664 11.0349 4.66664 10.6667V5.33333C4.66664 4.96514 4.96513 4.66667 5.33331 4.66667Z" fill="#FCFDFD" />
</svg>
);
}
@@ -286,7 +286,7 @@ export function RotateIcon() {
export function GroupIcon() {
return (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.99998 1.31111H4.7251L3.88539 0.471406L4.3568 0L6.00124 1.64444L4.37502 3.27067L3.90361 2.79927L4.72511 1.97777H3.99998C2.89541 1.97777 1.99998 2.87321 1.99998 3.97777H1.33331C1.33331 2.50501 2.52722 1.31111 3.99998 1.31111ZM3.99998 5.33333C3.99998 4.59696 4.59693 4 5.33331 4H10.6667C11.4031 4 12 4.59696 12 5.33333V10.6667C12 11.4031 11.4031 12 10.6667 12H5.33331C4.59693 12 3.99998 11.4031 3.99998 10.6667V5.33333ZM5.33331 4.66667H10.6667C11.0349 4.66667 11.3333 4.96514 11.3333 5.33333V10.6667C11.3333 11.0349 11.0349 11.3333 10.6667 11.3333H5.33331C4.96513 11.3333 4.66664 11.0349 4.66664 10.6667V5.33333C4.66664 4.96514 4.96513 4.66667 5.33331 4.66667Z" fill="#FCFDFD" />
<path fillRule="evenodd" clipRule="evenodd" d="M3.99998 1.31111H4.7251L3.88539 0.471406L4.3568 0L6.00124 1.64444L4.37502 3.27067L3.90361 2.79927L4.72511 1.97777H3.99998C2.89541 1.97777 1.99998 2.87321 1.99998 3.97777H1.33331C1.33331 2.50501 2.52722 1.31111 3.99998 1.31111ZM3.99998 5.33333C3.99998 4.59696 4.59693 4 5.33331 4H10.6667C11.4031 4 12 4.59696 12 5.33333V10.6667C12 11.4031 11.4031 12 10.6667 12H5.33331C4.59693 12 3.99998 11.4031 3.99998 10.6667V5.33333ZM5.33331 4.66667H10.6667C11.0349 4.66667 11.3333 4.96514 11.3333 5.33333V10.6667C11.3333 11.0349 11.0349 11.3333 10.6667 11.3333H5.33331C4.96513 11.3333 4.66664 11.0349 4.66664 10.6667V5.33333C4.66664 4.96514 4.96513 4.66667 5.33331 4.66667Z" fill="#FCFDFD" />
</svg>
);
}

View File

@@ -1335,3 +1335,14 @@ export const GreenTickIcon = () => {
</svg>
);
};
export const SuccessIcon = () => {
return (
<svg width="155" height="155" viewBox="0 0 155 155" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M115.596 48.1456C117.888 50.4283 117.896 54.1368 115.613 56.4288L66.2605 105.982C63.9779 108.274 60.2695 108.281 57.9775 105.999L39.9789 88.075C37.6868 85.7924 37.6791 82.0838 39.9617 79.7917C42.2443 77.4996 45.9528 77.4919 48.2449 79.7745L62.0935 93.5657L107.313 48.1624C109.596 45.8704 113.304 45.8629 115.596 48.1456Z" fill="#244A84" />
<path fillRule="evenodd" clipRule="evenodd" d="M77.1586 11.7143C46.1114 11.7143 20.065 33.3937 13.379 62.4361L13.379 62.4362C12.2906 67.1635 11.7143 72.0909 11.7143 77.1586C11.7143 113.275 41.0421 142.603 77.1586 142.603C113.275 142.603 142.603 113.275 142.603 77.1586C142.603 41.0421 113.275 11.7143 77.1586 11.7143ZM1.96331 59.808C9.8455 25.5697 40.53 0 77.1586 0C119.745 0 154.317 34.5725 154.317 77.1586C154.317 119.745 119.745 154.317 77.1586 154.317C34.5725 154.317 0 119.745 0 77.1586C0 71.1999 0.677961 65.3909 1.96331 59.808Z" fill="#C2CDE0" />
</svg>
)
}

View File

@@ -13,6 +13,7 @@ import storage from "../../../assets/image/categories/storage.png";
import office from "../../../assets/image/categories/office.png";
import safety from "../../../assets/image/categories/safety.png";
import feneration from "../../../assets/image/categories/feneration.png";
import decal from "../../../assets/image/categories/decal.png";
import SkeletonUI from "../../templates/SkeletonUI";
// -------------------------------------
@@ -86,6 +87,7 @@ const Assets: React.FC = () => {
useEffect(() => {
setCategoryList([
{ category: "Fenestration", categoryImage: feneration },
{ category: "Decals", categoryImage: decal },
{ category: "Vehicles", categoryImage: vehicle },
{ category: "Workstation", categoryImage: workStation },
{ category: "Machines", categoryImage: machines },