feat(auth): implement new authentication pages and refactor routes

- Added new authentication pages: LoginPage, SignupPage, ForgotPasswordPage, AuthenticatingPage, LoggingOutPage, and VerifyResetEmailPage.
- Refactored auth routes to use updated component names for better consistency and clarity.
- Enhanced user experience with improved loading states and error handling across authentication processes.
This commit is contained in:
chamikaJ
2025-07-22 12:27:05 +05:30
parent a112d39321
commit 5f86ba6b13
7 changed files with 150 additions and 29 deletions

View File

@@ -4,12 +4,12 @@ import { Navigate } from 'react-router-dom';
import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallback';
// Lazy load auth page components for better code splitting
const LoginPage = lazy(() => import('@/pages/auth/login-page'));
const SignupPage = lazy(() => import('@/pages/auth/signup-page'));
const ForgotPasswordPage = lazy(() => import('@/pages/auth/forgot-password-page'));
const LoggingOutPage = lazy(() => import('@/pages/auth/logging-out'));
const AuthenticatingPage = lazy(() => import('@/pages/auth/authenticating'));
const VerifyResetEmailPage = lazy(() => import('@/pages/auth/verify-reset-email'));
const LoginPage = lazy(() => import('@/pages/auth/LoginPage'));
const SignupPage = lazy(() => import('@/pages/auth/SignupPage'));
const ForgotPasswordPage = lazy(() => import('@/pages/auth/ForgotPasswordPage'));
const LoggingOutPage = lazy(() => import('@/pages/auth/LoggingOutPage'));
const AuthenticatingPage = lazy(() => import('@/pages/auth/AuthenticatingPage'));
const VerifyResetEmailPage = lazy(() => import('@/pages/auth/VerifyResetEmailPage'));
const authRoutes = [
{

View File

@@ -118,7 +118,7 @@ const ForgotPasswordPage = () => {
>
<Input
prefix={<UserOutlined />}
placeholder={t('emailPlaceholder')}
placeholder={t('emailPlaceholder', {defaultValue: 'Enter your email'})}
size="large"
style={{ borderRadius: 4 }}
/>
@@ -134,7 +134,7 @@ const ForgotPasswordPage = () => {
loading={isLoading}
style={{ borderRadius: 4 }}
>
{t('resetPasswordButton')}
{t('resetPasswordButton', {defaultValue: 'Reset Password'})}
</Button>
<Typography.Text style={{ textAlign: 'center' }}>{t('orText')}</Typography.Text>
<Link to="/auth/login">
@@ -146,7 +146,7 @@ const ForgotPasswordPage = () => {
borderRadius: 4,
}}
>
{t('returnToLoginButton')}
{t('returnToLoginButton', {defaultValue: 'Return to Login'})}
</Button>
</Link>
</Flex>

View File

@@ -5,6 +5,8 @@ import { useMediaQuery } from 'react-responsive';
import { LockOutlined, MailOutlined, UserOutlined } from '@ant-design/icons';
import { Form, Card, Input, Flex, Button, Typography, Space, message } from 'antd/es';
import { Rule } from 'antd/es/form';
import { CheckCircleTwoTone, CloseCircleTwoTone } from '@ant-design/icons';
import { useAppSelector } from '@/hooks/useAppSelector';
import googleIcon from '@/assets/images/google-icon.png';
import PageHeader from '@components/AuthPageHeader';
@@ -291,19 +293,51 @@ const SignupPage = () => {
password: [
{
required: true,
message: t('passwordRequired'),
message: null,
},
{
min: 8,
message: t('passwordMinCharacterRequired'),
message: null,
},
{
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#])/,
message: t('passwordPatternRequired'),
message: null,
},
],
};
const passwordChecklistItems = [
{
key: 'minLength',
test: (v: string) => v.length >= 8,
label: t('passwordChecklist.minLength', { defaultValue: 'At least 8 characters' }),
},
{
key: 'uppercase',
test: (v: string) => /[A-Z]/.test(v),
label: t('passwordChecklist.uppercase', { defaultValue: 'One uppercase letter' }),
},
{
key: 'lowercase',
test: (v: string) => /[a-z]/.test(v),
label: t('passwordChecklist.lowercase', { defaultValue: 'One lowercase letter' }),
},
{
key: 'number',
test: (v: string) => /\d/.test(v),
label: t('passwordChecklist.number', { defaultValue: 'One number' }),
},
{
key: 'special',
test: (v: string) => /[@$!%*?&#]/.test(v),
label: t('passwordChecklist.special', { defaultValue: 'One special character' }),
},
];
const themeMode = useAppSelector(state => state.themeReducer.mode);
const [passwordValue, setPasswordValue] = useState('');
const [passwordTouched, setPasswordTouched] = useState(false);
return (
<Card
style={{
@@ -317,7 +351,7 @@ const SignupPage = () => {
}}
variant="outlined"
>
<PageHeader description={t('headerDescription')} />
<PageHeader description={t('headerDescription', {defaultValue: 'Sign up to get started'})} />
<Form
form={form}
name="signup"
@@ -331,35 +365,62 @@ const SignupPage = () => {
name: urlParams.name,
}}
>
<Form.Item name="name" label={t('nameLabel')} rules={formRules.name}>
<Form.Item name="name" label={t('nameLabel', {defaultValue: 'Full Name'})} rules={formRules.name}>
<Input
prefix={<UserOutlined />}
placeholder={t('namePlaceholder')}
placeholder={t('namePlaceholder', {defaultValue: 'Enter your full name'})}
size="large"
style={{ borderRadius: 4 }}
/>
</Form.Item>
<Form.Item name="email" label={t('emailLabel')} rules={formRules.email as Rule[]}>
<Form.Item name="email" label={t('emailLabel', {defaultValue: 'Email'})} rules={formRules.email as Rule[]}>
<Input
prefix={<MailOutlined />}
placeholder={t('emailPlaceholder')}
placeholder={t('emailPlaceholder', {defaultValue: 'Enter your email'})}
size="large"
style={{ borderRadius: 4 }}
/>
</Form.Item>
<Form.Item name="password" label={t('passwordLabel')} rules={formRules.password}>
<Form.Item
name="password"
label={t('passwordLabel', {defaultValue: 'Password'})}
rules={formRules.password}
validateTrigger={['onBlur', 'onSubmit']}
>
<div>
<Input.Password
prefix={<LockOutlined />}
placeholder={t('strongPasswordPlaceholder')}
placeholder={t('strongPasswordPlaceholder', {defaultValue: 'Enter a strong password'})}
size="large"
style={{ borderRadius: 4 }}
value={passwordValue}
onChange={e => {
setPasswordValue(e.target.value);
if (!passwordTouched) setPasswordTouched(true);
}}
onBlur={() => setPasswordTouched(true)}
/>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{t('passwordValidationAltText')}
</Typography.Text>
<div style={{ marginTop: 8, marginBottom: 4 }}>
{passwordChecklistItems.map(item => {
const passed = item.test(passwordValue);
// Only green if passed, otherwise neutral (never red)
let color = passed
? (themeMode === 'dark' ? '#52c41a' : '#389e0d')
: (themeMode === 'dark' ? '#b0b3b8' : '#bfbfbf');
return (
<Flex key={item.key} align="center" gap={8} style={{ color, fontSize: 13 }}>
{passed ? (
<CheckCircleTwoTone twoToneColor={themeMode === 'dark' ? '#52c41a' : '#52c41a'} />
) : (
<CloseCircleTwoTone twoToneColor={themeMode === 'dark' ? '#b0b3b8' : '#bfbfbf'} />
)}
<span>{item.label}</span>
</Flex>
);
})}
</div>
</div>
</Form.Item>
@@ -416,7 +477,7 @@ const SignupPage = () => {
<Form.Item>
<Space>
<Typography.Text style={{ fontSize: 14 }}>
{t('alreadyHaveAccountText')}
{t('alreadyHaveAccountText', {defaultValue: 'Already have an account?'})}
</Typography.Text>
<Link

View File

@@ -4,6 +4,8 @@ import { Form, Card, Input, Flex, Button, Typography, Result } from 'antd/es';
import { LockOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
import { useMediaQuery } from 'react-responsive';
import { CheckCircleTwoTone, CloseCircleTwoTone } from '@ant-design/icons';
import { useAppSelector } from '@/hooks/useAppSelector';
import PageHeader from '@components/AuthPageHeader';
@@ -36,6 +38,36 @@ const VerifyResetEmailPage = () => {
const { t } = useTranslation('auth/verify-reset-email');
const isMobile = useMediaQuery({ query: '(max-width: 576px)' });
const themeMode = useAppSelector(state => state.themeReducer.mode);
const [passwordValue, setPasswordValue] = useState('');
const [passwordTouched, setPasswordTouched] = useState(false);
const passwordChecklistItems = [
{
key: 'minLength',
test: (v: string) => v.length >= 8,
label: t('passwordChecklist.minLength', { defaultValue: 'At least 8 characters' }),
},
{
key: 'uppercase',
test: (v: string) => /[A-Z]/.test(v),
label: t('passwordChecklist.uppercase', { defaultValue: 'One uppercase letter' }),
},
{
key: 'lowercase',
test: (v: string) => /[a-z]/.test(v),
label: t('passwordChecklist.lowercase', { defaultValue: 'One lowercase letter' }),
},
{
key: 'number',
test: (v: string) => /\d/.test(v),
label: t('passwordChecklist.number', { defaultValue: 'One number' }),
},
{
key: 'special',
test: (v: string) => /[@$!%*?&#]/.test(v),
label: t('passwordChecklist.special', { defaultValue: 'One special character' }),
},
];
useEffect(() => {
trackMixpanelEvent(evt_verify_reset_email_page_visit);
@@ -104,12 +136,38 @@ const VerifyResetEmailPage = () => {
},
]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder={t('placeholder')}
size="large"
style={{ borderRadius: 4 }}
/>
<div>
<Input.Password
prefix={<LockOutlined />}
placeholder={t('placeholder')}
size="large"
style={{ borderRadius: 4 }}
value={passwordValue}
onChange={e => {
setPasswordValue(e.target.value);
if (!passwordTouched) setPasswordTouched(true);
}}
onBlur={() => setPasswordTouched(true)}
/>
<div style={{ marginTop: 8, marginBottom: 4 }}>
{passwordChecklistItems.map(item => {
const passed = item.test(passwordValue);
let color = passed
? (themeMode === 'dark' ? '#52c41a' : '#389e0d')
: (themeMode === 'dark' ? '#b0b3b8' : '#bfbfbf');
return (
<Flex key={item.key} align="center" gap={8} style={{ color, fontSize: 13 }}>
{passed ? (
<CheckCircleTwoTone twoToneColor={themeMode === 'dark' ? '#52c41a' : '#52c41a'} />
) : (
<CloseCircleTwoTone twoToneColor={themeMode === 'dark' ? '#b0b3b8' : '#bfbfbf'} />
)}
<span>{item.label}</span>
</Flex>
);
})}
</div>
</div>
</Form.Item>
<Form.Item
name="confirmPassword"
@@ -136,6 +194,8 @@ const VerifyResetEmailPage = () => {
placeholder={t('confirmPasswordPlaceholder')}
size="large"
style={{ borderRadius: 4 }}
value={form.getFieldValue('confirmPassword') || ''}
onChange={e => form.setFieldsValue({ confirmPassword: e.target.value })}
/>
</Form.Item>