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'; import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallback';
// Lazy load auth page components for better code splitting // Lazy load auth page components for better code splitting
const LoginPage = lazy(() => import('@/pages/auth/login-page')); const LoginPage = lazy(() => import('@/pages/auth/LoginPage'));
const SignupPage = lazy(() => import('@/pages/auth/signup-page')); const SignupPage = lazy(() => import('@/pages/auth/SignupPage'));
const ForgotPasswordPage = lazy(() => import('@/pages/auth/forgot-password-page')); const ForgotPasswordPage = lazy(() => import('@/pages/auth/ForgotPasswordPage'));
const LoggingOutPage = lazy(() => import('@/pages/auth/logging-out')); const LoggingOutPage = lazy(() => import('@/pages/auth/LoggingOutPage'));
const AuthenticatingPage = lazy(() => import('@/pages/auth/authenticating')); const AuthenticatingPage = lazy(() => import('@/pages/auth/AuthenticatingPage'));
const VerifyResetEmailPage = lazy(() => import('@/pages/auth/verify-reset-email')); const VerifyResetEmailPage = lazy(() => import('@/pages/auth/VerifyResetEmailPage'));
const authRoutes = [ const authRoutes = [
{ {

View File

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

View File

@@ -5,6 +5,8 @@ import { useMediaQuery } from 'react-responsive';
import { LockOutlined, MailOutlined, UserOutlined } from '@ant-design/icons'; import { LockOutlined, MailOutlined, UserOutlined } from '@ant-design/icons';
import { Form, Card, Input, Flex, Button, Typography, Space, message } from 'antd/es'; import { Form, Card, Input, Flex, Button, Typography, Space, message } from 'antd/es';
import { Rule } from 'antd/es/form'; 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 googleIcon from '@/assets/images/google-icon.png';
import PageHeader from '@components/AuthPageHeader'; import PageHeader from '@components/AuthPageHeader';
@@ -291,19 +293,51 @@ const SignupPage = () => {
password: [ password: [
{ {
required: true, required: true,
message: t('passwordRequired'), message: null,
}, },
{ {
min: 8, min: 8,
message: t('passwordMinCharacterRequired'), message: null,
}, },
{ {
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#])/, 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 ( return (
<Card <Card
style={{ style={{
@@ -317,7 +351,7 @@ const SignupPage = () => {
}} }}
variant="outlined" variant="outlined"
> >
<PageHeader description={t('headerDescription')} /> <PageHeader description={t('headerDescription', {defaultValue: 'Sign up to get started'})} />
<Form <Form
form={form} form={form}
name="signup" name="signup"
@@ -331,35 +365,62 @@ const SignupPage = () => {
name: urlParams.name, 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 <Input
prefix={<UserOutlined />} prefix={<UserOutlined />}
placeholder={t('namePlaceholder')} placeholder={t('namePlaceholder', {defaultValue: 'Enter your full name'})}
size="large" size="large"
style={{ borderRadius: 4 }} style={{ borderRadius: 4 }}
/> />
</Form.Item> </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 <Input
prefix={<MailOutlined />} prefix={<MailOutlined />}
placeholder={t('emailPlaceholder')} placeholder={t('emailPlaceholder', {defaultValue: 'Enter your email'})}
size="large" size="large"
style={{ borderRadius: 4 }} style={{ borderRadius: 4 }}
/> />
</Form.Item> </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> <div>
<Input.Password <Input.Password
prefix={<LockOutlined />} prefix={<LockOutlined />}
placeholder={t('strongPasswordPlaceholder')} placeholder={t('strongPasswordPlaceholder', {defaultValue: 'Enter a strong password'})}
size="large" size="large"
style={{ borderRadius: 4 }} 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 }}> <div style={{ marginTop: 8, marginBottom: 4 }}>
{t('passwordValidationAltText')} {passwordChecklistItems.map(item => {
</Typography.Text> 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> </div>
</Form.Item> </Form.Item>
@@ -416,7 +477,7 @@ const SignupPage = () => {
<Form.Item> <Form.Item>
<Space> <Space>
<Typography.Text style={{ fontSize: 14 }}> <Typography.Text style={{ fontSize: 14 }}>
{t('alreadyHaveAccountText')} {t('alreadyHaveAccountText', {defaultValue: 'Already have an account?'})}
</Typography.Text> </Typography.Text>
<Link <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 { LockOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useMediaQuery } from 'react-responsive'; import { useMediaQuery } from 'react-responsive';
import { CheckCircleTwoTone, CloseCircleTwoTone } from '@ant-design/icons';
import { useAppSelector } from '@/hooks/useAppSelector';
import PageHeader from '@components/AuthPageHeader'; import PageHeader from '@components/AuthPageHeader';
@@ -36,6 +38,36 @@ const VerifyResetEmailPage = () => {
const { t } = useTranslation('auth/verify-reset-email'); const { t } = useTranslation('auth/verify-reset-email');
const isMobile = useMediaQuery({ query: '(max-width: 576px)' }); 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(() => { useEffect(() => {
trackMixpanelEvent(evt_verify_reset_email_page_visit); trackMixpanelEvent(evt_verify_reset_email_page_visit);
@@ -104,12 +136,38 @@ const VerifyResetEmailPage = () => {
}, },
]} ]}
> >
<Input.Password <div>
prefix={<LockOutlined />} <Input.Password
placeholder={t('placeholder')} prefix={<LockOutlined />}
size="large" placeholder={t('placeholder')}
style={{ borderRadius: 4 }} 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>
<Form.Item <Form.Item
name="confirmPassword" name="confirmPassword"
@@ -136,6 +194,8 @@ const VerifyResetEmailPage = () => {
placeholder={t('confirmPasswordPlaceholder')} placeholder={t('confirmPasswordPlaceholder')}
size="large" size="large"
style={{ borderRadius: 4 }} style={{ borderRadius: 4 }}
value={form.getFieldValue('confirmPassword') || ''}
onChange={e => form.setFieldsValue({ confirmPassword: e.target.value })}
/> />
</Form.Item> </Form.Item>