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:
@@ -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 = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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
|
||||||
@@ -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>
|
||||||
|
|
||||||
Reference in New Issue
Block a user