mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12: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.app.json)",
|
||||||
"Bash(node_modules/.bin/tsc --noEmit -p apps/portal/tsconfig.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(../../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,
|
<!-- EMA logo — white/mono version for dark backgrounds (auth brand panel).
|
||||||
e.g. the auth brand panel. Replace this file (keep the same name/path). -->
|
Same faceted "A" + "ema" wordmark, rendered in white with subtle facet
|
||||||
<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">
|
shading so the folds still read. -->
|
||||||
<rect width="48" height="48" rx="11" fill="#ffffff" />
|
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="ema">
|
||||||
<g transform="translate(12 12)" stroke="#3160b7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none">
|
<polygon points="24,4 8,33 16,33" fill="#ffffff" fill-opacity="0.95" />
|
||||||
<circle cx="12" cy="5" r="2" />
|
<polygon points="24,4 16,33 24,33" fill="#ffffff" fill-opacity="0.78" />
|
||||||
<line x1="12" y1="22" x2="12" y2="7" />
|
<polygon points="24,4 24,33 32,33" fill="#ffffff" fill-opacity="0.55" />
|
||||||
<path d="M5 12H2a10 10 0 0 0 20 0h-3" />
|
<polygon points="24,4 32,33 40,33" fill="#ffffff" fill-opacity="0.88" />
|
||||||
</g>
|
<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>
|
</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
|
<!-- EMA logo — colour version (faceted "A" peak + "ema" wordmark).
|
||||||
(keep the same name/path) with the real EMA mark. SVG or swap for PNG via
|
Hand-built vector approximation of the brand mark; tweak the facet points
|
||||||
the LOGO map in src/app/components/Logo.tsx. -->
|
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 logo">
|
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="ema">
|
||||||
<defs>
|
<!-- faceted A / peak -->
|
||||||
<linearGradient id="emaMark" x1="0" y1="0" x2="48" y2="48" gradientUnits="userSpaceOnUse">
|
<polygon points="24,4 8,33 16,33" fill="#8db0ee" />
|
||||||
<stop stop-color="#3160b7" />
|
<polygon points="24,4 16,33 24,33" fill="#4b7fe5" />
|
||||||
<stop offset="1" stop-color="#1fc29d" />
|
<polygon points="24,4 24,33 32,33" fill="#1c2c4a" />
|
||||||
</linearGradient>
|
<polygon points="24,4 32,33 40,33" fill="#3160b7" />
|
||||||
</defs>
|
<!-- wordmark -->
|
||||||
<rect width="48" height="48" rx="11" fill="url(#emaMark)" />
|
<text x="24" y="46" text-anchor="middle"
|
||||||
<g transform="translate(12 12)" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none">
|
font-family="Inter, 'Segoe UI', Arial, sans-serif"
|
||||||
<circle cx="12" cy="5" r="2" />
|
font-size="13" font-weight="700" letter-spacing="-0.5" fill="#3160b7">ema</text>
|
||||||
<line x1="12" y1="22" x2="12" y2="7" />
|
|
||||||
<path d="M5 12H2a10 10 0 0 0 20 0h-3" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
</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">
|
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<defs>
|
<polygon points="24,3 5,45 16,45" fill="#8db0ee" />
|
||||||
<linearGradient id="favMark" x1="0" y1="0" x2="48" y2="48" gradientUnits="userSpaceOnUse">
|
<polygon points="24,3 16,45 24,45" fill="#4b7fe5" />
|
||||||
<stop stop-color="#3160b7" />
|
<polygon points="24,3 24,45 32,45" fill="#1c2c4a" />
|
||||||
<stop offset="1" stop-color="#1fc29d" />
|
<polygon points="24,3 32,45 43,45" fill="#3160b7" />
|
||||||
</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>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 728 B After Width: | Height: | Size: 406 B |
@@ -1,45 +1,196 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
|
Divider,
|
||||||
Group,
|
Group,
|
||||||
Paper,
|
Paper,
|
||||||
|
PasswordInput,
|
||||||
Select,
|
Select,
|
||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
Stack,
|
Stack,
|
||||||
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
} from '@mantine/core';
|
} 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 { useTranslation } from 'react-i18next';
|
||||||
import { notify } from '@ema-platform/ui';
|
import { notify } from '@ema-platform/ui';
|
||||||
|
import { useApiMutation } from '@ema-platform/api';
|
||||||
import { PageHeader } from '../../../components/PageHeader';
|
import { PageHeader } from '../../../components/PageHeader';
|
||||||
import { SUPPORTED_LANGUAGES, type AppLanguage } from '../../../i18n/config';
|
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() {
|
export function ProfilePage() {
|
||||||
const { t, i18n } = useTranslation();
|
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 (
|
return (
|
||||||
<Stack gap="lg" maw={820}>
|
<Stack gap="lg" maw={820}>
|
||||||
<PageHeader title={t('profile.title')} subtitle={t('profile.subtitle')} />
|
<PageHeader title={t('profile.title')} subtitle={t('profile.subtitle')} />
|
||||||
|
|
||||||
|
{/* Profile details */}
|
||||||
<Paper p="lg" shadow="sm" radius="md" withBorder>
|
<Paper p="lg" shadow="sm" radius="md" withBorder>
|
||||||
<Group mb="lg">
|
<Group mb="lg" align="center">
|
||||||
<Avatar color="emaPrimary" radius="xl" size={64}>
|
<Avatar color="emaPrimary" radius="xl" size={64}>
|
||||||
BN
|
{getInitials(displayName, user?.email ?? '')}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div>
|
<div>
|
||||||
<Title order={4}>Blue Nile Shipping PLC</Title>
|
<Group gap="xs" align="center">
|
||||||
<TextInput
|
<Title order={4}>{displayName || '—'}</Title>
|
||||||
variant="unstyled"
|
<Badge
|
||||||
defaultValue="muluhabit@gmail.com"
|
variant="light"
|
||||||
readOnly
|
color={user?.isPhoneNumberVerified ? 'teal' : 'gray'}
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
>
|
||||||
|
{user?.isPhoneNumberVerified
|
||||||
|
? t('profile.verified')
|
||||||
|
: t('profile.unverified')}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{user?.email}
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
|
<form onSubmit={handleProfileSubmit(onSaveProfile)}>
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<div>
|
<div>
|
||||||
<Title order={5} mb="sm">
|
<Title order={5} mb="sm">
|
||||||
@@ -47,12 +198,21 @@ export function ProfilePage() {
|
|||||||
</Title>
|
</Title>
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label={t('profile.fields.fullName')}
|
label={t('profile.fields.fullNameEn')}
|
||||||
defaultValue="Dawit Bekele"
|
leftSection={<IconUser size={18} />}
|
||||||
|
error={profileErrors.nameEn?.message}
|
||||||
|
{...registerProfile('nameEn')}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label={t('profile.fields.organization')}
|
label={t('profile.fields.fullNameAm')}
|
||||||
defaultValue="Blue Nile Shipping PLC"
|
leftSection={<IconUser size={18} />}
|
||||||
|
error={profileErrors.nameAm?.message}
|
||||||
|
{...registerProfile('nameAm')}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label={t('profile.fields.username')}
|
||||||
|
error={profileErrors.username?.message}
|
||||||
|
{...registerProfile('username')}
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,20 +224,79 @@ export function ProfilePage() {
|
|||||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label={t('profile.fields.email')}
|
label={t('profile.fields.email')}
|
||||||
defaultValue="muluhabit@gmail.com"
|
leftSection={<IconMail size={18} />}
|
||||||
|
error={profileErrors.email?.message}
|
||||||
|
{...registerProfile('email')}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label={t('profile.fields.phone')}
|
label={t('profile.fields.phone')}
|
||||||
defaultValue="+251 91 234 5678"
|
leftSection={<IconPhone size={18} />}
|
||||||
/>
|
error={profileErrors.phoneNumber?.message}
|
||||||
<TextInput
|
{...registerProfile('phoneNumber')}
|
||||||
label={t('profile.fields.address')}
|
|
||||||
defaultValue="Addis Ababa, Ethiopia"
|
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<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')}
|
||||||
|
/>
|
||||||
|
<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
|
||||||
|
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">
|
<Title order={5} mb="sm">
|
||||||
{t('profile.preferences')}
|
{t('profile.preferences')}
|
||||||
</Title>
|
</Title>
|
||||||
@@ -92,17 +311,6 @@ export function ProfilePage() {
|
|||||||
onChange={(v) => v && i18n.changeLanguage(v as AppLanguage)}
|
onChange={(v) => v && i18n.changeLanguage(v as AppLanguage)}
|
||||||
allowDeselect={false}
|
allowDeselect={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Group>
|
|
||||||
<Button
|
|
||||||
leftSection={<IconDeviceFloppy size={18} />}
|
|
||||||
onClick={handleSave}
|
|
||||||
>
|
|
||||||
{t('common.save')}
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -229,13 +229,37 @@ export const am: Translations = {
|
|||||||
personal: 'የግል መረጃ',
|
personal: 'የግል መረጃ',
|
||||||
contact: 'መገናኛ',
|
contact: 'መገናኛ',
|
||||||
preferences: 'ምርጫዎች',
|
preferences: 'ምርጫዎች',
|
||||||
|
security: 'ደህንነት',
|
||||||
|
securityHint: 'በሌላ ቦታ የማይጠቀሙበትን ጠንካራ የይለፍ ቃል ይምረጡ።',
|
||||||
|
changePassword: 'የይለፍ ቃል ይቀይሩ',
|
||||||
|
updateProfile: 'ለውጦችን አስቀምጥ',
|
||||||
|
verified: 'ተረጋግጧል',
|
||||||
|
unverified: 'አልተረጋገጠም',
|
||||||
|
profileUpdated: 'መገለጫ በተሳካ ሁኔታ ተዘምኗል',
|
||||||
|
passwordChanged: 'የይለፍ ቃል በተሳካ ሁኔታ ተቀይሯል',
|
||||||
|
updateFailed: 'መገለጫን ማዘመን አልተቻለም። እባክዎ እንደገና ይሞክሩ።',
|
||||||
|
passwordFailed:
|
||||||
|
'የይለፍ ቃል መቀየር አልተቻለም። የአሁኑን የይለፍ ቃል ያረጋግጡና እንደገና ይሞክሩ።',
|
||||||
fields: {
|
fields: {
|
||||||
fullName: 'ሙሉ ስም',
|
fullNameEn: 'ሙሉ ስም (እንግሊዝኛ)',
|
||||||
|
fullNameAm: 'ሙሉ ስም (አማርኛ)',
|
||||||
|
username: 'የተጠቃሚ ስም',
|
||||||
organization: 'ድርጅት',
|
organization: 'ድርጅት',
|
||||||
email: 'የኢሜይል አድራሻ',
|
email: 'የኢሜይል አድራሻ',
|
||||||
phone: 'ስልክ ቁጥር',
|
phone: 'ስልክ ቁጥር',
|
||||||
address: 'አድራሻ',
|
address: 'አድራሻ',
|
||||||
language: 'የሚመረጥ ቋንቋ',
|
language: 'የሚመረጥ ቋንቋ',
|
||||||
|
currentPassword: 'የአሁኑ የይለፍ ቃል',
|
||||||
|
newPassword: 'አዲስ የይለፍ ቃል',
|
||||||
|
confirmPassword: 'አዲሱን የይለፍ ቃል ያረጋግጡ',
|
||||||
|
},
|
||||||
|
validation: {
|
||||||
|
nameRequired: 'ስም ያስፈልጋል',
|
||||||
|
emailInvalid: 'ትክክለኛ ኢሜይል ያስገቡ',
|
||||||
|
usernameRequired: 'የተጠቃሚ ስም ያስፈልጋል',
|
||||||
|
phoneRequired: 'ስልክ ቁጥር ያስፈልጋል',
|
||||||
|
passwordMin: 'የይለፍ ቃል ቢያንስ 8 ቁምፊዎች መሆን አለበት',
|
||||||
|
passwordMismatch: 'የይለፍ ቃላት አይዛመዱም',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -228,13 +228,37 @@ export const en = {
|
|||||||
personal: 'Personal information',
|
personal: 'Personal information',
|
||||||
contact: 'Contact',
|
contact: 'Contact',
|
||||||
preferences: 'Preferences',
|
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: {
|
fields: {
|
||||||
fullName: 'Full name',
|
fullNameEn: 'Full name (English)',
|
||||||
|
fullNameAm: 'Full name (Amharic)',
|
||||||
|
username: 'Username',
|
||||||
organization: 'Organization',
|
organization: 'Organization',
|
||||||
email: 'Email address',
|
email: 'Email address',
|
||||||
phone: 'Phone number',
|
phone: 'Phone number',
|
||||||
address: 'Address',
|
address: 'Address',
|
||||||
language: 'Preferred language',
|
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