diff --git a/worklenz-frontend/src/app/routes/auth-routes.tsx b/worklenz-frontend/src/app/routes/auth-routes.tsx index 5cddb925..b0909963 100644 --- a/worklenz-frontend/src/app/routes/auth-routes.tsx +++ b/worklenz-frontend/src/app/routes/auth-routes.tsx @@ -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 = [ { diff --git a/worklenz-frontend/src/pages/auth/authenticating.tsx b/worklenz-frontend/src/pages/auth/AuthenticatingPage.tsx similarity index 100% rename from worklenz-frontend/src/pages/auth/authenticating.tsx rename to worklenz-frontend/src/pages/auth/AuthenticatingPage.tsx diff --git a/worklenz-frontend/src/pages/auth/forgot-password-page.tsx b/worklenz-frontend/src/pages/auth/ForgotPasswordPage.tsx similarity index 95% rename from worklenz-frontend/src/pages/auth/forgot-password-page.tsx rename to worklenz-frontend/src/pages/auth/ForgotPasswordPage.tsx index 46e51259..94f2863b 100644 --- a/worklenz-frontend/src/pages/auth/forgot-password-page.tsx +++ b/worklenz-frontend/src/pages/auth/ForgotPasswordPage.tsx @@ -118,7 +118,7 @@ const ForgotPasswordPage = () => { > } - 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'})} {t('orText')} @@ -146,7 +146,7 @@ const ForgotPasswordPage = () => { borderRadius: 4, }} > - {t('returnToLoginButton')} + {t('returnToLoginButton', {defaultValue: 'Return to Login'})} diff --git a/worklenz-frontend/src/pages/auth/logging-out.tsx b/worklenz-frontend/src/pages/auth/LoggingOutPage.tsx similarity index 100% rename from worklenz-frontend/src/pages/auth/logging-out.tsx rename to worklenz-frontend/src/pages/auth/LoggingOutPage.tsx diff --git a/worklenz-frontend/src/pages/auth/login-page.tsx b/worklenz-frontend/src/pages/auth/LoginPage.tsx similarity index 100% rename from worklenz-frontend/src/pages/auth/login-page.tsx rename to worklenz-frontend/src/pages/auth/LoginPage.tsx diff --git a/worklenz-frontend/src/pages/auth/signup-page.tsx b/worklenz-frontend/src/pages/auth/SignupPage.tsx similarity index 78% rename from worklenz-frontend/src/pages/auth/signup-page.tsx rename to worklenz-frontend/src/pages/auth/SignupPage.tsx index 68d3f9e7..de6ae10a 100644 --- a/worklenz-frontend/src/pages/auth/signup-page.tsx +++ b/worklenz-frontend/src/pages/auth/SignupPage.tsx @@ -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 ( { }} variant="outlined" > - +
{ name: urlParams.name, }} > - + } - placeholder={t('namePlaceholder')} + placeholder={t('namePlaceholder', {defaultValue: 'Enter your full name'})} size="large" style={{ borderRadius: 4 }} /> - + } - placeholder={t('emailPlaceholder')} + placeholder={t('emailPlaceholder', {defaultValue: 'Enter your email'})} size="large" style={{ borderRadius: 4 }} /> - +
} - 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)} /> - - {t('passwordValidationAltText')} - +
+ {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 ( + + {passed ? ( + + ) : ( + + )} + {item.label} + + ); + })} +
@@ -416,7 +477,7 @@ const SignupPage = () => { - {t('alreadyHaveAccountText')} + {t('alreadyHaveAccountText', {defaultValue: 'Already have an account?'})} { 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 = () => { }, ]} > - } - placeholder={t('placeholder')} - size="large" - style={{ borderRadius: 4 }} - /> +
+ } + placeholder={t('placeholder')} + size="large" + style={{ borderRadius: 4 }} + value={passwordValue} + onChange={e => { + setPasswordValue(e.target.value); + if (!passwordTouched) setPasswordTouched(true); + }} + onBlur={() => setPasswordTouched(true)} + /> +
+ {passwordChecklistItems.map(item => { + const passed = item.test(passwordValue); + let color = passed + ? (themeMode === 'dark' ? '#52c41a' : '#389e0d') + : (themeMode === 'dark' ? '#b0b3b8' : '#bfbfbf'); + return ( + + {passed ? ( + + ) : ( + + )} + {item.label} + + ); + })} +
+
{ placeholder={t('confirmPasswordPlaceholder')} size="large" style={{ borderRadius: 4 }} + value={form.getFieldValue('confirmPassword') || ''} + onChange={e => form.setFieldsValue({ confirmPassword: e.target.value })} />