mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 13:02:50 +00:00
fix: localize the login pages
This commit is contained in:
@@ -48,6 +48,111 @@ export const am: Translations = {
|
|||||||
collapse: 'ሰብስብ',
|
collapse: 'ሰብስብ',
|
||||||
expand: 'ዘርጋ',
|
expand: 'ዘርጋ',
|
||||||
toggleTheme: 'የብርሃን / ጨለማ ሁነታን ይቀይሩ',
|
toggleTheme: 'የብርሃን / ጨለማ ሁነታን ይቀይሩ',
|
||||||
|
or: 'ወይም',
|
||||||
|
somethingWentWrong: 'የሆነ ስህተት ተከስቷል',
|
||||||
|
},
|
||||||
|
|
||||||
|
auth: {
|
||||||
|
login: 'ግባ',
|
||||||
|
logout: 'ውጣ',
|
||||||
|
signup: 'ይመዝገቡ',
|
||||||
|
account: 'መለያ',
|
||||||
|
guest: 'እንግዳ ተጠቃሚ',
|
||||||
|
welcomeTo: 'እንኳን ወደ {{appName}} በደህና መጡ',
|
||||||
|
signInToAccess: 'መለያዎን ለመድረስ ይግቡ።',
|
||||||
|
emailOrPhone: 'ኢሜይል ወይም ስልክ',
|
||||||
|
emailPlaceholder: 'you@example.com',
|
||||||
|
password: 'የይለፍ ቃል',
|
||||||
|
passwordPlaceholder: 'የይለፍ ቃልዎ',
|
||||||
|
rememberMe: 'አስታውሰኝ',
|
||||||
|
forgotPassword: 'የይለፍ ቃል ረሱት?',
|
||||||
|
signIn: 'ግባ',
|
||||||
|
signInWithPhone: 'በስልክ ቁጥር ይግቡ',
|
||||||
|
noAccount: 'መለያ የለዎትም?',
|
||||||
|
createOne: 'አዲስ ይፍጠሩ',
|
||||||
|
forgotYourPassword: 'የይለፍ ቃልዎን ረሱት?',
|
||||||
|
forgotDescription:
|
||||||
|
'ከመለያዎ ጋር የተገናኘውን ኢሜይል ያስገቡ፤ የይለፍ ቃልዎን እንደገና ለማስተካከል አገናኝ እንልክልዎታለን።',
|
||||||
|
emailAddress: 'የኢሜይል አድራሻ',
|
||||||
|
sendResetLink: 'የማስተካከያ አገናኝ ይላኩ',
|
||||||
|
backToSignIn: 'ወደ መግቢያ ተመለስ',
|
||||||
|
checkYourEmail: 'ኢሜይልዎን ያረጋግጡ',
|
||||||
|
resetLinkSent:
|
||||||
|
'የይለፍ ቃል ማስተካከያ አገናኝ ወደ {{email}} ልከናል። አዲስ የይለፍ ቃል ለመምረጥ በኢሜይሉ ውስጥ ያለውን አገናኝ ይከተሉ።',
|
||||||
|
openEmailApp: 'የኢሜይል መተግበሪያ ይክፈቱ',
|
||||||
|
didNotGetEmail: 'ኢሜይሉ አልደረሰዎትም?',
|
||||||
|
resendLink: 'አገናኙን እንደገና ይላኩ',
|
||||||
|
resetLinkSentAgain: 'የማስተካከያ አገናኝ እንደገና ተልኳል',
|
||||||
|
createAccount: 'መለያ ይፍጠሩ',
|
||||||
|
signupDescription: 'ለመጀመር አንድ ደቂቃ ብቻ ይወስዳል።',
|
||||||
|
nameEn: 'ስም (እንግሊዝኛ)',
|
||||||
|
nameEnPlaceholder: 'አበበ በቀለ',
|
||||||
|
nameAm: 'ስም (አማርኛ)',
|
||||||
|
nameAmPlaceholder: 'ሙሉ ስም',
|
||||||
|
username: 'የተጠቃሚ ስም',
|
||||||
|
usernamePlaceholder: 'የተጠቃሚ ስም ይምረጡ',
|
||||||
|
phoneNumber: 'ስልክ ቁጥር',
|
||||||
|
phonePlaceholder: '+251 911 234 567',
|
||||||
|
confirmPassword: 'የይለፍ ቃል ያረጋግጡ',
|
||||||
|
confirmPasswordPlaceholder: 'የይለፍ ቃሉን እንደገና ያስገቡ',
|
||||||
|
agreeToTerms: 'እኔ እስማማለሁ በ',
|
||||||
|
termsAndPrivacy: 'ደንቦች እና የግላዊነት ፖሊሲ',
|
||||||
|
signupWithFayda: 'በብሔራዊ መታወቂያ (ፋይዳ) ይመዝገቡ',
|
||||||
|
alreadyHaveAccount: 'መለያ አለዎት?',
|
||||||
|
faydaFailed: 'የብሔራዊ መታወቂያ ማረጋገጫ መጀመር አልተቻለም',
|
||||||
|
resetPasswordBrandTitle: 'የይለፍ ቃልዎን በአስተማማኝ ሁኔታ ያስተካክሉ።',
|
||||||
|
resetPasswordBrandSubtitle:
|
||||||
|
'አይጨነቁ \u2014 አዲስ የይለፍ ቃል ለመፍጠር አስተማማኝ አገናኝ እንልክልዎታለን።',
|
||||||
|
signupBrandTitle: 'ጉዞዎ እዚህ ይጀምራል።',
|
||||||
|
signupBrandSubtitle: 'መለያ ይፍጠሩ እና የማይቻል የሚመስሉ ነገሮችን ይክፈቱ።',
|
||||||
|
verifyAccount: 'መለያዎን ያረጋግጡ',
|
||||||
|
otpSent: 'የማረጋገጫ ኮድ ወደ {{email}} ተልኳል። ለመቀጠል ከዚህ በታች ያስገቡት።',
|
||||||
|
verify: 'አረጋግጥ',
|
||||||
|
havingTrouble: 'ችግር አለብዎት?',
|
||||||
|
skipVerification: 'ለአሁን ማረጋገጫውን ዘልለው',
|
||||||
|
noCode: 'ኮድ አልደረሰዎትም?',
|
||||||
|
resendIn: 'እንደገና ይላኩ በ{{seconds}}ሰ',
|
||||||
|
resendCode: 'ኮዱን እንደገና ይላኩ',
|
||||||
|
otpDigitCode: 'የ{{length}}-አሃዝ ኮዱን ያስገቡ',
|
||||||
|
phoneVerified: 'ስልክ ቁጥር በተሳካ ሁኔታ ተረጋግጧል',
|
||||||
|
codeResent: 'የማረጋገጫ ኮድ እንደገና ወደ ኢሜይልዎ ተልኳል',
|
||||||
|
otpBrandTitle: 'መለያዎን ለማስጠበቅ የመጨረሻው እርምጃ።',
|
||||||
|
otpBrandSubtitle: 'መዳረሻ ከመስጠታችን በፊት እርስዎ መሆንዎን ለማረጋገጥ የአንድ ጊዜ ኮድ እንጠቀማለን።',
|
||||||
|
setPassword: 'የይለፍ ቃልዎን ያዘጋጁ',
|
||||||
|
newPassword: 'አዲስ የይለፍ ቃል',
|
||||||
|
setPasswordBrandTitle: 'የመለያ ይለፍ ቃልዎን ያዘጋጁ።',
|
||||||
|
setPasswordBrandSubtitle: 'መለያዎን ለማስጠበቅ እና ሁሉንም የፖርታል ተግባራት ለመጠቀም ጠንካራ የይለፍ ቃል ይምረጡ።',
|
||||||
|
welcomeNationalId: 'እንኳን ደህና መጡ፣ {{name}}። የብሔራዊ መታወቂያዎ በተሳካ ሁኔታ ተረጋግጧል። የመለያ ዝግጅትዎን ለማጠናቀቅ የይለፍ ቃል ይምረጡ።',
|
||||||
|
setPasswordAndContinue: 'የይለፍ ቃል ያዘጋጁ እና ይቀጥሉ',
|
||||||
|
passwordSet: 'የይለፍ ቃል በተሳካ ሁኔታ ተዘጋጅቷል',
|
||||||
|
sessionExpired: 'ክፍለ ጊዜ ጊዜው አልፏል። እባክዎ እንደገና ይመዝገቡ።',
|
||||||
|
faydaBrandTitle: 'የብሔራዊ መታወቂያዎን በማረጋገጥ ላይ...',
|
||||||
|
faydaBrandSubtitle: 'እባክዎ ማንነትዎን ከብሔራዊ መታወቂያ አገልግሎት ደህንነቱ በተጠበቀ ሁኔታ ስናረጋግጥ ይጠብቁ።',
|
||||||
|
faydaVerifying: 'ማንነትዎን በማረጋገጥ ላይ',
|
||||||
|
faydaFailedTitle: 'ማረጋገጫ አልተሳካም',
|
||||||
|
faydaProcessing: 'የብሔራዊ መታወቂያ መረጃዎን በደህንነት እያስኬድን ነው። ይህ ጥቂት ጊዜ ብቻ ይወስዳል።',
|
||||||
|
faydaTryAgain: 'እባክዎ እንደገና ይሞክሩ ወይም መደበኛውን የምዝገባ ዘዴ ይጠቀሙ።',
|
||||||
|
faydaInvalidResponse: 'ከብሔራዊ መታወቂያ አገልግሎት የተሳሳተ ምላሽ። ኮድ ወይም ሁኔታ የለም።',
|
||||||
|
faydaSessionExpired: 'ክፍለ ጊዜ ጊዜው አልፏል። እባክዎ እንደገና ለመመዝገብ ይሞክሩ።',
|
||||||
|
faydaVerifyFailed: 'የብሔራዊ መታወቂያ ማረጋገጫን ማጠናቀቅ አልተቻለም',
|
||||||
|
faydaVerified: 'ብሔራዊ መታወቂያ በተሳካ ሁኔታ ተረጋግጧል',
|
||||||
|
hero: {
|
||||||
|
brandTitle: 'የባሕር ፍቃድ አሰጣጥ፣ ቀላል ተደረገ።',
|
||||||
|
brandSubtitle:
|
||||||
|
'ለመርከብ እና ለመርከበኛ ፈቃዶች ያመልክቱ፣ ሰነዶችን ይስቀሉ እና እያንዳንዱን ማመልከቻ በአንድ ደህንነቱ በተጠበቀ ፖርታል ይከታተሉ።',
|
||||||
|
featureOnline: 'ማመልከቻዎችን በመስመር ያቅርቡ፣ ቀንና ሌሊት',
|
||||||
|
featureTracking: 'በእውነተኛ ጊዜ የሁኔታ ክትትል እና ማሳወቂያዎች',
|
||||||
|
featureBilingual: 'በእንግሊዝኛ እና በአማርኛ ይገኛል',
|
||||||
|
},
|
||||||
|
validation: {
|
||||||
|
emailInvalid: 'ትክክለኛ ኢሜይል ያስገቡ',
|
||||||
|
passwordMin: 'የይለፍ ቃል ቢያንስ {{min}} ቁምፊዎች መሆን አለበት',
|
||||||
|
usernameMin: 'የተጠቃሚ ስም ቢያንስ 3 ቁምፊዎች መሆን አለበት',
|
||||||
|
phoneRequired: 'ስልክ ቁጥር ያስፈልጋል',
|
||||||
|
nameEnRequired: 'ስም (እንግሊዝኛ) ያስፈልጋል',
|
||||||
|
confirmRequired: 'የይለፍ ቃልዎን ያረጋግጡ',
|
||||||
|
passwordMismatch: 'የይለፍ ቃላት አይዛመዱም',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
breadcrumbs: {
|
breadcrumbs: {
|
||||||
|
|||||||
@@ -46,6 +46,118 @@ export const en = {
|
|||||||
collapse: 'Collapse',
|
collapse: 'Collapse',
|
||||||
expand: 'Expand',
|
expand: 'Expand',
|
||||||
toggleTheme: 'Toggle light / dark mode',
|
toggleTheme: 'Toggle light / dark mode',
|
||||||
|
or: 'or',
|
||||||
|
somethingWentWrong: 'Something went wrong',
|
||||||
|
},
|
||||||
|
|
||||||
|
auth: {
|
||||||
|
login: 'Log in',
|
||||||
|
logout: 'Log out',
|
||||||
|
signup: 'Sign up',
|
||||||
|
account: 'Account',
|
||||||
|
guest: 'Guest user',
|
||||||
|
welcomeTo: 'Welcome to {{appName}}',
|
||||||
|
signInToAccess: 'Sign in to access your account.',
|
||||||
|
emailOrPhone: 'Email or phone',
|
||||||
|
emailPlaceholder: 'you@example.com',
|
||||||
|
password: 'Password',
|
||||||
|
passwordPlaceholder: 'Your password',
|
||||||
|
rememberMe: 'Remember me',
|
||||||
|
forgotPassword: 'Forgot password?',
|
||||||
|
signIn: 'Sign in',
|
||||||
|
signInWithPhone: 'Sign in with phone number',
|
||||||
|
noAccount: 'Don\u2019t have an account?',
|
||||||
|
createOne: 'Create one',
|
||||||
|
forgotYourPassword: 'Forgot your password?',
|
||||||
|
forgotDescription:
|
||||||
|
'Enter the email linked to your account and we\u2019ll send you a link to reset your password.',
|
||||||
|
emailAddress: 'Email address',
|
||||||
|
sendResetLink: 'Send reset link',
|
||||||
|
backToSignIn: 'Back to sign in',
|
||||||
|
checkYourEmail: 'Check your email',
|
||||||
|
resetLinkSent:
|
||||||
|
'We\u2019ve sent a password reset link to {{email}}. Follow the link in that email to choose a new password.',
|
||||||
|
openEmailApp: 'Open email app',
|
||||||
|
didNotGetEmail: 'Didn\u2019t get the email?',
|
||||||
|
resendLink: 'Resend link',
|
||||||
|
resetLinkSentAgain: 'Reset link sent again',
|
||||||
|
createAccount: 'Create account',
|
||||||
|
signupDescription: 'It only takes a minute to get started.',
|
||||||
|
nameEn: 'Name (English)',
|
||||||
|
nameEnPlaceholder: 'Abebe Bekele',
|
||||||
|
nameAm: 'Name (Amharic)',
|
||||||
|
nameAmPlaceholder: '\u1235\u121d',
|
||||||
|
username: 'Username',
|
||||||
|
usernamePlaceholder: 'Choose a username',
|
||||||
|
phoneNumber: 'Phone number',
|
||||||
|
phonePlaceholder: '+251 911 234 567',
|
||||||
|
confirmPassword: 'Confirm password',
|
||||||
|
confirmPasswordPlaceholder: 'Re-enter password',
|
||||||
|
agreeToTerms: 'I agree to the',
|
||||||
|
termsAndPrivacy: 'Terms & Privacy Policy',
|
||||||
|
signupWithFayda: 'Sign up with National ID (Fayda)',
|
||||||
|
alreadyHaveAccount: 'Already have an account?',
|
||||||
|
faydaFailed: 'Failed to initiate National ID verification',
|
||||||
|
resetPasswordBrandTitle: 'Reset your password securely.',
|
||||||
|
resetPasswordBrandSubtitle:
|
||||||
|
'No worries \u2014 we\u2019ll send you a secure link to create a new password and get you back on track.',
|
||||||
|
signupBrandTitle: 'Your journey begins here.',
|
||||||
|
signupBrandSubtitle: 'Create an account and unlock a world of possibilities.',
|
||||||
|
verifyAccount: 'Verify your account',
|
||||||
|
otpSent: 'A verification code has been sent to {{email}}. Enter it below to continue.',
|
||||||
|
verify: 'Verify',
|
||||||
|
havingTrouble: 'Having trouble?',
|
||||||
|
skipVerification: 'Skip verification for now',
|
||||||
|
noCode: 'Didn\u2019t receive a code?',
|
||||||
|
resendIn: 'Resend in {{seconds}}s',
|
||||||
|
resendCode: 'Resend code',
|
||||||
|
otpDigitCode: 'Enter the {{length}}-digit code',
|
||||||
|
phoneVerified: 'Phone number verified successfully',
|
||||||
|
codeResent: 'Verification code resent to your email',
|
||||||
|
otpBrandTitle: 'One last step to secure your account.',
|
||||||
|
otpBrandSubtitle:
|
||||||
|
'We use a one-time code to confirm it\u2019s really you before granting access.',
|
||||||
|
setPassword: 'Set your password',
|
||||||
|
newPassword: 'New password',
|
||||||
|
setPasswordBrandTitle: 'Set your account password.',
|
||||||
|
setPasswordBrandSubtitle:
|
||||||
|
'Choose a strong password to secure your account and access all Portal features.',
|
||||||
|
welcomeNationalId:
|
||||||
|
'Welcome, {{name}}. Your National ID has been verified successfully. Choose a password to complete your account setup.',
|
||||||
|
setPasswordAndContinue: 'Set password & continue',
|
||||||
|
passwordSet: 'Password set successfully',
|
||||||
|
sessionExpired: 'Session expired. Please sign up again.',
|
||||||
|
faydaBrandTitle: 'Verifying your National ID...',
|
||||||
|
faydaBrandSubtitle:
|
||||||
|
'Please wait while we securely verify your identity with the National ID service.',
|
||||||
|
faydaVerifying: 'Verifying your identity',
|
||||||
|
faydaFailedTitle: 'Verification failed',
|
||||||
|
faydaProcessing:
|
||||||
|
'We are securely processing your National ID information. This should only take a moment.',
|
||||||
|
faydaTryAgain:
|
||||||
|
'Please try again or use the standard sign-up method instead.',
|
||||||
|
faydaInvalidResponse:
|
||||||
|
'Invalid response from National ID service. Missing code or state.',
|
||||||
|
faydaSessionExpired: 'Session expired. Please try signing up again.',
|
||||||
|
faydaVerifyFailed: 'Failed to complete National ID verification',
|
||||||
|
faydaVerified: 'National ID verified successfully',
|
||||||
|
hero: {
|
||||||
|
brandTitle: 'Maritime licensing, made simple.',
|
||||||
|
brandSubtitle:
|
||||||
|
'Apply for vessel and seafarer licenses, upload documents, and track every application in one secure portal.',
|
||||||
|
featureOnline: 'Submit applications online, 24/7',
|
||||||
|
featureTracking: 'Real-time status tracking & alerts',
|
||||||
|
featureBilingual: 'Available in English & አማርኛ',
|
||||||
|
},
|
||||||
|
validation: {
|
||||||
|
emailInvalid: 'Enter a valid email',
|
||||||
|
passwordMin: 'Password must be at least {{min}} characters',
|
||||||
|
usernameMin: 'Username must be at least 3 characters',
|
||||||
|
phoneRequired: 'Phone number is required',
|
||||||
|
nameEnRequired: 'Name (English) is required',
|
||||||
|
confirmRequired: 'Confirm your password',
|
||||||
|
passwordMismatch: 'Passwords do not match',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
breadcrumbs: {
|
breadcrumbs: {
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ const router = createBrowserRouter([
|
|||||||
{
|
{
|
||||||
element: <AuthLayout />,
|
element: <AuthLayout />,
|
||||||
children: [
|
children: [
|
||||||
{ path: '/login', element: <LoginPage appName="Backoffice" enableSignup={false} /> },
|
{ path: '/login', element: <LoginPage enableSignup={false} /> },
|
||||||
{ path: '/forgot-password', element: <ForgotPasswordPage appName="Backoffice" /> },
|
{ path: '/forgot-password', element: <ForgotPasswordPage /> },
|
||||||
{ path: '/otp-verify', element: <OTPVerificationPage /> },
|
{ path: '/otp-verify', element: <OTPVerificationPage /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { Translations } from './en';
|
|||||||
|
|
||||||
export const am: Translations = {
|
export const am: Translations = {
|
||||||
app: {
|
app: {
|
||||||
name: 'ኢባባ ፖርታል',
|
name: 'ኢማ ፖርታል',
|
||||||
authority: 'የኢትዮጵያ ባሕር ጉዳዮች ባለሥልጣን',
|
authority: 'የኢትዮጵያ ባሕር ጉዳዮች ባለሥልጣን',
|
||||||
tagline: 'የባሕር ፍቃድና የምስክር ወረቀት አገልግሎቶች',
|
tagline: 'የባሕር ፍቃድና የምስክር ወረቀት አገልግሎቶች',
|
||||||
},
|
},
|
||||||
@@ -58,6 +58,8 @@ export const am: Translations = {
|
|||||||
learnMore: 'ተጨማሪ ይወቁ',
|
learnMore: 'ተጨማሪ ይወቁ',
|
||||||
toggleTheme: 'ብርሃን / ጨለማ ገጽታ ቀይር',
|
toggleTheme: 'ብርሃን / ጨለማ ገጽታ ቀይር',
|
||||||
welcome: 'እንኳን ደህና መጡ',
|
welcome: 'እንኳን ደህና መጡ',
|
||||||
|
or: 'ወይም',
|
||||||
|
somethingWentWrong: 'የሆነ ስህተት ተከስቷል',
|
||||||
},
|
},
|
||||||
|
|
||||||
auth: {
|
auth: {
|
||||||
@@ -66,6 +68,101 @@ export const am: Translations = {
|
|||||||
signup: 'ይመዝገቡ',
|
signup: 'ይመዝገቡ',
|
||||||
account: 'መለያ',
|
account: 'መለያ',
|
||||||
guest: 'እንግዳ ተጠቃሚ',
|
guest: 'እንግዳ ተጠቃሚ',
|
||||||
|
welcomeTo: 'እንኳን ወደ {{appName}} በደህና መጡ',
|
||||||
|
signInToAccess: 'መለያዎን ለመድረስ ይግቡ።',
|
||||||
|
emailOrPhone: 'ኢሜይል ወይም ስልክ',
|
||||||
|
emailPlaceholder: 'you@example.com',
|
||||||
|
password: 'የይለፍ ቃል',
|
||||||
|
passwordPlaceholder: 'የይለፍ ቃልዎ',
|
||||||
|
rememberMe: 'አስታውሰኝ',
|
||||||
|
forgotPassword: 'የይለፍ ቃል ረሱት?',
|
||||||
|
signIn: 'ግባ',
|
||||||
|
signInWithPhone: 'በስልክ ቁጥር ይግቡ',
|
||||||
|
noAccount: 'መለያ የለዎትም?',
|
||||||
|
createOne: 'አዲስ ይፍጠሩ',
|
||||||
|
forgotYourPassword: 'የይለፍ ቃልዎን ረሱት?',
|
||||||
|
forgotDescription:
|
||||||
|
'ከመለያዎ ጋር የተገናኘውን ኢሜይል ያስገቡ፤ የይለፍ ቃልዎን እንደገና ለማስተካከል አገናኝ እንልክልዎታለን።',
|
||||||
|
emailAddress: 'የኢሜይል አድራሻ',
|
||||||
|
sendResetLink: 'የማስተካከያ አገናኝ ይላኩ',
|
||||||
|
backToSignIn: 'ወደ መግቢያ ተመለስ',
|
||||||
|
checkYourEmail: 'ኢሜይልዎን ያረጋግጡ',
|
||||||
|
resetLinkSent:
|
||||||
|
'የይለፍ ቃል ማስተካከያ አገናኝ ወደ {{email}} ልከናል። አዲስ የይለፍ ቃል ለመምረጥ በኢሜይሉ ውስጥ ያለውን አገናኝ ይከተሉ።',
|
||||||
|
openEmailApp: 'የኢሜይል መተግበሪያ ይክፈቱ',
|
||||||
|
didNotGetEmail: 'ኢሜይሉ አልደረሰዎትም?',
|
||||||
|
resendLink: 'አገናኙን እንደገና ይላኩ',
|
||||||
|
resetLinkSentAgain: 'የማስተካከያ አገናኝ እንደገና ተልኳል',
|
||||||
|
createAccount: 'መለያ ይፍጠሩ',
|
||||||
|
signupDescription: 'ለመጀመር አንድ ደቂቃ ብቻ ይወስዳል።',
|
||||||
|
nameEn: 'ስም (እንግሊዝኛ)',
|
||||||
|
nameEnPlaceholder: 'Abebe Bekila',
|
||||||
|
nameAm: 'ስም (አማርኛ)',
|
||||||
|
nameAmPlaceholder: 'አበበ በቀለ',
|
||||||
|
username: 'የተጠቃሚ ስም',
|
||||||
|
usernamePlaceholder: 'የተጠቃሚ ስም ይምረጡ',
|
||||||
|
phoneNumber: 'ስልክ ቁጥር',
|
||||||
|
phonePlaceholder: '+251 911 234 567',
|
||||||
|
confirmPassword: 'የይለፍ ቃል ያረጋግጡ',
|
||||||
|
confirmPasswordPlaceholder: 'የይለፍ ቃሉን እንደገና ያስገቡ',
|
||||||
|
agreeToTerms: 'እኔ እስማማለሁ በ',
|
||||||
|
termsAndPrivacy: 'ደንቦች እና የግላዊነት ፖሊሲ',
|
||||||
|
signupWithFayda: 'በብሔራዊ መታወቂያ (ፋይዳ) ይመዝገቡ',
|
||||||
|
alreadyHaveAccount: 'መለያ አለዎት?',
|
||||||
|
faydaFailed: 'የብሔራዊ መታወቂያ ማረጋገጫ መጀመር አልተቻለም',
|
||||||
|
resetPasswordBrandTitle: 'የይለፍ ቃልዎን በአስተማማኝ ሁኔታ ያስተካክሉ።',
|
||||||
|
resetPasswordBrandSubtitle:
|
||||||
|
'አይጨነቁ \u2014 አዲስ የይለፍ ቃል ለመፍጠር አስተማማኝ አገናኝ እንልክልዎታለን።',
|
||||||
|
signupBrandTitle: 'ጉዞዎ እዚህ ይጀምራል።',
|
||||||
|
signupBrandSubtitle: 'መለያ ይፍጠሩ እና የማይቻል የሚመስሉ ነገሮችን ይክፈቱ።',
|
||||||
|
verifyAccount: 'መለያዎን ያረጋግጡ',
|
||||||
|
otpSent: 'የማረጋገጫ ኮድ ወደ {{email}} ተልኳል። ለመቀጠል ከዚህ በታች ያስገቡት።',
|
||||||
|
verify: 'አረጋግጥ',
|
||||||
|
havingTrouble: 'ችግር አለብዎት?',
|
||||||
|
skipVerification: 'ለአሁን ማረጋገጫውን ዘልለው',
|
||||||
|
noCode: 'ኮድ አልደረሰዎትም?',
|
||||||
|
resendIn: 'እንደገና ይላኩ በ{{seconds}}ሰ',
|
||||||
|
resendCode: 'ኮዱን እንደገና ይላኩ',
|
||||||
|
otpDigitCode: 'የ{{length}}-አሃዝ ኮዱን ያስገቡ',
|
||||||
|
phoneVerified: 'ስልክ ቁጥር በተሳካ ሁኔታ ተረጋግጧል',
|
||||||
|
codeResent: 'የማረጋገጫ ኮድ እንደገና ወደ ኢሜይልዎ ተልኳል',
|
||||||
|
otpBrandTitle: 'መለያዎን ለማስጠበቅ የመጨረሻው እርምጃ።',
|
||||||
|
otpBrandSubtitle: 'መዳረሻ ከመስጠታችን በፊት እርስዎ መሆንዎን ለማረጋገጥ የአንድ ጊዜ ኮድ እንጠቀማለን።',
|
||||||
|
setPassword: 'የይለፍ ቃልዎን ያዘጋጁ',
|
||||||
|
newPassword: 'አዲስ የይለፍ ቃል',
|
||||||
|
setPasswordBrandTitle: 'የመለያ ይለፍ ቃልዎን ያዘጋጁ።',
|
||||||
|
setPasswordBrandSubtitle: 'መለያዎን ለማስጠበቅ እና ሁሉንም የፖርታል ተግባራት ለመጠቀም ጠንካራ የይለፍ ቃል ይምረጡ።',
|
||||||
|
welcomeNationalId: 'እንኳን ደህና መጡ፣ {{name}}። የብሔራዊ መታወቂያዎ በተሳካ ሁኔታ ተረጋግጧል። የመለያ ዝግጅትዎን ለማጠናቀቅ የይለፍ ቃል ይምረጡ።',
|
||||||
|
setPasswordAndContinue: 'የይለፍ ቃል ያዘጋጁ እና ይቀጥሉ',
|
||||||
|
passwordSet: 'የይለፍ ቃል በተሳካ ሁኔታ ተዘጋጅቷል',
|
||||||
|
sessionExpired: 'ክፍለ ጊዜ ጊዜው አልፏል። እባክዎ እንደገና ይመዝገቡ።',
|
||||||
|
faydaBrandTitle: 'የብሔራዊ መታወቂያዎን በማረጋገጥ ላይ...',
|
||||||
|
faydaBrandSubtitle: 'እባክዎ ማንነትዎን ከብሔራዊ መታወቂያ አገልግሎት ደህንነቱ በተጠበቀ ሁኔታ ስናረጋግጥ ይጠብቁ።',
|
||||||
|
faydaVerifying: 'ማንነትዎን በማረጋገጥ ላይ',
|
||||||
|
faydaFailedTitle: 'ማረጋገጫ አልተሳካም',
|
||||||
|
faydaProcessing: 'የብሔራዊ መታወቂያ መረጃዎን በደህንነት እያስኬድን ነው። ይህ ጥቂት ጊዜ ብቻ ይወስዳል።',
|
||||||
|
faydaTryAgain: 'እባክዎ እንደገና ይሞክሩ ወይም መደበኛውን የምዝገባ ዘዴ ይጠቀሙ።',
|
||||||
|
faydaInvalidResponse: 'ከብሔራዊ መታወቂያ አገልግሎት የተሳሳተ ምላሽ። ኮድ ወይም ሁኔታ የለም።',
|
||||||
|
faydaSessionExpired: 'ክፍለ ጊዜ ጊዜው አልፏል። እባክዎ እንደገና ለመመዝገብ ይሞክሩ።',
|
||||||
|
faydaVerifyFailed: 'የብሔራዊ መታወቂያ ማረጋገጫን ማጠናቀቅ አልተቻለም',
|
||||||
|
faydaVerified: 'ብሔራዊ መታወቂያ በተሳካ ሁኔታ ተረጋግጧል',
|
||||||
|
hero: {
|
||||||
|
brandTitle: 'የባሕር ፍቃድ አሰጣጥ፣ ቀላል ተደረገ።',
|
||||||
|
brandSubtitle:
|
||||||
|
'ለመርከብ እና ለመርከበኛ ፈቃዶች ያመልክቱ፣ ሰነዶችን ይስቀሉ እና እያንዳንዱን ማመልከቻ በአንድ ደህንነቱ በተጠበቀ ፖርታል ይከታተሉ።',
|
||||||
|
featureOnline: 'ማመልከቻዎችን በመስመር ያቅርቡ፣ ቀንና ሌሊት',
|
||||||
|
featureTracking: 'በእውነተኛ ጊዜ የሁኔታ ክትትል እና ማሳወቂያዎች',
|
||||||
|
featureBilingual: 'በእንግሊዝኛ እና በአማርኛ ይገኛል',
|
||||||
|
},
|
||||||
|
validation: {
|
||||||
|
emailInvalid: 'ትክክለኛ ኢሜይል ያስገቡ',
|
||||||
|
passwordMin: 'የይለፍ ቃል ቢያንስ {{min}} ቁምፊዎች መሆን አለበት',
|
||||||
|
usernameMin: 'የተጠቃሚ ስም ቢያንስ 3 ቁምፊዎች መሆን አለበት',
|
||||||
|
phoneRequired: 'ስልክ ቁጥር ያስፈልጋል',
|
||||||
|
nameEnRequired: 'ስም (እንግሊዝኛ) ያስፈልጋል',
|
||||||
|
confirmRequired: 'የይለፍ ቃልዎን ያረጋግጡ',
|
||||||
|
passwordMismatch: 'የይለፍ ቃላት አይዛመዱም',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
dashboard: {
|
dashboard: {
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ export const en = {
|
|||||||
learnMore: 'Learn more',
|
learnMore: 'Learn more',
|
||||||
toggleTheme: 'Toggle light / dark mode',
|
toggleTheme: 'Toggle light / dark mode',
|
||||||
welcome: 'Welcome',
|
welcome: 'Welcome',
|
||||||
|
or: 'or',
|
||||||
|
somethingWentWrong: 'Something went wrong',
|
||||||
},
|
},
|
||||||
|
|
||||||
auth: {
|
auth: {
|
||||||
@@ -64,6 +66,108 @@ export const en = {
|
|||||||
signup: 'Sign up',
|
signup: 'Sign up',
|
||||||
account: 'Account',
|
account: 'Account',
|
||||||
guest: 'Guest user',
|
guest: 'Guest user',
|
||||||
|
welcomeTo: 'Welcome to {{appName}}',
|
||||||
|
signInToAccess: 'Sign in to access your account.',
|
||||||
|
emailOrPhone: 'Email or phone',
|
||||||
|
emailPlaceholder: 'you@example.com',
|
||||||
|
password: 'Password',
|
||||||
|
passwordPlaceholder: 'Your password',
|
||||||
|
rememberMe: 'Remember me',
|
||||||
|
forgotPassword: 'Forgot password?',
|
||||||
|
signIn: 'Sign in',
|
||||||
|
signInWithPhone: 'Sign in with phone number',
|
||||||
|
noAccount: 'Don\u2019t have an account?',
|
||||||
|
createOne: 'Create one',
|
||||||
|
forgotYourPassword: 'Forgot your password?',
|
||||||
|
forgotDescription:
|
||||||
|
'Enter the email linked to your account and we\u2019ll send you a link to reset your password.',
|
||||||
|
emailAddress: 'Email address',
|
||||||
|
sendResetLink: 'Send reset link',
|
||||||
|
backToSignIn: 'Back to sign in',
|
||||||
|
checkYourEmail: 'Check your email',
|
||||||
|
resetLinkSent:
|
||||||
|
'We\u2019ve sent a password reset link to {{email}}. Follow the link in that email to choose a new password.',
|
||||||
|
openEmailApp: 'Open email app',
|
||||||
|
didNotGetEmail: 'Didn\u2019t get the email?',
|
||||||
|
resendLink: 'Resend link',
|
||||||
|
resetLinkSentAgain: 'Reset link sent again',
|
||||||
|
createAccount: 'Create account',
|
||||||
|
signupDescription: 'It only takes a minute to get started.',
|
||||||
|
nameEn: 'Name (English)',
|
||||||
|
nameEnPlaceholder: 'Abebe Bekele',
|
||||||
|
nameAm: 'Name (Amharic)',
|
||||||
|
nameAmPlaceholder: 'አበበ በቂላ',
|
||||||
|
username: 'Username',
|
||||||
|
usernamePlaceholder: 'Choose a username',
|
||||||
|
phoneNumber: 'Phone number',
|
||||||
|
phonePlaceholder: '+251 911 234 567',
|
||||||
|
confirmPassword: 'Confirm password',
|
||||||
|
confirmPasswordPlaceholder: 'Re-enter password',
|
||||||
|
agreeToTerms: 'I agree to the',
|
||||||
|
termsAndPrivacy: 'Terms & Privacy Policy',
|
||||||
|
signupWithFayda: 'Sign up with National ID (Fayda)',
|
||||||
|
alreadyHaveAccount: 'Already have an account?',
|
||||||
|
faydaFailed: 'Failed to initiate National ID verification',
|
||||||
|
resetPasswordBrandTitle: 'Reset your password securely.',
|
||||||
|
resetPasswordBrandSubtitle:
|
||||||
|
'No worries \u2014 we\u2019ll send you a secure link to create a new password and get you back on track.',
|
||||||
|
signupBrandTitle: 'Your journey begins here.',
|
||||||
|
signupBrandSubtitle: 'Create an account and unlock a world of possibilities.',
|
||||||
|
verifyAccount: 'Verify your account',
|
||||||
|
otpSent: 'A verification code has been sent to {{email}}. Enter it below to continue.',
|
||||||
|
verify: 'Verify',
|
||||||
|
havingTrouble: 'Having trouble?',
|
||||||
|
skipVerification: 'Skip verification for now',
|
||||||
|
noCode: 'Didn\u2019t receive a code?',
|
||||||
|
resendIn: 'Resend in {{seconds}}s',
|
||||||
|
resendCode: 'Resend code',
|
||||||
|
otpDigitCode: 'Enter the {{length}}-digit code',
|
||||||
|
phoneVerified: 'Phone number verified successfully',
|
||||||
|
codeResent: 'Verification code resent to your email',
|
||||||
|
otpBrandTitle: 'One last step to secure your account.',
|
||||||
|
otpBrandSubtitle:
|
||||||
|
'We use a one-time code to confirm it\u2019s really you before granting access.',
|
||||||
|
setPassword: 'Set your password',
|
||||||
|
newPassword: 'New password',
|
||||||
|
setPasswordBrandTitle: 'Set your account password.',
|
||||||
|
setPasswordBrandSubtitle:
|
||||||
|
'Choose a strong password to secure your account and access all Portal features.',
|
||||||
|
welcomeNationalId:
|
||||||
|
'Welcome, {{name}}. Your National ID has been verified successfully. Choose a password to complete your account setup.',
|
||||||
|
setPasswordAndContinue: 'Set password & continue',
|
||||||
|
passwordSet: 'Password set successfully',
|
||||||
|
sessionExpired: 'Session expired. Please sign up again.',
|
||||||
|
faydaBrandTitle: 'Verifying your National ID...',
|
||||||
|
faydaBrandSubtitle:
|
||||||
|
'Please wait while we securely verify your identity with the National ID service.',
|
||||||
|
faydaVerifying: 'Verifying your identity',
|
||||||
|
faydaFailedTitle: 'Verification failed',
|
||||||
|
faydaProcessing:
|
||||||
|
'We are securely processing your National ID information. This should only take a moment.',
|
||||||
|
faydaTryAgain:
|
||||||
|
'Please try again or use the standard sign-up method instead.',
|
||||||
|
faydaInvalidResponse:
|
||||||
|
'Invalid response from National ID service. Missing code or state.',
|
||||||
|
faydaSessionExpired: 'Session expired. Please try signing up again.',
|
||||||
|
faydaVerifyFailed: 'Failed to complete National ID verification',
|
||||||
|
faydaVerified: 'National ID verified successfully',
|
||||||
|
hero: {
|
||||||
|
brandTitle: 'Maritime licensing, made simple.',
|
||||||
|
brandSubtitle:
|
||||||
|
'Apply for vessel and seafarer licenses, upload documents, and track every application in one secure portal.',
|
||||||
|
featureOnline: 'Submit applications online, 24/7',
|
||||||
|
featureTracking: 'Real-time status tracking & alerts',
|
||||||
|
featureBilingual: 'Available in English & አማርኛ',
|
||||||
|
},
|
||||||
|
validation: {
|
||||||
|
emailInvalid: 'Enter a valid email',
|
||||||
|
passwordMin: 'Password must be at least {{min}} characters',
|
||||||
|
usernameMin: 'Username must be at least 3 characters',
|
||||||
|
phoneRequired: 'Phone number is required',
|
||||||
|
nameEnRequired: 'Name (English) is required',
|
||||||
|
confirmRequired: 'Confirm your password',
|
||||||
|
passwordMismatch: 'Passwords do not match',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
dashboard: {
|
dashboard: {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import { I18nextProvider } from 'react-i18next';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { store } from '../store';
|
import { store } from '../store';
|
||||||
|
import { i18n } from '../i18n/config';
|
||||||
import { MantineThemeProvider } from './MantineThemeProvider';
|
import { MantineThemeProvider } from './MantineThemeProvider';
|
||||||
import { ErrorBoundary } from '../components/ErrorBoundary';
|
import { ErrorBoundary } from '../components/ErrorBoundary';
|
||||||
|
|
||||||
@@ -14,7 +16,9 @@ export function AppProviders({ children }: { children: ReactNode }) {
|
|||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<MantineThemeProvider>{children}</MantineThemeProvider>
|
<I18nextProvider i18n={i18n}>
|
||||||
|
<MantineThemeProvider>{children}</MantineThemeProvider>
|
||||||
|
</I18nextProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { createBrowserRouter, Navigate } from 'react-router-dom';
|
import { createBrowserRouter, Navigate } from 'react-router-dom';
|
||||||
import { I18nextProvider } from 'react-i18next';
|
|
||||||
import { i18n } from './i18n/config';
|
|
||||||
import { PortalLayout } from './layouts/PortalLayout';
|
import { PortalLayout } from './layouts/PortalLayout';
|
||||||
import { ProtectedRoute } from './components/ProtectedRoute';
|
import { ProtectedRoute } from './components/ProtectedRoute';
|
||||||
import { ProfileGuard } from './components/ProfileGuard';
|
import { ProfileGuard } from './components/ProfileGuard';
|
||||||
@@ -45,11 +43,11 @@ import { VesselOwnerDashboardPage } from './features/vessel-owner/pages/VesselOw
|
|||||||
|
|
||||||
export const router = createBrowserRouter([
|
export const router = createBrowserRouter([
|
||||||
// Public auth pages
|
// Public auth pages
|
||||||
{ path: '/login', element: <LoginPage appName="Portal" /> },
|
{ path: '/login', element: <LoginPage /> },
|
||||||
{ path: '/signup', element: <SignupPage appName="Portal" /> },
|
{ path: '/signup', element: <SignupPage /> },
|
||||||
|
|
||||||
// Public auth pages
|
// Public auth pages
|
||||||
{ path: '/forgot-password', element: <ForgotPasswordPage appName="Portal" /> },
|
{ path: '/forgot-password', element: <ForgotPasswordPage /> },
|
||||||
|
|
||||||
// Fayda / National ID callback
|
// Fayda / National ID callback
|
||||||
{ path: '/callback', element: <FaydaCallbackPage /> },
|
{ path: '/callback', element: <FaydaCallbackPage /> },
|
||||||
@@ -72,9 +70,7 @@ export const router = createBrowserRouter([
|
|||||||
element: (
|
element: (
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<ProfileGuard>
|
<ProfileGuard>
|
||||||
<I18nextProvider i18n={i18n}>
|
<PortalLayout />
|
||||||
<PortalLayout />
|
|
||||||
</I18nextProvider>
|
|
||||||
</ProfileGuard>
|
</ProfileGuard>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import {
|
|||||||
type BoxProps,
|
type BoxProps,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconCheck, IconMoon, IconSun } from '@tabler/icons-react';
|
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';
|
const LOGO_URL = '/brand/ema-white.png';
|
||||||
|
|
||||||
@@ -60,23 +62,26 @@ function ThemeToggle() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const FEATURES = [
|
const FEATURE_KEYS = [
|
||||||
'Submit applications online, 24/7',
|
'auth.hero.featureOnline',
|
||||||
'Real-time status tracking & alerts',
|
'auth.hero.featureTracking',
|
||||||
'Available in English & አማርኛ',
|
'auth.hero.featureBilingual',
|
||||||
];
|
];
|
||||||
|
|
||||||
interface AuthShellProps {
|
interface AuthShellProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
brandTitle?: string;
|
brandTitle?: string;
|
||||||
brandSubtitle?: string;
|
brandSubtitle?: string;
|
||||||
|
supportedLanguages?: readonly string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AuthShell({
|
export function AuthShell({
|
||||||
children,
|
children,
|
||||||
brandTitle = 'Maritime licensing, made simple.',
|
brandTitle,
|
||||||
brandSubtitle = 'Apply for vessel and seafarer licenses, upload documents, and track every application in one secure portal.',
|
brandSubtitle,
|
||||||
|
supportedLanguages = ['en', 'am'],
|
||||||
}: AuthShellProps) {
|
}: AuthShellProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
const heroGradient = theme.other.heroGradient as string;
|
const heroGradient = theme.other.heroGradient as string;
|
||||||
const computed = useComputedColorScheme('light');
|
const computed = useComputedColorScheme('light');
|
||||||
@@ -91,6 +96,18 @@ export function AuthShell({
|
|||||||
}}
|
}}
|
||||||
p="md"
|
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 */}
|
{/* Theme toggle — top-right corner */}
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
@@ -174,16 +191,16 @@ export function AuthShell({
|
|||||||
|
|
||||||
<Stack gap={6}>
|
<Stack gap={6}>
|
||||||
<Title order={2} c="white" fz={rem(28)} lh={1.2} fw={700}>
|
<Title order={2} c="white" fz={rem(28)} lh={1.2} fw={700}>
|
||||||
{brandTitle}
|
{brandTitle ?? t('auth.hero.brandTitle')}
|
||||||
</Title>
|
</Title>
|
||||||
<Text fz="sm" lh={1.6} style={{ color: 'rgba(255,255,255,0.85)' }}>
|
<Text fz="sm" lh={1.6} style={{ color: 'rgba(255,255,255,0.85)' }}>
|
||||||
{brandSubtitle}
|
{t('auth.hero.brandSubtitle')}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
{FEATURES.map((feature) => (
|
{FEATURE_KEYS.map((key) => (
|
||||||
<Group key={feature} gap="sm" wrap="nowrap">
|
<Group key={key} gap="sm" wrap="nowrap">
|
||||||
<Center
|
<Center
|
||||||
w={22}
|
w={22}
|
||||||
h={22}
|
h={22}
|
||||||
@@ -196,7 +213,7 @@ export function AuthShell({
|
|||||||
<IconCheck size={12} color="white" stroke={2.4} />
|
<IconCheck size={12} color="white" stroke={2.4} />
|
||||||
</Center>
|
</Center>
|
||||||
<Text c="white" fz="sm" fw={500}>
|
<Text c="white" fz="sm" fw={500}>
|
||||||
{feature}
|
{t(key)}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Alert, Center, Loader, Stack, Text, Title } from '@mantine/core';
|
import { Alert, Center, Loader, Stack, Text, Title } from '@mantine/core';
|
||||||
import { IconAlertCircle, IconShieldCheck } from '@tabler/icons-react';
|
import { IconAlertCircle, IconShieldCheck } from '@tabler/icons-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { useApiMutation } from '@ema-platform/api';
|
import { useApiMutation } from '@ema-platform/api';
|
||||||
import { notify } from '@ema-platform/ui';
|
import { notify } from '@ema-platform/ui';
|
||||||
@@ -18,6 +19,7 @@ interface FaydaUserInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function FaydaCallbackPage() {
|
export function FaydaCallbackPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const calledRef = useRef(false);
|
const calledRef = useRef(false);
|
||||||
@@ -33,7 +35,7 @@ export function FaydaCallbackPage() {
|
|||||||
const state = searchParams.get('state');
|
const state = searchParams.get('state');
|
||||||
|
|
||||||
if (!code || !state) {
|
if (!code || !state) {
|
||||||
const msg = 'Invalid response from National ID service. Missing code or state.';
|
const msg = t('auth.faydaInvalidResponse');
|
||||||
setError(msg);
|
setError(msg);
|
||||||
notify.error(msg);
|
notify.error(msg);
|
||||||
return;
|
return;
|
||||||
@@ -43,7 +45,7 @@ export function FaydaCallbackPage() {
|
|||||||
sessionStorage.removeItem('fayda_authId');
|
sessionStorage.removeItem('fayda_authId');
|
||||||
|
|
||||||
if (!authId) {
|
if (!authId) {
|
||||||
const msg = 'Session expired. Please try signing up again.';
|
const msg = t('auth.faydaSessionExpired');
|
||||||
setError(msg);
|
setError(msg);
|
||||||
notify.error(msg);
|
notify.error(msg);
|
||||||
return;
|
return;
|
||||||
@@ -60,12 +62,12 @@ export function FaydaCallbackPage() {
|
|||||||
sessionStorage.setItem('fayda_userId', userInfo.sub);
|
sessionStorage.setItem('fayda_userId', userInfo.sub);
|
||||||
sessionStorage.setItem('fayda_userInfo', JSON.stringify(userInfo));
|
sessionStorage.setItem('fayda_userInfo', JSON.stringify(userInfo));
|
||||||
|
|
||||||
notify.success('National ID verified successfully');
|
notify.success(t('auth.faydaVerified'));
|
||||||
navigate('/set-password', { replace: true });
|
navigate('/set-password', { replace: true });
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const msg =
|
const msg =
|
||||||
(err as { data?: { message?: string } })?.data?.message ??
|
(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);
|
setError(msg);
|
||||||
notify.error(msg);
|
notify.error(msg);
|
||||||
}
|
}
|
||||||
@@ -76,19 +78,17 @@ export function FaydaCallbackPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthShell
|
<AuthShell
|
||||||
brandTitle="Verifying your National ID..."
|
brandTitle={t('auth.faydaBrandTitle')}
|
||||||
brandSubtitle="Please wait while we securely verify your identity with the National ID service."
|
brandSubtitle={t('auth.faydaBrandSubtitle')}
|
||||||
>
|
>
|
||||||
<Stack gap="lg" align="center">
|
<Stack gap="lg" align="center">
|
||||||
<IconShieldCheck size={56} stroke={1.5} />
|
<IconShieldCheck size={56} stroke={1.5} />
|
||||||
<div>
|
<div>
|
||||||
<Title order={2} fz={30} ta="center">
|
<Title order={2} fz={30} ta="center">
|
||||||
{error ? 'Verification failed' : 'Verifying your identity'}
|
{error ? t('auth.faydaFailedTitle') : t('auth.faydaVerifying')}
|
||||||
</Title>
|
</Title>
|
||||||
<Text c="dimmed" mt={6} ta="center">
|
<Text c="dimmed" mt={6} ta="center">
|
||||||
{error
|
{error ? error : t('auth.faydaProcessing')}
|
||||||
? error
|
|
||||||
: 'We are securely processing your National ID information. This should only take a moment.'}
|
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ export function FaydaCallbackPage() {
|
|||||||
{error}
|
{error}
|
||||||
</Alert>
|
</Alert>
|
||||||
<Text size="sm" c="dimmed" ta="center">
|
<Text size="sm" c="dimmed" ta="center">
|
||||||
Please try again or use the standard sign-up method instead.
|
{t('auth.faydaTryAgain')}
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
IconMailForward,
|
IconMailForward,
|
||||||
IconSend,
|
IconSend,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -31,11 +32,8 @@ const schema = z.object({
|
|||||||
|
|
||||||
type FormValues = z.infer<typeof schema>;
|
type FormValues = z.infer<typeof schema>;
|
||||||
|
|
||||||
interface ForgotPasswordPageProps {
|
export function ForgotPasswordPage() {
|
||||||
appName?: string;
|
const { t } = useTranslation();
|
||||||
}
|
|
||||||
|
|
||||||
export function ForgotPasswordPage({ appName = 'Portal' }: ForgotPasswordPageProps) {
|
|
||||||
const [forgotTrigger, { isLoading }] = useApiMutation();
|
const [forgotTrigger, { isLoading }] = useApiMutation();
|
||||||
const [sentTo, setSentTo] = useState<string | null>(null);
|
const [sentTo, setSentTo] = useState<string | null>(null);
|
||||||
const [serverError, setServerError] = useState<string | null>(null);
|
const [serverError, setServerError] = useState<string | null>(null);
|
||||||
@@ -63,7 +61,7 @@ export function ForgotPasswordPage({ appName = 'Portal' }: ForgotPasswordPagePro
|
|||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const msg =
|
const msg =
|
||||||
(err as { data?: { message?: string } })?.data?.message ??
|
(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);
|
setServerError(msg);
|
||||||
notify.error(msg);
|
notify.error(msg);
|
||||||
}
|
}
|
||||||
@@ -73,11 +71,11 @@ export function ForgotPasswordPage({ appName = 'Portal' }: ForgotPasswordPagePro
|
|||||||
if (!sentTo || isLoading) return;
|
if (!sentTo || isLoading) return;
|
||||||
try {
|
try {
|
||||||
await sendResetLink(sentTo);
|
await sendResetLink(sentTo);
|
||||||
notify.success('Reset link sent again');
|
notify.success(t('auth.resetLinkSentAgain'));
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const msg =
|
const msg =
|
||||||
(err as { data?: { message?: string } })?.data?.message ??
|
(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);
|
setServerError(msg);
|
||||||
notify.error(msg);
|
notify.error(msg);
|
||||||
}
|
}
|
||||||
@@ -93,7 +91,7 @@ export function ForgotPasswordPage({ appName = 'Portal' }: ForgotPasswordPagePro
|
|||||||
style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}
|
style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}
|
||||||
>
|
>
|
||||||
<IconArrowLeft size={16} />
|
<IconArrowLeft size={16} />
|
||||||
Back to sign in
|
{t('auth.backToSignIn')}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</Center>
|
</Center>
|
||||||
);
|
);
|
||||||
@@ -101,8 +99,8 @@ export function ForgotPasswordPage({ appName = 'Portal' }: ForgotPasswordPagePro
|
|||||||
if (sentTo) {
|
if (sentTo) {
|
||||||
return (
|
return (
|
||||||
<AuthShell
|
<AuthShell
|
||||||
brandTitle="Reset your password securely."
|
brandTitle={t('auth.resetPasswordBrandTitle')}
|
||||||
brandSubtitle={`We'll email you a secure link to set a new password and get you back into ${appName}.`}
|
brandSubtitle={t('auth.resetPasswordBrandSubtitle')}
|
||||||
>
|
>
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<ThemeIcon size={56} radius="md" variant="light" color="emaPrimary">
|
<ThemeIcon size={56} radius="md" variant="light" color="emaPrimary">
|
||||||
@@ -111,14 +109,10 @@ export function ForgotPasswordPage({ appName = 'Portal' }: ForgotPasswordPagePro
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Title order={2} fz={30}>
|
<Title order={2} fz={30}>
|
||||||
Check your email
|
{t('auth.checkYourEmail')}
|
||||||
</Title>
|
</Title>
|
||||||
<Text c="dimmed" mt={6}>
|
<Text c="dimmed" mt={6}>
|
||||||
We've sent a password reset link to{' '}
|
{t('auth.resetLinkSent', { email: sentTo })}
|
||||||
<Text span fw={600} c="dark">
|
|
||||||
{sentTo}
|
|
||||||
</Text>
|
|
||||||
. Follow the link in that email to choose a new password.
|
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -131,12 +125,12 @@ export function ForgotPasswordPage({ appName = 'Portal' }: ForgotPasswordPagePro
|
|||||||
size="md"
|
size="md"
|
||||||
leftSection={<IconMail size={18} />}
|
leftSection={<IconMail size={18} />}
|
||||||
>
|
>
|
||||||
Open email app
|
{t('auth.openEmailApp')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Group justify="center" gap={6}>
|
<Group justify="center" gap={6}>
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
Didn't get the email?
|
{t('auth.didNotGetEmail')}
|
||||||
</Text>
|
</Text>
|
||||||
<Anchor
|
<Anchor
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -144,7 +138,7 @@ export function ForgotPasswordPage({ appName = 'Portal' }: ForgotPasswordPagePro
|
|||||||
onClick={handleResend}
|
onClick={handleResend}
|
||||||
style={isLoading ? { pointerEvents: 'none', opacity: 0.6 } : undefined}
|
style={isLoading ? { pointerEvents: 'none', opacity: 0.6 } : undefined}
|
||||||
>
|
>
|
||||||
Resend link
|
{t('auth.resendLink')}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
@@ -156,17 +150,16 @@ export function ForgotPasswordPage({ appName = 'Portal' }: ForgotPasswordPagePro
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthShell
|
<AuthShell
|
||||||
brandTitle="Reset your password securely."
|
brandTitle={t('auth.resetPasswordBrandTitle')}
|
||||||
brandSubtitle={`We'll email you a secure link to set a new password and get you back into ${appName}.`}
|
brandSubtitle={t('auth.resetPasswordBrandSubtitle')}
|
||||||
>
|
>
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<div>
|
<div>
|
||||||
<Title order={2} fz={30}>
|
<Title order={2} fz={30}>
|
||||||
Forgot your password?
|
{t('auth.forgotYourPassword')}
|
||||||
</Title>
|
</Title>
|
||||||
<Text c="dimmed" mt={6}>
|
<Text c="dimmed" mt={6}>
|
||||||
Enter the email linked to your account and we'll send you a link
|
{t('auth.forgotDescription')}
|
||||||
to reset your password.
|
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -179,11 +172,11 @@ export function ForgotPasswordPage({ appName = 'Portal' }: ForgotPasswordPagePro
|
|||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Email address"
|
label={t('auth.emailAddress')}
|
||||||
placeholder="you@example.com"
|
placeholder={t('auth.emailPlaceholder')}
|
||||||
size="md"
|
size="md"
|
||||||
leftSection={<IconMail size={18} />}
|
leftSection={<IconMail size={18} />}
|
||||||
error={errors.email?.message}
|
error={errors.email ? t('auth.validation.emailInvalid') : undefined}
|
||||||
{...register('email')}
|
{...register('email')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -194,7 +187,7 @@ export function ForgotPasswordPage({ appName = 'Portal' }: ForgotPasswordPagePro
|
|||||||
size="md"
|
size="md"
|
||||||
rightSection={<IconSend size={18} />}
|
rightSection={<IconSend size={18} />}
|
||||||
>
|
>
|
||||||
Send reset link
|
{t('auth.sendResetLink')}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { useApiMutation } from '@ema-platform/api';
|
import { useApiMutation } from '@ema-platform/api';
|
||||||
@@ -44,10 +45,11 @@ interface LoginPageProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function LoginPage({
|
export function LoginPage({
|
||||||
appName = 'Portal',
|
appName,
|
||||||
enableSignup = true,
|
enableSignup = true,
|
||||||
enableForgotPassword = true,
|
enableForgotPassword = true,
|
||||||
}: LoginPageProps) {
|
}: LoginPageProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@@ -118,7 +120,7 @@ export function LoginPage({
|
|||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const msg =
|
const msg =
|
||||||
(err as { data?: { message?: string } })?.data?.message ??
|
(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);
|
setServerError(msg);
|
||||||
notify.error(msg);
|
notify.error(msg);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -131,10 +133,10 @@ export function LoginPage({
|
|||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<div>
|
<div>
|
||||||
<Title order={2} fz={30}>
|
<Title order={2} fz={30}>
|
||||||
Welcome to {appName}
|
{t('auth.welcomeTo', { appName: appName ?? t('app.name') })}
|
||||||
</Title>
|
</Title>
|
||||||
<Text c="dimmed" mt={6}>
|
<Text c="dimmed" mt={6}>
|
||||||
Sign in to access your account.
|
{t('auth.signInToAccess')}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -147,25 +149,25 @@ export function LoginPage({
|
|||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Email or phone"
|
label={t('auth.emailOrPhone')}
|
||||||
placeholder="you@example.com"
|
placeholder={t('auth.emailPlaceholder')}
|
||||||
size="md"
|
size="md"
|
||||||
leftSection={<IconMail size={18} />}
|
leftSection={<IconMail size={18} />}
|
||||||
error={errors.email?.message}
|
error={errors.email ? t('auth.validation.emailInvalid') : undefined}
|
||||||
{...register('email')}
|
{...register('email')}
|
||||||
/>
|
/>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
label="Password"
|
label={t('auth.password')}
|
||||||
placeholder="Your password"
|
placeholder={t('auth.passwordPlaceholder')}
|
||||||
size="md"
|
size="md"
|
||||||
leftSection={<IconLock size={18} />}
|
leftSection={<IconLock size={18} />}
|
||||||
error={errors.password?.message}
|
error={errors.password ? t('auth.validation.passwordMin', { min: 6 }) : undefined}
|
||||||
{...register('password')}
|
{...register('password')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label="Remember me"
|
label={t('auth.rememberMe')}
|
||||||
size="sm"
|
size="sm"
|
||||||
checked={rememberMe}
|
checked={rememberMe}
|
||||||
onChange={(e) => setRememberMe(e.currentTarget.checked)}
|
onChange={(e) => setRememberMe(e.currentTarget.checked)}
|
||||||
@@ -177,7 +179,7 @@ export function LoginPage({
|
|||||||
size="sm"
|
size="sm"
|
||||||
fw={600}
|
fw={600}
|
||||||
>
|
>
|
||||||
Forgot password?
|
{t('auth.forgotPassword')}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
@@ -189,12 +191,12 @@ export function LoginPage({
|
|||||||
size="md"
|
size="md"
|
||||||
rightSection={<IconArrowRight size={18} />}
|
rightSection={<IconArrowRight size={18} />}
|
||||||
>
|
>
|
||||||
Sign in
|
{t('auth.signIn')}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<Divider label="or" labelPosition="center" />
|
<Divider label={t('common.or')} labelPosition="center" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
@@ -203,14 +205,14 @@ export function LoginPage({
|
|||||||
leftSection={<IconDeviceMobile size={18} />}
|
leftSection={<IconDeviceMobile size={18} />}
|
||||||
onClick={() => notify.info('Phone sign-in is coming soon.')}
|
onClick={() => notify.info('Phone sign-in is coming soon.')}
|
||||||
>
|
>
|
||||||
Sign in with phone number
|
{t('auth.signInWithPhone')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{enableSignup && (
|
{enableSignup && (
|
||||||
<Text ta="center" size="sm" c="dimmed">
|
<Text ta="center" size="sm" c="dimmed">
|
||||||
Don't have an account?{' '}
|
{t('auth.noAccount')}{' '}
|
||||||
<Anchor component={Link} to="/signup" fw={700}>
|
<Anchor component={Link} to="/signup" fw={700}>
|
||||||
Create one
|
{t('auth.createOne')}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { IconArrowRight, IconShieldLock } from '@tabler/icons-react';
|
|||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { useApiMutation } from '@ema-platform/api';
|
import { useApiMutation } from '@ema-platform/api';
|
||||||
import { notify } from '@ema-platform/ui';
|
import { notify } from '@ema-platform/ui';
|
||||||
@@ -25,14 +26,13 @@ const CODE_LENGTH = 6;
|
|||||||
const RESEND_SECONDS = 30;
|
const RESEND_SECONDS = 30;
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
verificationCode: z
|
verificationCode: z.string().length(CODE_LENGTH),
|
||||||
.string()
|
|
||||||
.length(CODE_LENGTH, { message: `Enter the ${CODE_LENGTH}-digit code` }),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormValues = z.infer<typeof schema>;
|
type FormValues = z.infer<typeof schema>;
|
||||||
|
|
||||||
export function OTPVerificationPage() {
|
export function OTPVerificationPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const state = location.state as
|
const state = location.state as
|
||||||
@@ -70,12 +70,12 @@ export function OTPVerificationPage() {
|
|||||||
body: { email, phoneNumber, verificationCode: values.verificationCode },
|
body: { email, phoneNumber, verificationCode: values.verificationCode },
|
||||||
}).unwrap();
|
}).unwrap();
|
||||||
|
|
||||||
notify.success('Phone number verified successfully');
|
notify.success(t('auth.phoneVerified'));
|
||||||
navigate(needsProfile ? '/profile-setup' : '/dashboard');
|
navigate(needsProfile ? '/profile-setup' : '/dashboard');
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const msg =
|
const msg =
|
||||||
(err as { data?: { message?: string } })?.data?.message ??
|
(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);
|
setServerError(msg);
|
||||||
notify.error(msg);
|
notify.error(msg);
|
||||||
}
|
}
|
||||||
@@ -90,13 +90,13 @@ export function OTPVerificationPage() {
|
|||||||
body: { email, phoneNumber, type: 'verify-phone-number' },
|
body: { email, phoneNumber, type: 'verify-phone-number' },
|
||||||
}).unwrap();
|
}).unwrap();
|
||||||
|
|
||||||
notify.success('Verification code resent to your email');
|
notify.success(t('auth.codeResent'));
|
||||||
setSecondsLeft(RESEND_SECONDS);
|
setSecondsLeft(RESEND_SECONDS);
|
||||||
setServerError(null);
|
setServerError(null);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const msg =
|
const msg =
|
||||||
(err as { data?: { message?: string } })?.data?.message ??
|
(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);
|
setServerError(msg);
|
||||||
notify.error(msg);
|
notify.error(msg);
|
||||||
}
|
}
|
||||||
@@ -104,8 +104,8 @@ export function OTPVerificationPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthShell
|
<AuthShell
|
||||||
brandTitle="One last step to secure your account."
|
brandTitle={t('auth.otpBrandTitle')}
|
||||||
brandSubtitle="We use a one-time code to confirm it's really you before granting access."
|
brandSubtitle={t('auth.otpBrandSubtitle')}
|
||||||
>
|
>
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<ThemeIcon size={56} radius="md" variant="light" color="emaPrimary">
|
<ThemeIcon size={56} radius="md" variant="light" color="emaPrimary">
|
||||||
@@ -114,14 +114,10 @@ export function OTPVerificationPage() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Title order={2} fz={30}>
|
<Title order={2} fz={30}>
|
||||||
Verify your account
|
{t('auth.verifyAccount')}
|
||||||
</Title>
|
</Title>
|
||||||
<Text c="dimmed" mt={6}>
|
<Text c="dimmed" mt={6}>
|
||||||
A verification code has been sent to{' '}
|
{t('auth.otpSent', { email: email || t('auth.emailPlaceholder') })}
|
||||||
<Text span fw={600} c="dark">
|
|
||||||
{email || 'your email'}
|
|
||||||
</Text>
|
|
||||||
. Enter it below to continue.
|
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -150,9 +146,9 @@ export function OTPVerificationPage() {
|
|||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
onComplete={() => handleSubmit(onSubmit)()}
|
onComplete={() => handleSubmit(onSubmit)()}
|
||||||
/>
|
/>
|
||||||
{errors.verificationCode?.message && (
|
{errors.verificationCode && (
|
||||||
<Text c="red" size="sm">
|
<Text c="red" size="sm">
|
||||||
{errors.verificationCode.message}
|
{t('auth.otpDigitCode', { length: CODE_LENGTH })}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -165,13 +161,13 @@ export function OTPVerificationPage() {
|
|||||||
size="md"
|
size="md"
|
||||||
rightSection={<IconArrowRight size={18} />}
|
rightSection={<IconArrowRight size={18} />}
|
||||||
>
|
>
|
||||||
Verify
|
{t('auth.verify')}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<Divider
|
<Divider
|
||||||
label="Having trouble?"
|
label={t('auth.havingTrouble')}
|
||||||
labelPosition="center"
|
labelPosition="center"
|
||||||
variant="dashed"
|
variant="dashed"
|
||||||
/>
|
/>
|
||||||
@@ -182,17 +178,17 @@ export function OTPVerificationPage() {
|
|||||||
size="md"
|
size="md"
|
||||||
onClick={() => navigate('/dashboard')}
|
onClick={() => navigate('/dashboard')}
|
||||||
>
|
>
|
||||||
Skip verification for now
|
{t('auth.skipVerification')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Center>
|
<Center>
|
||||||
<Group justify="center" gap={6}>
|
<Group justify="center" gap={6}>
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
Didn't receive a code?
|
{t('auth.noCode')}
|
||||||
</Text>
|
</Text>
|
||||||
{secondsLeft > 0 ? (
|
{secondsLeft > 0 ? (
|
||||||
<Text size="sm" c="dimmed" fw={600}>
|
<Text size="sm" c="dimmed" fw={600}>
|
||||||
Resend in {secondsLeft}s
|
{t('auth.resendIn', { seconds: secondsLeft })}
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<Anchor
|
<Anchor
|
||||||
@@ -203,7 +199,7 @@ export function OTPVerificationPage() {
|
|||||||
resending ? { pointerEvents: 'none', opacity: 0.6 } : undefined
|
resending ? { pointerEvents: 'none', opacity: 0.6 } : undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Resend code
|
{t('auth.resendCode')}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { IconArrowRight, IconLock, IconLockOpen } from '@tabler/icons-react';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useApiMutation } from '@ema-platform/api';
|
import { useApiMutation } from '@ema-platform/api';
|
||||||
import { notify } from '@ema-platform/ui';
|
import { notify } from '@ema-platform/ui';
|
||||||
@@ -11,8 +12,8 @@ import { AuthShell } from '../components/AuthShell';
|
|||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
newPassword: z.string().min(8, { message: 'Password must be at least 8 characters' }),
|
newPassword: z.string().min(8),
|
||||||
confirmPassword: z.string().min(8, { message: 'Confirm your password' }),
|
confirmPassword: z.string().min(8),
|
||||||
})
|
})
|
||||||
.refine((data) => data.newPassword === data.confirmPassword, {
|
.refine((data) => data.newPassword === data.confirmPassword, {
|
||||||
message: 'Passwords do not match',
|
message: 'Passwords do not match',
|
||||||
@@ -22,6 +23,7 @@ const schema = z
|
|||||||
type FormValues = z.infer<typeof schema>;
|
type FormValues = z.infer<typeof schema>;
|
||||||
|
|
||||||
export function SetPasswordPage() {
|
export function SetPasswordPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [serverError, setServerError] = useState<string | null>(null);
|
const [serverError, setServerError] = useState<string | null>(null);
|
||||||
const [setPasswordTrigger, { isLoading }] = useApiMutation();
|
const [setPasswordTrigger, { isLoading }] = useApiMutation();
|
||||||
@@ -49,7 +51,7 @@ export function SetPasswordPage() {
|
|||||||
|
|
||||||
const onSubmit = async (values: FormValues) => {
|
const onSubmit = async (values: FormValues) => {
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
notify.error('Session expired. Please sign up again.');
|
notify.error(t('auth.sessionExpired'));
|
||||||
navigate('/signup', { replace: true });
|
navigate('/signup', { replace: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -68,12 +70,12 @@ export function SetPasswordPage() {
|
|||||||
sessionStorage.removeItem('fayda_userId');
|
sessionStorage.removeItem('fayda_userId');
|
||||||
sessionStorage.removeItem('fayda_userInfo');
|
sessionStorage.removeItem('fayda_userInfo');
|
||||||
|
|
||||||
notify.success('Password set successfully');
|
notify.success(t('auth.passwordSet'));
|
||||||
navigate('/login', { replace: true });
|
navigate('/login', { replace: true });
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const msg =
|
const msg =
|
||||||
(err as { data?: { message?: string } })?.data?.message ??
|
(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);
|
setServerError(msg);
|
||||||
notify.error(msg);
|
notify.error(msg);
|
||||||
}
|
}
|
||||||
@@ -81,8 +83,8 @@ export function SetPasswordPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthShell
|
<AuthShell
|
||||||
brandTitle="Set your account password."
|
brandTitle={t('auth.setPasswordBrandTitle')}
|
||||||
brandSubtitle="Choose a strong password to secure your account and access all Portal features."
|
brandSubtitle={t('auth.setPasswordBrandSubtitle')}
|
||||||
>
|
>
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<ThemeIcon size={56} radius="md" variant="light" color="emaPrimary">
|
<ThemeIcon size={56} radius="md" variant="light" color="emaPrimary">
|
||||||
@@ -91,11 +93,10 @@ export function SetPasswordPage() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Title order={2} fz={30}>
|
<Title order={2} fz={30}>
|
||||||
Set your password
|
{t('auth.setPassword')}
|
||||||
</Title>
|
</Title>
|
||||||
<Text c="dimmed" mt={6}>
|
<Text c="dimmed" mt={6}>
|
||||||
Welcome, <Text span fw={600}>{userName}</Text>. Your National ID has been verified successfully.
|
{t('auth.welcomeNationalId', { name: userName })}
|
||||||
Choose a password to complete your account setup.
|
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -108,17 +109,23 @@ export function SetPasswordPage() {
|
|||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
label="New password"
|
label={t('auth.newPassword')}
|
||||||
placeholder="At least 8 characters"
|
placeholder={t('auth.passwordPlaceholder')}
|
||||||
leftSection={<IconLock size={18} />}
|
leftSection={<IconLock size={18} />}
|
||||||
error={errors.newPassword?.message}
|
error={errors.newPassword ? t('auth.validation.passwordMin', { min: 8 }) : undefined}
|
||||||
{...register('newPassword')}
|
{...register('newPassword')}
|
||||||
/>
|
/>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
label="Confirm password"
|
label={t('auth.confirmPassword')}
|
||||||
placeholder="Re-enter password"
|
placeholder={t('auth.confirmPasswordPlaceholder')}
|
||||||
leftSection={<IconLock size={18} />}
|
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')}
|
{...register('confirmPassword')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -129,7 +136,7 @@ export function SetPasswordPage() {
|
|||||||
size="md"
|
size="md"
|
||||||
rightSection={<IconArrowRight size={18} />}
|
rightSection={<IconArrowRight size={18} />}
|
||||||
>
|
>
|
||||||
Set password & continue
|
{t('auth.setPasswordAndContinue')}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { useApiMutation } from '@ema-platform/api';
|
import { useApiMutation } from '@ema-platform/api';
|
||||||
@@ -64,11 +65,8 @@ interface SignupPayload {
|
|||||||
confirmPassword: string;
|
confirmPassword: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SignupPageProps {
|
export function SignupPage() {
|
||||||
appName?: string;
|
const { t } = useTranslation();
|
||||||
}
|
|
||||||
|
|
||||||
export function SignupPage({ appName = 'Portal' }: SignupPageProps) {
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [agreed, setAgreed] = useState(false);
|
const [agreed, setAgreed] = useState(false);
|
||||||
@@ -101,7 +99,7 @@ export function SignupPage({ appName = 'Portal' }: SignupPageProps) {
|
|||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const msg =
|
const msg =
|
||||||
(err as { data?: { message?: string } })?.data?.message ??
|
(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);
|
setServerError(msg);
|
||||||
notify.error(msg);
|
notify.error(msg);
|
||||||
}
|
}
|
||||||
@@ -150,7 +148,7 @@ export function SignupPage({ appName = 'Portal' }: SignupPageProps) {
|
|||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const msg =
|
const msg =
|
||||||
(err as { data?: { message?: string } })?.data?.message ??
|
(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);
|
setServerError(msg);
|
||||||
notify.error(msg);
|
notify.error(msg);
|
||||||
}
|
}
|
||||||
@@ -158,16 +156,16 @@ export function SignupPage({ appName = 'Portal' }: SignupPageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthShell
|
<AuthShell
|
||||||
brandTitle={`Join ${appName}'s community.`}
|
brandTitle={t('auth.signupBrandTitle')}
|
||||||
brandSubtitle={`Create your account to access ${appName} features.`}
|
brandSubtitle={t('auth.signupBrandSubtitle')}
|
||||||
>
|
>
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<div>
|
<div>
|
||||||
<Title order={2} fz={30}>
|
<Title order={2} fz={30}>
|
||||||
Create account
|
{t('auth.createAccount')}
|
||||||
</Title>
|
</Title>
|
||||||
<Text c="dimmed" mt={6}>
|
<Text c="dimmed" mt={6}>
|
||||||
It only takes a minute to get started.
|
{t('auth.signupDescription')}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -181,59 +179,64 @@ export function SignupPage({ appName = 'Portal' }: SignupPageProps) {
|
|||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<SimpleGrid cols={{ base: 1, xs: 2 }} spacing="md">
|
<SimpleGrid cols={{ base: 1, xs: 2 }} spacing="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Name (English)"
|
label={t('auth.nameEn')}
|
||||||
placeholder="Abebe Bekele"
|
placeholder={t('auth.nameEnPlaceholder')}
|
||||||
leftSection={<IconUser size={18} />}
|
leftSection={<IconUser size={18} />}
|
||||||
error={errors.nameEn?.message}
|
error={errors.nameEn ? t('auth.validation.nameEnRequired') : undefined}
|
||||||
{...register('nameEn')}
|
{...register('nameEn')}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Name (Amharic)"
|
label={t('auth.nameAm')}
|
||||||
placeholder="ስም"
|
placeholder={t('auth.nameAmPlaceholder')}
|
||||||
leftSection={<IconUser size={18} />}
|
leftSection={<IconUser size={18} />}
|
||||||
error={errors.nameAm?.message}
|
|
||||||
{...register('nameAm')}
|
{...register('nameAm')}
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
<SimpleGrid cols={{ base: 1, xs: 2 }} spacing="md">
|
<SimpleGrid cols={{ base: 1, xs: 2 }} spacing="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Email address"
|
label={t('auth.emailAddress')}
|
||||||
placeholder="you@example.com"
|
placeholder={t('auth.emailPlaceholder')}
|
||||||
leftSection={<IconMail size={18} />}
|
leftSection={<IconMail size={18} />}
|
||||||
error={errors.email?.message}
|
error={errors.email ? t('auth.validation.emailInvalid') : undefined}
|
||||||
{...register('email')}
|
{...register('email')}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Username"
|
label={t('auth.username')}
|
||||||
placeholder="Choose a username"
|
placeholder={t('auth.usernamePlaceholder')}
|
||||||
leftSection={<IconAt size={18} />}
|
leftSection={<IconAt size={18} />}
|
||||||
error={errors.username?.message}
|
error={errors.username ? t('auth.validation.usernameMin') : undefined}
|
||||||
{...register('username')}
|
{...register('username')}
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Phone number"
|
label={t('auth.phoneNumber')}
|
||||||
placeholder="+251 911 234 567"
|
placeholder={t('auth.phonePlaceholder')}
|
||||||
leftSection={<IconDeviceMobile size={18} />}
|
leftSection={<IconDeviceMobile size={18} />}
|
||||||
error={errors.phoneNumber?.message}
|
error={errors.phoneNumber ? t('auth.validation.phoneRequired') : undefined}
|
||||||
{...register('phoneNumber')}
|
{...register('phoneNumber')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SimpleGrid cols={{ base: 1, xs: 2 }} spacing="md">
|
<SimpleGrid cols={{ base: 1, xs: 2 }} spacing="md">
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
label="Password"
|
label={t('auth.password')}
|
||||||
placeholder="At least 8 characters"
|
placeholder={t('auth.passwordPlaceholder')}
|
||||||
leftSection={<IconLock size={18} />}
|
leftSection={<IconLock size={18} />}
|
||||||
error={errors.password?.message}
|
error={errors.password ? t('auth.validation.passwordMin', { min: 8 }) : undefined}
|
||||||
{...register('password')}
|
{...register('password')}
|
||||||
/>
|
/>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
label="Confirm password"
|
label={t('auth.confirmPassword')}
|
||||||
placeholder="Re-enter password"
|
placeholder={t('auth.confirmPasswordPlaceholder')}
|
||||||
leftSection={<IconLock size={18} />}
|
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')}
|
{...register('confirmPassword')}
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
@@ -244,13 +247,13 @@ export function SignupPage({ appName = 'Portal' }: SignupPageProps) {
|
|||||||
onChange={(e) => setAgreed(e.currentTarget.checked)}
|
onChange={(e) => setAgreed(e.currentTarget.checked)}
|
||||||
label={
|
label={
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
I agree to the{' '}
|
{t('auth.agreeToTerms')}{' '}
|
||||||
<Anchor
|
<Anchor
|
||||||
size="sm"
|
size="sm"
|
||||||
fw={600}
|
fw={600}
|
||||||
onClick={(e) => e.preventDefault()}
|
onClick={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
Terms & Privacy Policy
|
{t('auth.termsAndPrivacy')}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
@@ -264,10 +267,10 @@ export function SignupPage({ appName = 'Portal' }: SignupPageProps) {
|
|||||||
size="md"
|
size="md"
|
||||||
rightSection={<IconArrowRight size={18} />}
|
rightSection={<IconArrowRight size={18} />}
|
||||||
>
|
>
|
||||||
Create account
|
{t('auth.createAccount')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Divider label="or" labelPosition="center" variant="dashed" />
|
<Divider label={t('common.or')} labelPosition="center" variant="dashed" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -277,15 +280,15 @@ export function SignupPage({ appName = 'Portal' }: SignupPageProps) {
|
|||||||
leftSection={<IconId size={18} />}
|
leftSection={<IconId size={18} />}
|
||||||
onClick={handleFaydaSignup}
|
onClick={handleFaydaSignup}
|
||||||
>
|
>
|
||||||
Sign up with National ID (Fayda)
|
{t('auth.signupWithFayda')}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<Text ta="center" size="sm" c="dimmed">
|
<Text ta="center" size="sm" c="dimmed">
|
||||||
Already have an account?{' '}
|
{t('auth.alreadyHaveAccount')}{' '}
|
||||||
<Anchor component={Link} to="/login" fw={700}>
|
<Anchor component={Link} to="/login" fw={700}>
|
||||||
Sign in
|
{t('auth.signIn')}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
Reference in New Issue
Block a user