fix: localize the login pages

This commit is contained in:
mengstabketemaw
2026-07-11 12:41:09 +03:00
parent e8c02ab853
commit 15514fe5dd
14 changed files with 595 additions and 159 deletions

View File

@@ -15,6 +15,8 @@ import {
type BoxProps,
} from '@mantine/core';
import { IconCheck, IconMoon, IconSun } from '@tabler/icons-react';
import { useTranslation } from 'react-i18next';
import { LanguageSwitcher } from '@ema-platform/ui';
const LOGO_URL = '/brand/ema-white.png';
@@ -60,23 +62,26 @@ function ThemeToggle() {
);
}
const FEATURES = [
'Submit applications online, 24/7',
'Real-time status tracking & alerts',
'Available in English & አማርኛ',
const FEATURE_KEYS = [
'auth.hero.featureOnline',
'auth.hero.featureTracking',
'auth.hero.featureBilingual',
];
interface AuthShellProps {
children: ReactNode;
brandTitle?: string;
brandSubtitle?: string;
supportedLanguages?: readonly string[];
}
export function AuthShell({
children,
brandTitle = 'Maritime licensing, made simple.',
brandSubtitle = 'Apply for vessel and seafarer licenses, upload documents, and track every application in one secure portal.',
brandTitle,
brandSubtitle,
supportedLanguages = ['en', 'am'],
}: AuthShellProps) {
const { t } = useTranslation();
const theme = useMantineTheme();
const heroGradient = theme.other.heroGradient as string;
const computed = useComputedColorScheme('light');
@@ -91,6 +96,18 @@ export function AuthShell({
}}
p="md"
>
{/* Language switcher — top-left corner */}
<Box
style={{
position: 'absolute',
top: rem(16),
left: rem(16),
zIndex: 10,
}}
>
<LanguageSwitcher supportedLanguages={supportedLanguages} />
</Box>
{/* Theme toggle — top-right corner */}
<Box
style={{
@@ -174,16 +191,16 @@ export function AuthShell({
<Stack gap={6}>
<Title order={2} c="white" fz={rem(28)} lh={1.2} fw={700}>
{brandTitle}
{brandTitle ?? t('auth.hero.brandTitle')}
</Title>
<Text fz="sm" lh={1.6} style={{ color: 'rgba(255,255,255,0.85)' }}>
{brandSubtitle}
{t('auth.hero.brandSubtitle')}
</Text>
</Stack>
<Stack gap="sm">
{FEATURES.map((feature) => (
<Group key={feature} gap="sm" wrap="nowrap">
{FEATURE_KEYS.map((key) => (
<Group key={key} gap="sm" wrap="nowrap">
<Center
w={22}
h={22}
@@ -196,7 +213,7 @@ export function AuthShell({
<IconCheck size={12} color="white" stroke={2.4} />
</Center>
<Text c="white" fz="sm" fw={500}>
{feature}
{t(key)}
</Text>
</Group>
))}

View File

@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { Alert, Center, Loader, Stack, Text, Title } from '@mantine/core';
import { IconAlertCircle, IconShieldCheck } from '@tabler/icons-react';
import { useTranslation } from 'react-i18next';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useApiMutation } from '@ema-platform/api';
import { notify } from '@ema-platform/ui';
@@ -18,6 +19,7 @@ interface FaydaUserInfo {
}
export function FaydaCallbackPage() {
const { t } = useTranslation();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const calledRef = useRef(false);
@@ -33,7 +35,7 @@ export function FaydaCallbackPage() {
const state = searchParams.get('state');
if (!code || !state) {
const msg = 'Invalid response from National ID service. Missing code or state.';
const msg = t('auth.faydaInvalidResponse');
setError(msg);
notify.error(msg);
return;
@@ -43,7 +45,7 @@ export function FaydaCallbackPage() {
sessionStorage.removeItem('fayda_authId');
if (!authId) {
const msg = 'Session expired. Please try signing up again.';
const msg = t('auth.faydaSessionExpired');
setError(msg);
notify.error(msg);
return;
@@ -60,12 +62,12 @@ export function FaydaCallbackPage() {
sessionStorage.setItem('fayda_userId', userInfo.sub);
sessionStorage.setItem('fayda_userInfo', JSON.stringify(userInfo));
notify.success('National ID verified successfully');
notify.success(t('auth.faydaVerified'));
navigate('/set-password', { replace: true });
} catch (err: unknown) {
const msg =
(err as { data?: { message?: string } })?.data?.message ??
(err instanceof Error ? err.message : 'Failed to complete National ID verification');
(err instanceof Error ? err.message : t('auth.faydaVerifyFailed'));
setError(msg);
notify.error(msg);
}
@@ -76,19 +78,17 @@ export function FaydaCallbackPage() {
return (
<AuthShell
brandTitle="Verifying your National ID..."
brandSubtitle="Please wait while we securely verify your identity with the National ID service."
brandTitle={t('auth.faydaBrandTitle')}
brandSubtitle={t('auth.faydaBrandSubtitle')}
>
<Stack gap="lg" align="center">
<IconShieldCheck size={56} stroke={1.5} />
<div>
<Title order={2} fz={30} ta="center">
{error ? 'Verification failed' : 'Verifying your identity'}
{error ? t('auth.faydaFailedTitle') : t('auth.faydaVerifying')}
</Title>
<Text c="dimmed" mt={6} ta="center">
{error
? error
: 'We are securely processing your National ID information. This should only take a moment.'}
{error ? error : t('auth.faydaProcessing')}
</Text>
</div>
@@ -100,7 +100,7 @@ export function FaydaCallbackPage() {
{error}
</Alert>
<Text size="sm" c="dimmed" ta="center">
Please try again or use the standard sign-up method instead.
{t('auth.faydaTryAgain')}
</Text>
</>
)}

View File

@@ -17,6 +17,7 @@ import {
IconMailForward,
IconSend,
} from '@tabler/icons-react';
import { useTranslation } from 'react-i18next';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
@@ -31,11 +32,8 @@ const schema = z.object({
type FormValues = z.infer<typeof schema>;
interface ForgotPasswordPageProps {
appName?: string;
}
export function ForgotPasswordPage({ appName = 'Portal' }: ForgotPasswordPageProps) {
export function ForgotPasswordPage() {
const { t } = useTranslation();
const [forgotTrigger, { isLoading }] = useApiMutation();
const [sentTo, setSentTo] = useState<string | null>(null);
const [serverError, setServerError] = useState<string | null>(null);
@@ -63,7 +61,7 @@ export function ForgotPasswordPage({ appName = 'Portal' }: ForgotPasswordPagePro
} catch (err: unknown) {
const msg =
(err as { data?: { message?: string } })?.data?.message ??
(err instanceof Error ? err.message : 'Something went wrong');
(err instanceof Error ? err.message : t('common.somethingWentWrong'));
setServerError(msg);
notify.error(msg);
}
@@ -73,11 +71,11 @@ export function ForgotPasswordPage({ appName = 'Portal' }: ForgotPasswordPagePro
if (!sentTo || isLoading) return;
try {
await sendResetLink(sentTo);
notify.success('Reset link sent again');
notify.success(t('auth.resetLinkSentAgain'));
} catch (err: unknown) {
const msg =
(err as { data?: { message?: string } })?.data?.message ??
(err instanceof Error ? err.message : 'Something went wrong');
(err instanceof Error ? err.message : t('common.somethingWentWrong'));
setServerError(msg);
notify.error(msg);
}
@@ -93,7 +91,7 @@ export function ForgotPasswordPage({ appName = 'Portal' }: ForgotPasswordPagePro
style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}
>
<IconArrowLeft size={16} />
Back to sign in
{t('auth.backToSignIn')}
</Anchor>
</Center>
);
@@ -101,8 +99,8 @@ export function ForgotPasswordPage({ appName = 'Portal' }: ForgotPasswordPagePro
if (sentTo) {
return (
<AuthShell
brandTitle="Reset your password securely."
brandSubtitle={`We'll email you a secure link to set a new password and get you back into ${appName}.`}
brandTitle={t('auth.resetPasswordBrandTitle')}
brandSubtitle={t('auth.resetPasswordBrandSubtitle')}
>
<Stack gap="lg">
<ThemeIcon size={56} radius="md" variant="light" color="emaPrimary">
@@ -111,14 +109,10 @@ export function ForgotPasswordPage({ appName = 'Portal' }: ForgotPasswordPagePro
<div>
<Title order={2} fz={30}>
Check your email
{t('auth.checkYourEmail')}
</Title>
<Text c="dimmed" mt={6}>
We&apos;ve sent a password reset link to{' '}
<Text span fw={600} c="dark">
{sentTo}
</Text>
. Follow the link in that email to choose a new password.
{t('auth.resetLinkSent', { email: sentTo })}
</Text>
</div>
@@ -131,12 +125,12 @@ export function ForgotPasswordPage({ appName = 'Portal' }: ForgotPasswordPagePro
size="md"
leftSection={<IconMail size={18} />}
>
Open email app
{t('auth.openEmailApp')}
</Button>
<Group justify="center" gap={6}>
<Text size="sm" c="dimmed">
Didn&apos;t get the email?
{t('auth.didNotGetEmail')}
</Text>
<Anchor
size="sm"
@@ -144,7 +138,7 @@ export function ForgotPasswordPage({ appName = 'Portal' }: ForgotPasswordPagePro
onClick={handleResend}
style={isLoading ? { pointerEvents: 'none', opacity: 0.6 } : undefined}
>
Resend link
{t('auth.resendLink')}
</Anchor>
</Group>
@@ -156,17 +150,16 @@ export function ForgotPasswordPage({ appName = 'Portal' }: ForgotPasswordPagePro
return (
<AuthShell
brandTitle="Reset your password securely."
brandSubtitle={`We'll email you a secure link to set a new password and get you back into ${appName}.`}
brandTitle={t('auth.resetPasswordBrandTitle')}
brandSubtitle={t('auth.resetPasswordBrandSubtitle')}
>
<Stack gap="lg">
<div>
<Title order={2} fz={30}>
Forgot your password?
{t('auth.forgotYourPassword')}
</Title>
<Text c="dimmed" mt={6}>
Enter the email linked to your account and we&apos;ll send you a link
to reset your password.
{t('auth.forgotDescription')}
</Text>
</div>
@@ -179,11 +172,11 @@ export function ForgotPasswordPage({ appName = 'Portal' }: ForgotPasswordPagePro
<form onSubmit={handleSubmit(onSubmit)}>
<Stack gap="md">
<TextInput
label="Email address"
placeholder="you@example.com"
label={t('auth.emailAddress')}
placeholder={t('auth.emailPlaceholder')}
size="md"
leftSection={<IconMail size={18} />}
error={errors.email?.message}
error={errors.email ? t('auth.validation.emailInvalid') : undefined}
{...register('email')}
/>
@@ -194,7 +187,7 @@ export function ForgotPasswordPage({ appName = 'Portal' }: ForgotPasswordPagePro
size="md"
rightSection={<IconSend size={18} />}
>
Send reset link
{t('auth.sendResetLink')}
</Button>
</Stack>
</form>

View File

@@ -21,6 +21,7 @@ import {
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useTranslation } from 'react-i18next';
import { useNavigate, Link } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { useApiMutation } from '@ema-platform/api';
@@ -44,10 +45,11 @@ interface LoginPageProps {
}
export function LoginPage({
appName = 'Portal',
appName,
enableSignup = true,
enableForgotPassword = true,
}: LoginPageProps) {
const { t } = useTranslation();
const navigate = useNavigate();
const dispatch = useDispatch();
const [isLoading, setIsLoading] = useState(false);
@@ -118,7 +120,7 @@ export function LoginPage({
} catch (err: unknown) {
const msg =
(err as { data?: { message?: string } })?.data?.message ??
(err instanceof Error ? err.message : 'Something went wrong');
(err instanceof Error ? err.message : t('common.somethingWentWrong'));
setServerError(msg);
notify.error(msg);
} finally {
@@ -131,10 +133,10 @@ export function LoginPage({
<Stack gap="lg">
<div>
<Title order={2} fz={30}>
Welcome to {appName}
{t('auth.welcomeTo', { appName: appName ?? t('app.name') })}
</Title>
<Text c="dimmed" mt={6}>
Sign in to access your account.
{t('auth.signInToAccess')}
</Text>
</div>
@@ -147,25 +149,25 @@ export function LoginPage({
<form onSubmit={handleSubmit(onSubmit)}>
<Stack gap="md">
<TextInput
label="Email or phone"
placeholder="you@example.com"
label={t('auth.emailOrPhone')}
placeholder={t('auth.emailPlaceholder')}
size="md"
leftSection={<IconMail size={18} />}
error={errors.email?.message}
error={errors.email ? t('auth.validation.emailInvalid') : undefined}
{...register('email')}
/>
<PasswordInput
label="Password"
placeholder="Your password"
label={t('auth.password')}
placeholder={t('auth.passwordPlaceholder')}
size="md"
leftSection={<IconLock size={18} />}
error={errors.password?.message}
error={errors.password ? t('auth.validation.passwordMin', { min: 6 }) : undefined}
{...register('password')}
/>
<Group justify="space-between">
<Checkbox
label="Remember me"
label={t('auth.rememberMe')}
size="sm"
checked={rememberMe}
onChange={(e) => setRememberMe(e.currentTarget.checked)}
@@ -177,7 +179,7 @@ export function LoginPage({
size="sm"
fw={600}
>
Forgot password?
{t('auth.forgotPassword')}
</Anchor>
)}
</Group>
@@ -189,12 +191,12 @@ export function LoginPage({
size="md"
rightSection={<IconArrowRight size={18} />}
>
Sign in
{t('auth.signIn')}
</Button>
</Stack>
</form>
<Divider label="or" labelPosition="center" />
<Divider label={t('common.or')} labelPosition="center" />
<Button
variant="default"
@@ -203,14 +205,14 @@ export function LoginPage({
leftSection={<IconDeviceMobile size={18} />}
onClick={() => notify.info('Phone sign-in is coming soon.')}
>
Sign in with phone number
{t('auth.signInWithPhone')}
</Button>
{enableSignup && (
<Text ta="center" size="sm" c="dimmed">
Don&apos;t have an account?{' '}
{t('auth.noAccount')}{' '}
<Anchor component={Link} to="/signup" fw={700}>
Create one
{t('auth.createOne')}
</Anchor>
</Text>
)}

View File

@@ -16,6 +16,7 @@ import { IconArrowRight, IconShieldLock } from '@tabler/icons-react';
import { Controller, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useTranslation } from 'react-i18next';
import { useNavigate, useLocation } from 'react-router-dom';
import { useApiMutation } from '@ema-platform/api';
import { notify } from '@ema-platform/ui';
@@ -25,14 +26,13 @@ const CODE_LENGTH = 6;
const RESEND_SECONDS = 30;
const schema = z.object({
verificationCode: z
.string()
.length(CODE_LENGTH, { message: `Enter the ${CODE_LENGTH}-digit code` }),
verificationCode: z.string().length(CODE_LENGTH),
});
type FormValues = z.infer<typeof schema>;
export function OTPVerificationPage() {
const { t } = useTranslation();
const navigate = useNavigate();
const location = useLocation();
const state = location.state as
@@ -70,12 +70,12 @@ export function OTPVerificationPage() {
body: { email, phoneNumber, verificationCode: values.verificationCode },
}).unwrap();
notify.success('Phone number verified successfully');
notify.success(t('auth.phoneVerified'));
navigate(needsProfile ? '/profile-setup' : '/dashboard');
} catch (err: unknown) {
const msg =
(err as { data?: { message?: string } })?.data?.message ??
(err instanceof Error ? err.message : 'Something went wrong');
(err instanceof Error ? err.message : t('common.somethingWentWrong'));
setServerError(msg);
notify.error(msg);
}
@@ -90,13 +90,13 @@ export function OTPVerificationPage() {
body: { email, phoneNumber, type: 'verify-phone-number' },
}).unwrap();
notify.success('Verification code resent to your email');
notify.success(t('auth.codeResent'));
setSecondsLeft(RESEND_SECONDS);
setServerError(null);
} catch (err: unknown) {
const msg =
(err as { data?: { message?: string } })?.data?.message ??
(err instanceof Error ? err.message : 'Something went wrong');
(err instanceof Error ? err.message : t('common.somethingWentWrong'));
setServerError(msg);
notify.error(msg);
}
@@ -104,8 +104,8 @@ export function OTPVerificationPage() {
return (
<AuthShell
brandTitle="One last step to secure your account."
brandSubtitle="We use a one-time code to confirm it's really you before granting access."
brandTitle={t('auth.otpBrandTitle')}
brandSubtitle={t('auth.otpBrandSubtitle')}
>
<Stack gap="lg">
<ThemeIcon size={56} radius="md" variant="light" color="emaPrimary">
@@ -114,14 +114,10 @@ export function OTPVerificationPage() {
<div>
<Title order={2} fz={30}>
Verify your account
{t('auth.verifyAccount')}
</Title>
<Text c="dimmed" mt={6}>
A verification code has been sent to{' '}
<Text span fw={600} c="dark">
{email || 'your email'}
</Text>
. Enter it below to continue.
{t('auth.otpSent', { email: email || t('auth.emailPlaceholder') })}
</Text>
</div>
@@ -150,9 +146,9 @@ export function OTPVerificationPage() {
onChange={field.onChange}
onComplete={() => handleSubmit(onSubmit)()}
/>
{errors.verificationCode?.message && (
{errors.verificationCode && (
<Text c="red" size="sm">
{errors.verificationCode.message}
{t('auth.otpDigitCode', { length: CODE_LENGTH })}
</Text>
)}
</Stack>
@@ -165,13 +161,13 @@ export function OTPVerificationPage() {
size="md"
rightSection={<IconArrowRight size={18} />}
>
Verify
{t('auth.verify')}
</Button>
</Stack>
</form>
<Divider
label="Having trouble?"
label={t('auth.havingTrouble')}
labelPosition="center"
variant="dashed"
/>
@@ -182,17 +178,17 @@ export function OTPVerificationPage() {
size="md"
onClick={() => navigate('/dashboard')}
>
Skip verification for now
{t('auth.skipVerification')}
</Button>
<Center>
<Group justify="center" gap={6}>
<Text size="sm" c="dimmed">
Didn&apos;t receive a code?
{t('auth.noCode')}
</Text>
{secondsLeft > 0 ? (
<Text size="sm" c="dimmed" fw={600}>
Resend in {secondsLeft}s
{t('auth.resendIn', { seconds: secondsLeft })}
</Text>
) : (
<Anchor
@@ -203,7 +199,7 @@ export function OTPVerificationPage() {
resending ? { pointerEvents: 'none', opacity: 0.6 } : undefined
}
>
Resend code
{t('auth.resendCode')}
</Anchor>
)}
</Group>

View File

@@ -4,6 +4,7 @@ import { IconArrowRight, IconLock, IconLockOpen } from '@tabler/icons-react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { useApiMutation } from '@ema-platform/api';
import { notify } from '@ema-platform/ui';
@@ -11,8 +12,8 @@ import { AuthShell } from '../components/AuthShell';
const schema = z
.object({
newPassword: z.string().min(8, { message: 'Password must be at least 8 characters' }),
confirmPassword: z.string().min(8, { message: 'Confirm your password' }),
newPassword: z.string().min(8),
confirmPassword: z.string().min(8),
})
.refine((data) => data.newPassword === data.confirmPassword, {
message: 'Passwords do not match',
@@ -22,6 +23,7 @@ const schema = z
type FormValues = z.infer<typeof schema>;
export function SetPasswordPage() {
const { t } = useTranslation();
const navigate = useNavigate();
const [serverError, setServerError] = useState<string | null>(null);
const [setPasswordTrigger, { isLoading }] = useApiMutation();
@@ -49,7 +51,7 @@ export function SetPasswordPage() {
const onSubmit = async (values: FormValues) => {
if (!userId) {
notify.error('Session expired. Please sign up again.');
notify.error(t('auth.sessionExpired'));
navigate('/signup', { replace: true });
return;
}
@@ -68,12 +70,12 @@ export function SetPasswordPage() {
sessionStorage.removeItem('fayda_userId');
sessionStorage.removeItem('fayda_userInfo');
notify.success('Password set successfully');
notify.success(t('auth.passwordSet'));
navigate('/login', { replace: true });
} catch (err: unknown) {
const msg =
(err as { data?: { message?: string } })?.data?.message ??
(err instanceof Error ? err.message : 'Something went wrong');
(err instanceof Error ? err.message : t('common.somethingWentWrong'));
setServerError(msg);
notify.error(msg);
}
@@ -81,8 +83,8 @@ export function SetPasswordPage() {
return (
<AuthShell
brandTitle="Set your account password."
brandSubtitle="Choose a strong password to secure your account and access all Portal features."
brandTitle={t('auth.setPasswordBrandTitle')}
brandSubtitle={t('auth.setPasswordBrandSubtitle')}
>
<Stack gap="lg">
<ThemeIcon size={56} radius="md" variant="light" color="emaPrimary">
@@ -91,11 +93,10 @@ export function SetPasswordPage() {
<div>
<Title order={2} fz={30}>
Set your password
{t('auth.setPassword')}
</Title>
<Text c="dimmed" mt={6}>
Welcome, <Text span fw={600}>{userName}</Text>. Your National ID has been verified successfully.
Choose a password to complete your account setup.
{t('auth.welcomeNationalId', { name: userName })}
</Text>
</div>
@@ -108,17 +109,23 @@ export function SetPasswordPage() {
<form onSubmit={handleSubmit(onSubmit)}>
<Stack gap="md">
<PasswordInput
label="New password"
placeholder="At least 8 characters"
label={t('auth.newPassword')}
placeholder={t('auth.passwordPlaceholder')}
leftSection={<IconLock size={18} />}
error={errors.newPassword?.message}
error={errors.newPassword ? t('auth.validation.passwordMin', { min: 8 }) : undefined}
{...register('newPassword')}
/>
<PasswordInput
label="Confirm password"
placeholder="Re-enter password"
label={t('auth.confirmPassword')}
placeholder={t('auth.confirmPasswordPlaceholder')}
leftSection={<IconLock size={18} />}
error={errors.confirmPassword?.message}
error={
errors.confirmPassword?.message === 'Passwords do not match'
? t('auth.validation.passwordMismatch')
: errors.confirmPassword
? t('auth.validation.confirmRequired')
: undefined
}
{...register('confirmPassword')}
/>
@@ -129,7 +136,7 @@ export function SetPasswordPage() {
size="md"
rightSection={<IconArrowRight size={18} />}
>
Set password &amp; continue
{t('auth.setPasswordAndContinue')}
</Button>
</Stack>
</form>

View File

@@ -25,6 +25,7 @@ import {
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useTranslation } from 'react-i18next';
import { useNavigate, Link } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { useApiMutation } from '@ema-platform/api';
@@ -64,11 +65,8 @@ interface SignupPayload {
confirmPassword: string;
}
interface SignupPageProps {
appName?: string;
}
export function SignupPage({ appName = 'Portal' }: SignupPageProps) {
export function SignupPage() {
const { t } = useTranslation();
const navigate = useNavigate();
const dispatch = useDispatch();
const [agreed, setAgreed] = useState(false);
@@ -101,7 +99,7 @@ export function SignupPage({ appName = 'Portal' }: SignupPageProps) {
} catch (err: unknown) {
const msg =
(err as { data?: { message?: string } })?.data?.message ??
(err instanceof Error ? err.message : 'Failed to initiate National ID verification');
(err instanceof Error ? err.message : t('auth.faydaFailed'));
setServerError(msg);
notify.error(msg);
}
@@ -150,7 +148,7 @@ export function SignupPage({ appName = 'Portal' }: SignupPageProps) {
} catch (err: unknown) {
const msg =
(err as { data?: { message?: string } })?.data?.message ??
(err instanceof Error ? err.message : 'Something went wrong');
(err instanceof Error ? err.message : t('common.somethingWentWrong'));
setServerError(msg);
notify.error(msg);
}
@@ -158,16 +156,16 @@ export function SignupPage({ appName = 'Portal' }: SignupPageProps) {
return (
<AuthShell
brandTitle={`Join ${appName}'s community.`}
brandSubtitle={`Create your account to access ${appName} features.`}
brandTitle={t('auth.signupBrandTitle')}
brandSubtitle={t('auth.signupBrandSubtitle')}
>
<Stack gap="lg">
<div>
<Title order={2} fz={30}>
Create account
{t('auth.createAccount')}
</Title>
<Text c="dimmed" mt={6}>
It only takes a minute to get started.
{t('auth.signupDescription')}
</Text>
</div>
@@ -181,59 +179,64 @@ export function SignupPage({ appName = 'Portal' }: SignupPageProps) {
<Stack gap="md">
<SimpleGrid cols={{ base: 1, xs: 2 }} spacing="md">
<TextInput
label="Name (English)"
placeholder="Abebe Bekele"
label={t('auth.nameEn')}
placeholder={t('auth.nameEnPlaceholder')}
leftSection={<IconUser size={18} />}
error={errors.nameEn?.message}
error={errors.nameEn ? t('auth.validation.nameEnRequired') : undefined}
{...register('nameEn')}
/>
<TextInput
label="Name (Amharic)"
placeholder="ስም"
label={t('auth.nameAm')}
placeholder={t('auth.nameAmPlaceholder')}
leftSection={<IconUser size={18} />}
error={errors.nameAm?.message}
{...register('nameAm')}
/>
</SimpleGrid>
<SimpleGrid cols={{ base: 1, xs: 2 }} spacing="md">
<TextInput
label="Email address"
placeholder="you@example.com"
label={t('auth.emailAddress')}
placeholder={t('auth.emailPlaceholder')}
leftSection={<IconMail size={18} />}
error={errors.email?.message}
error={errors.email ? t('auth.validation.emailInvalid') : undefined}
{...register('email')}
/>
<TextInput
label="Username"
placeholder="Choose a username"
label={t('auth.username')}
placeholder={t('auth.usernamePlaceholder')}
leftSection={<IconAt size={18} />}
error={errors.username?.message}
error={errors.username ? t('auth.validation.usernameMin') : undefined}
{...register('username')}
/>
</SimpleGrid>
<TextInput
label="Phone number"
placeholder="+251 911 234 567"
label={t('auth.phoneNumber')}
placeholder={t('auth.phonePlaceholder')}
leftSection={<IconDeviceMobile size={18} />}
error={errors.phoneNumber?.message}
error={errors.phoneNumber ? t('auth.validation.phoneRequired') : undefined}
{...register('phoneNumber')}
/>
<SimpleGrid cols={{ base: 1, xs: 2 }} spacing="md">
<PasswordInput
label="Password"
placeholder="At least 8 characters"
label={t('auth.password')}
placeholder={t('auth.passwordPlaceholder')}
leftSection={<IconLock size={18} />}
error={errors.password?.message}
error={errors.password ? t('auth.validation.passwordMin', { min: 8 }) : undefined}
{...register('password')}
/>
<PasswordInput
label="Confirm password"
placeholder="Re-enter password"
label={t('auth.confirmPassword')}
placeholder={t('auth.confirmPasswordPlaceholder')}
leftSection={<IconLock size={18} />}
error={errors.confirmPassword?.message}
error={
errors.confirmPassword?.message === 'Passwords do not match'
? t('auth.validation.passwordMismatch')
: errors.confirmPassword
? t('auth.validation.confirmRequired')
: undefined
}
{...register('confirmPassword')}
/>
</SimpleGrid>
@@ -244,13 +247,13 @@ export function SignupPage({ appName = 'Portal' }: SignupPageProps) {
onChange={(e) => setAgreed(e.currentTarget.checked)}
label={
<Text size="sm">
I agree to the{' '}
{t('auth.agreeToTerms')}{' '}
<Anchor
size="sm"
fw={600}
onClick={(e) => e.preventDefault()}
>
Terms &amp; Privacy Policy
{t('auth.termsAndPrivacy')}
</Anchor>
</Text>
}
@@ -264,10 +267,10 @@ export function SignupPage({ appName = 'Portal' }: SignupPageProps) {
size="md"
rightSection={<IconArrowRight size={18} />}
>
Create account
{t('auth.createAccount')}
</Button>
<Divider label="or" labelPosition="center" variant="dashed" />
<Divider label={t('common.or')} labelPosition="center" variant="dashed" />
<Button
type="button"
@@ -277,15 +280,15 @@ export function SignupPage({ appName = 'Portal' }: SignupPageProps) {
leftSection={<IconId size={18} />}
onClick={handleFaydaSignup}
>
Sign up with National ID (Fayda)
{t('auth.signupWithFayda')}
</Button>
</Stack>
</form>
<Text ta="center" size="sm" c="dimmed">
Already have an account?{' '}
{t('auth.alreadyHaveAccount')}{' '}
<Anchor component={Link} to="/login" fw={700}>
Sign in
{t('auth.signIn')}
</Anchor>
</Text>
</Stack>