mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 13:02:50 +00:00
implement profile page update
This commit is contained in:
@@ -8,7 +8,8 @@
|
||||
"Bash(node_modules/.bin/tsc --noEmit -p apps/portal/tsconfig.app.json)",
|
||||
"Bash(node_modules/.bin/tsc --noEmit -p apps/portal/tsconfig.json)",
|
||||
"Bash(node_modules/.bin/tsc --noEmit -p tsconfig.json)",
|
||||
"Bash(../../node_modules/.bin/tsc --noEmit -p tsconfig.json)"
|
||||
"Bash(../../node_modules/.bin/tsc --noEmit -p tsconfig.json)",
|
||||
"Bash(grep -E \"\\\\.\\(ts|tsx\\)$\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<!-- PLACEHOLDER app logo — white/mono mark for dark (gradient) backgrounds,
|
||||
e.g. the auth brand panel. Replace this file (keep the same name/path). -->
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="EMA logo">
|
||||
<rect width="48" height="48" rx="11" fill="#ffffff" />
|
||||
<g transform="translate(12 12)" stroke="#3160b7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none">
|
||||
<circle cx="12" cy="5" r="2" />
|
||||
<line x1="12" y1="22" x2="12" y2="7" />
|
||||
<path d="M5 12H2a10 10 0 0 0 20 0h-3" />
|
||||
</g>
|
||||
<!-- EMA logo — white/mono version for dark backgrounds (auth brand panel).
|
||||
Same faceted "A" + "ema" wordmark, rendered in white with subtle facet
|
||||
shading so the folds still read. -->
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="ema">
|
||||
<polygon points="24,4 8,33 16,33" fill="#ffffff" fill-opacity="0.95" />
|
||||
<polygon points="24,4 16,33 24,33" fill="#ffffff" fill-opacity="0.78" />
|
||||
<polygon points="24,4 24,33 32,33" fill="#ffffff" fill-opacity="0.55" />
|
||||
<polygon points="24,4 32,33 40,33" fill="#ffffff" fill-opacity="0.88" />
|
||||
<text x="24" y="46" text-anchor="middle"
|
||||
font-family="Inter, 'Segoe UI', Arial, sans-serif"
|
||||
font-size="13" font-weight="700" letter-spacing="-0.5" fill="#ffffff">ema</text>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 613 B After Width: | Height: | Size: 818 B |
@@ -1,17 +1,14 @@
|
||||
<!-- PLACEHOLDER app logo — color mark for light backgrounds. Replace this file
|
||||
(keep the same name/path) with the real EMA mark. SVG or swap for PNG via
|
||||
the LOGO map in src/app/components/Logo.tsx. -->
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="EMA logo">
|
||||
<defs>
|
||||
<linearGradient id="emaMark" x1="0" y1="0" x2="48" y2="48" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3160b7" />
|
||||
<stop offset="1" stop-color="#1fc29d" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="48" height="48" rx="11" fill="url(#emaMark)" />
|
||||
<g transform="translate(12 12)" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none">
|
||||
<circle cx="12" cy="5" r="2" />
|
||||
<line x1="12" y1="22" x2="12" y2="7" />
|
||||
<path d="M5 12H2a10 10 0 0 0 20 0h-3" />
|
||||
</g>
|
||||
<!-- EMA logo — colour version (faceted "A" peak + "ema" wordmark).
|
||||
Hand-built vector approximation of the brand mark; tweak the facet points
|
||||
or colours below to fine-tune. Replace this file to swap in official art. -->
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="ema">
|
||||
<!-- faceted A / peak -->
|
||||
<polygon points="24,4 8,33 16,33" fill="#8db0ee" />
|
||||
<polygon points="24,4 16,33 24,33" fill="#4b7fe5" />
|
||||
<polygon points="24,4 24,33 32,33" fill="#1c2c4a" />
|
||||
<polygon points="24,4 32,33 40,33" fill="#3160b7" />
|
||||
<!-- wordmark -->
|
||||
<text x="24" y="46" text-anchor="middle"
|
||||
font-family="Inter, 'Segoe UI', Arial, sans-serif"
|
||||
font-size="13" font-weight="700" letter-spacing="-0.5" fill="#3160b7">ema</text>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 893 B After Width: | Height: | Size: 822 B |
@@ -1,15 +1,7 @@
|
||||
<!-- PLACEHOLDER favicon (browser tab). Replace with the real mark; keep name. -->
|
||||
<!-- EMA favicon — emblem only (no wordmark, for legibility at small sizes). -->
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="favMark" x1="0" y1="0" x2="48" y2="48" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3160b7" />
|
||||
<stop offset="1" stop-color="#1fc29d" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="48" height="48" rx="11" fill="url(#favMark)" />
|
||||
<g transform="translate(12 12)" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none">
|
||||
<circle cx="12" cy="5" r="2" />
|
||||
<line x1="12" y1="22" x2="12" y2="7" />
|
||||
<path d="M5 12H2a10 10 0 0 0 20 0h-3" />
|
||||
</g>
|
||||
<polygon points="24,3 5,45 16,45" fill="#8db0ee" />
|
||||
<polygon points="24,3 16,45 24,45" fill="#4b7fe5" />
|
||||
<polygon points="24,3 24,45 32,45" fill="#1c2c4a" />
|
||||
<polygon points="24,3 32,45 43,45" fill="#3160b7" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 728 B After Width: | Height: | Size: 406 B |
@@ -1,108 +1,316 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Avatar,
|
||||
Badge,
|
||||
Button,
|
||||
Divider,
|
||||
Group,
|
||||
Paper,
|
||||
PasswordInput,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { IconDeviceFloppy } from '@tabler/icons-react';
|
||||
import {
|
||||
IconDeviceFloppy,
|
||||
IconLock,
|
||||
IconMail,
|
||||
IconPhone,
|
||||
IconUser,
|
||||
} 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 { notify } from '@ema-platform/ui';
|
||||
import { useApiMutation } from '@ema-platform/api';
|
||||
import { PageHeader } from '../../../components/PageHeader';
|
||||
import { SUPPORTED_LANGUAGES, type AppLanguage } from '../../../i18n/config';
|
||||
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
|
||||
import { setUser } from '../../auth/store/auth.slice';
|
||||
import type { AuthUser } from '../../auth/types/auth.types';
|
||||
|
||||
function getInitials(name: string, fallback: string) {
|
||||
const source = name?.trim() || fallback?.trim() || '';
|
||||
if (!source) return '?';
|
||||
const parts = source.split(/\s+/);
|
||||
const letters = parts.length > 1 ? parts[0][0] + parts[1][0] : source.slice(0, 2);
|
||||
return letters.toUpperCase();
|
||||
}
|
||||
|
||||
export function ProfilePage() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const user = useAppSelector((state) => state.auth.user);
|
||||
|
||||
const handleSave = () => notify.success(t('common.saved'));
|
||||
const [updateTrigger] = useApiMutation<AuthUser>();
|
||||
const [meTrigger] = useApiMutation<AuthUser>();
|
||||
const [passwordTrigger] = useApiMutation<unknown>();
|
||||
|
||||
const [isSavingProfile, setIsSavingProfile] = useState(false);
|
||||
const [isSavingPassword, setIsSavingPassword] = useState(false);
|
||||
|
||||
// ---- Profile form ----
|
||||
const profileSchema = z.object({
|
||||
nameEn: z.string().min(1, { message: t('profile.validation.nameRequired') }),
|
||||
nameAm: z.string().min(1, { message: t('profile.validation.nameRequired') }),
|
||||
username: z
|
||||
.string()
|
||||
.min(1, { message: t('profile.validation.usernameRequired') }),
|
||||
email: z.string().email({ message: t('profile.validation.emailInvalid') }),
|
||||
phoneNumber: z
|
||||
.string()
|
||||
.min(1, { message: t('profile.validation.phoneRequired') }),
|
||||
});
|
||||
type ProfileValues = z.infer<typeof profileSchema>;
|
||||
|
||||
const {
|
||||
register: registerProfile,
|
||||
handleSubmit: handleProfileSubmit,
|
||||
formState: { errors: profileErrors },
|
||||
} = useForm<ProfileValues>({
|
||||
resolver: zodResolver(profileSchema),
|
||||
values: {
|
||||
nameEn: user?.name?.en ?? '',
|
||||
nameAm: user?.name?.am ?? '',
|
||||
username: user?.username ?? '',
|
||||
email: user?.email ?? '',
|
||||
phoneNumber: user?.phoneNumber ?? '',
|
||||
},
|
||||
});
|
||||
|
||||
const onSaveProfile = async (values: ProfileValues) => {
|
||||
setIsSavingProfile(true);
|
||||
try {
|
||||
await updateTrigger({
|
||||
url: '/auth/update-profile',
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
email: values.email,
|
||||
username: values.username,
|
||||
phoneNumber: values.phoneNumber,
|
||||
name: { am: values.nameAm, en: values.nameEn },
|
||||
},
|
||||
}).unwrap();
|
||||
|
||||
// Refresh the cached user so the rest of the app stays in sync.
|
||||
const me = await meTrigger({ url: '/auth/me', method: 'GET' }).unwrap();
|
||||
dispatch(setUser(me));
|
||||
|
||||
notify.success(t('profile.profileUpdated'));
|
||||
} catch {
|
||||
notify.error(t('profile.updateFailed'));
|
||||
} finally {
|
||||
setIsSavingProfile(false);
|
||||
}
|
||||
};
|
||||
|
||||
// ---- Password form ----
|
||||
const passwordSchema = z
|
||||
.object({
|
||||
oldPassword: z
|
||||
.string()
|
||||
.min(1, { message: t('profile.validation.passwordMin') }),
|
||||
newPassword: z
|
||||
.string()
|
||||
.min(8, { message: t('profile.validation.passwordMin') }),
|
||||
confirmPassword: z
|
||||
.string()
|
||||
.min(8, { message: t('profile.validation.passwordMin') }),
|
||||
})
|
||||
.refine((data) => data.newPassword === data.confirmPassword, {
|
||||
message: t('profile.validation.passwordMismatch'),
|
||||
path: ['confirmPassword'],
|
||||
});
|
||||
type PasswordValues = z.infer<typeof passwordSchema>;
|
||||
|
||||
const {
|
||||
register: registerPassword,
|
||||
handleSubmit: handlePasswordSubmit,
|
||||
reset: resetPassword,
|
||||
formState: { errors: passwordErrors },
|
||||
} = useForm<PasswordValues>({
|
||||
resolver: zodResolver(passwordSchema),
|
||||
defaultValues: { oldPassword: '', newPassword: '', confirmPassword: '' },
|
||||
});
|
||||
|
||||
const onChangePassword = async (values: PasswordValues) => {
|
||||
setIsSavingPassword(true);
|
||||
try {
|
||||
await passwordTrigger({
|
||||
url: '/auth/change-password',
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
oldPassword: values.oldPassword,
|
||||
newPassword: values.newPassword,
|
||||
confirmPassword: values.confirmPassword,
|
||||
},
|
||||
}).unwrap();
|
||||
|
||||
notify.success(t('profile.passwordChanged'));
|
||||
resetPassword();
|
||||
} catch {
|
||||
notify.error(t('profile.passwordFailed'));
|
||||
} finally {
|
||||
setIsSavingPassword(false);
|
||||
}
|
||||
};
|
||||
|
||||
const displayName = user?.name?.en || user?.username || '';
|
||||
|
||||
return (
|
||||
<Stack gap="lg" maw={820}>
|
||||
<PageHeader title={t('profile.title')} subtitle={t('profile.subtitle')} />
|
||||
|
||||
{/* Profile details */}
|
||||
<Paper p="lg" shadow="sm" radius="md" withBorder>
|
||||
<Group mb="lg">
|
||||
<Group mb="lg" align="center">
|
||||
<Avatar color="emaPrimary" radius="xl" size={64}>
|
||||
BN
|
||||
{getInitials(displayName, user?.email ?? '')}
|
||||
</Avatar>
|
||||
<div>
|
||||
<Title order={4}>Blue Nile Shipping PLC</Title>
|
||||
<TextInput
|
||||
variant="unstyled"
|
||||
defaultValue="muluhabit@gmail.com"
|
||||
readOnly
|
||||
size="sm"
|
||||
/>
|
||||
<Group gap="xs" align="center">
|
||||
<Title order={4}>{displayName || '—'}</Title>
|
||||
<Badge
|
||||
variant="light"
|
||||
color={user?.isPhoneNumberVerified ? 'teal' : 'gray'}
|
||||
size="sm"
|
||||
>
|
||||
{user?.isPhoneNumberVerified
|
||||
? t('profile.verified')
|
||||
: t('profile.unverified')}
|
||||
</Badge>
|
||||
</Group>
|
||||
<Text size="sm" c="dimmed">
|
||||
{user?.email}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
|
||||
<Stack gap="lg">
|
||||
<div>
|
||||
<Title order={5} mb="sm">
|
||||
{t('profile.personal')}
|
||||
</Title>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
||||
<TextInput
|
||||
label={t('profile.fields.fullName')}
|
||||
defaultValue="Dawit Bekele"
|
||||
/>
|
||||
<TextInput
|
||||
label={t('profile.fields.organization')}
|
||||
defaultValue="Blue Nile Shipping PLC"
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</div>
|
||||
<form onSubmit={handleProfileSubmit(onSaveProfile)}>
|
||||
<Stack gap="lg">
|
||||
<div>
|
||||
<Title order={5} mb="sm">
|
||||
{t('profile.personal')}
|
||||
</Title>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
||||
<TextInput
|
||||
label={t('profile.fields.fullNameEn')}
|
||||
leftSection={<IconUser size={18} />}
|
||||
error={profileErrors.nameEn?.message}
|
||||
{...registerProfile('nameEn')}
|
||||
/>
|
||||
<TextInput
|
||||
label={t('profile.fields.fullNameAm')}
|
||||
leftSection={<IconUser size={18} />}
|
||||
error={profileErrors.nameAm?.message}
|
||||
{...registerProfile('nameAm')}
|
||||
/>
|
||||
<TextInput
|
||||
label={t('profile.fields.username')}
|
||||
error={profileErrors.username?.message}
|
||||
{...registerProfile('username')}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Title order={5} mb="sm">
|
||||
{t('profile.contact')}
|
||||
</Title>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
||||
<TextInput
|
||||
label={t('profile.fields.email')}
|
||||
defaultValue="muluhabit@gmail.com"
|
||||
/>
|
||||
<TextInput
|
||||
label={t('profile.fields.phone')}
|
||||
defaultValue="+251 91 234 5678"
|
||||
/>
|
||||
<TextInput
|
||||
label={t('profile.fields.address')}
|
||||
defaultValue="Addis Ababa, Ethiopia"
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</div>
|
||||
<div>
|
||||
<Title order={5} mb="sm">
|
||||
{t('profile.contact')}
|
||||
</Title>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
||||
<TextInput
|
||||
label={t('profile.fields.email')}
|
||||
leftSection={<IconMail size={18} />}
|
||||
error={profileErrors.email?.message}
|
||||
{...registerProfile('email')}
|
||||
/>
|
||||
<TextInput
|
||||
label={t('profile.fields.phone')}
|
||||
leftSection={<IconPhone size={18} />}
|
||||
error={profileErrors.phoneNumber?.message}
|
||||
{...registerProfile('phoneNumber')}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Title order={5} mb="sm">
|
||||
{t('profile.preferences')}
|
||||
</Title>
|
||||
<Select
|
||||
maw={320}
|
||||
label={t('profile.fields.language')}
|
||||
data={SUPPORTED_LANGUAGES.map((lng) => ({
|
||||
value: lng,
|
||||
label: t(`language.${lng}`),
|
||||
}))}
|
||||
value={i18n.language}
|
||||
onChange={(v) => v && i18n.changeLanguage(v as AppLanguage)}
|
||||
allowDeselect={false}
|
||||
<Group>
|
||||
<Button
|
||||
type="submit"
|
||||
loading={isSavingProfile}
|
||||
leftSection={<IconDeviceFloppy size={18} />}
|
||||
>
|
||||
{t('profile.updateProfile')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
</Paper>
|
||||
|
||||
{/* Change password */}
|
||||
<Paper p="lg" shadow="sm" radius="md" withBorder>
|
||||
<Title order={5}>{t('profile.security')}</Title>
|
||||
<Text size="sm" c="dimmed" mb="md">
|
||||
{t('profile.securityHint')}
|
||||
</Text>
|
||||
<Divider mb="md" />
|
||||
|
||||
<form onSubmit={handlePasswordSubmit(onChangePassword)}>
|
||||
<Stack gap="md">
|
||||
<PasswordInput
|
||||
maw={360}
|
||||
label={t('profile.fields.currentPassword')}
|
||||
leftSection={<IconLock size={18} />}
|
||||
error={passwordErrors.oldPassword?.message}
|
||||
{...registerPassword('oldPassword')}
|
||||
/>
|
||||
</div>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
||||
<PasswordInput
|
||||
label={t('profile.fields.newPassword')}
|
||||
leftSection={<IconLock size={18} />}
|
||||
error={passwordErrors.newPassword?.message}
|
||||
{...registerPassword('newPassword')}
|
||||
/>
|
||||
<PasswordInput
|
||||
label={t('profile.fields.confirmPassword')}
|
||||
leftSection={<IconLock size={18} />}
|
||||
error={passwordErrors.confirmPassword?.message}
|
||||
{...registerPassword('confirmPassword')}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
|
||||
<Group>
|
||||
<Button
|
||||
leftSection={<IconDeviceFloppy size={18} />}
|
||||
onClick={handleSave}
|
||||
>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
<Group>
|
||||
<Button
|
||||
type="submit"
|
||||
loading={isSavingPassword}
|
||||
leftSection={<IconLock size={18} />}
|
||||
>
|
||||
{t('profile.changePassword')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
</Paper>
|
||||
|
||||
{/* Preferences */}
|
||||
<Paper p="lg" shadow="sm" radius="md" withBorder>
|
||||
<Title order={5} mb="sm">
|
||||
{t('profile.preferences')}
|
||||
</Title>
|
||||
<Select
|
||||
maw={320}
|
||||
label={t('profile.fields.language')}
|
||||
data={SUPPORTED_LANGUAGES.map((lng) => ({
|
||||
value: lng,
|
||||
label: t(`language.${lng}`),
|
||||
}))}
|
||||
value={i18n.language}
|
||||
onChange={(v) => v && i18n.changeLanguage(v as AppLanguage)}
|
||||
allowDeselect={false}
|
||||
/>
|
||||
</Paper>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -229,13 +229,37 @@ export const am: Translations = {
|
||||
personal: 'የግል መረጃ',
|
||||
contact: 'መገናኛ',
|
||||
preferences: 'ምርጫዎች',
|
||||
security: 'ደህንነት',
|
||||
securityHint: 'በሌላ ቦታ የማይጠቀሙበትን ጠንካራ የይለፍ ቃል ይምረጡ።',
|
||||
changePassword: 'የይለፍ ቃል ይቀይሩ',
|
||||
updateProfile: 'ለውጦችን አስቀምጥ',
|
||||
verified: 'ተረጋግጧል',
|
||||
unverified: 'አልተረጋገጠም',
|
||||
profileUpdated: 'መገለጫ በተሳካ ሁኔታ ተዘምኗል',
|
||||
passwordChanged: 'የይለፍ ቃል በተሳካ ሁኔታ ተቀይሯል',
|
||||
updateFailed: 'መገለጫን ማዘመን አልተቻለም። እባክዎ እንደገና ይሞክሩ።',
|
||||
passwordFailed:
|
||||
'የይለፍ ቃል መቀየር አልተቻለም። የአሁኑን የይለፍ ቃል ያረጋግጡና እንደገና ይሞክሩ።',
|
||||
fields: {
|
||||
fullName: 'ሙሉ ስም',
|
||||
fullNameEn: 'ሙሉ ስም (እንግሊዝኛ)',
|
||||
fullNameAm: 'ሙሉ ስም (አማርኛ)',
|
||||
username: 'የተጠቃሚ ስም',
|
||||
organization: 'ድርጅት',
|
||||
email: 'የኢሜይል አድራሻ',
|
||||
phone: 'ስልክ ቁጥር',
|
||||
address: 'አድራሻ',
|
||||
language: 'የሚመረጥ ቋንቋ',
|
||||
currentPassword: 'የአሁኑ የይለፍ ቃል',
|
||||
newPassword: 'አዲስ የይለፍ ቃል',
|
||||
confirmPassword: 'አዲሱን የይለፍ ቃል ያረጋግጡ',
|
||||
},
|
||||
validation: {
|
||||
nameRequired: 'ስም ያስፈልጋል',
|
||||
emailInvalid: 'ትክክለኛ ኢሜይል ያስገቡ',
|
||||
usernameRequired: 'የተጠቃሚ ስም ያስፈልጋል',
|
||||
phoneRequired: 'ስልክ ቁጥር ያስፈልጋል',
|
||||
passwordMin: 'የይለፍ ቃል ቢያንስ 8 ቁምፊዎች መሆን አለበት',
|
||||
passwordMismatch: 'የይለፍ ቃላት አይዛመዱም',
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -228,13 +228,37 @@ export const en = {
|
||||
personal: 'Personal information',
|
||||
contact: 'Contact',
|
||||
preferences: 'Preferences',
|
||||
security: 'Security',
|
||||
securityHint: 'Choose a strong password you do not use anywhere else.',
|
||||
changePassword: 'Change password',
|
||||
updateProfile: 'Save changes',
|
||||
verified: 'Verified',
|
||||
unverified: 'Unverified',
|
||||
profileUpdated: 'Profile updated successfully',
|
||||
passwordChanged: 'Password changed successfully',
|
||||
updateFailed: 'Could not update profile. Please try again.',
|
||||
passwordFailed:
|
||||
'Could not change password. Check your current password and try again.',
|
||||
fields: {
|
||||
fullName: 'Full name',
|
||||
fullNameEn: 'Full name (English)',
|
||||
fullNameAm: 'Full name (Amharic)',
|
||||
username: 'Username',
|
||||
organization: 'Organization',
|
||||
email: 'Email address',
|
||||
phone: 'Phone number',
|
||||
address: 'Address',
|
||||
language: 'Preferred language',
|
||||
currentPassword: 'Current password',
|
||||
newPassword: 'New password',
|
||||
confirmPassword: 'Confirm new password',
|
||||
},
|
||||
validation: {
|
||||
nameRequired: 'Name is required',
|
||||
emailInvalid: 'Enter a valid email',
|
||||
usernameRequired: 'Username is required',
|
||||
phoneRequired: 'Phone number is required',
|
||||
passwordMin: 'Password must be at least 8 characters',
|
||||
passwordMismatch: 'Passwords do not match',
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user