mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-07 17:45:43 +00:00
feat: add i18n support and localized labels to certification and profession features commit
This commit is contained in:
@@ -1,76 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
TextInput,
|
||||
UnstyledButton,
|
||||
rem,
|
||||
type TextInputProps,
|
||||
} from '@mantine/core';
|
||||
|
||||
export interface BilingualValue {
|
||||
en: string;
|
||||
am: string;
|
||||
}
|
||||
|
||||
interface BilingualInputProps
|
||||
extends Omit<TextInputProps, 'value' | 'onChange' | 'rightSection' | 'rightSectionWidth'> {
|
||||
value: BilingualValue;
|
||||
onChange: (value: BilingualValue) => void;
|
||||
}
|
||||
|
||||
export function BilingualInput({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
required,
|
||||
placeholder,
|
||||
...rest
|
||||
}: BilingualInputProps) {
|
||||
const [lang, setLang] = useState<'en' | 'am'>('en');
|
||||
|
||||
const toggle = () => setLang((l) => (l === 'en' ? 'am' : 'en'));
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
label={label}
|
||||
required={required}
|
||||
placeholder={placeholder ?? (lang === 'en' ? 'Enter in English' : 'በአማርኛ ያስገቡ')}
|
||||
value={value[lang]}
|
||||
onChange={(e) => onChange({ ...value, [lang]: e.currentTarget.value })}
|
||||
rightSection={
|
||||
<UnstyledButton
|
||||
onClick={toggle}
|
||||
aria-label={`Switch to ${lang === 'en' ? 'Amharic' : 'English'}`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: rem(28),
|
||||
height: rem(20),
|
||||
borderRadius: rem(4),
|
||||
fontSize: rem(10),
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.05em',
|
||||
background:
|
||||
lang === 'en'
|
||||
? 'var(--mantine-color-blue-1)'
|
||||
: 'var(--mantine-color-teal-1)',
|
||||
color:
|
||||
lang === 'en'
|
||||
? 'var(--mantine-color-blue-7)'
|
||||
: 'var(--mantine-color-teal-7)',
|
||||
cursor: 'pointer',
|
||||
transition: 'background 150ms ease',
|
||||
}}
|
||||
>
|
||||
{lang === 'en' ? 'EN' : 'AM'}
|
||||
</UnstyledButton>
|
||||
}
|
||||
styles={{
|
||||
input: {
|
||||
paddingRight: rem(42),
|
||||
},
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -6,11 +6,12 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface LocationPickerProps {
|
||||
value?: string;
|
||||
onChange: (locationId: string | null) => void;
|
||||
onChange?: (locationId: string | null) => void;
|
||||
onChainChange?: (chain: Location[]) => void;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export function LocationPicker({ value, onChange, required }: LocationPickerProps) {
|
||||
export function LocationPicker({ value, onChange, onChainChange, required }: LocationPickerProps) {
|
||||
const { t } = useTranslation();
|
||||
const { data: typesRes, isLoading: typesLoading } = useGetLocationTypesQuery();
|
||||
const { data: locsRes, isLoading: locsLoading } = useGetLocationsQuery({ take: 10000 });
|
||||
@@ -54,7 +55,8 @@ export function LocationPicker({ value, onChange, required }: LocationPickerProp
|
||||
current = current.parentId ? locMap.get(current.parentId) : undefined;
|
||||
}
|
||||
setSelectedChain(chain);
|
||||
}, [value, locMap]);
|
||||
onChainChange?.(chain);
|
||||
}, [value, locMap, onChainChange]);
|
||||
|
||||
const currentLevelChildren = useMemo(() => {
|
||||
const parentId =
|
||||
@@ -89,7 +91,8 @@ export function LocationPicker({ value, onChange, required }: LocationPickerProp
|
||||
if (!id) {
|
||||
const newChain = selectedChain.slice(0, -1);
|
||||
setSelectedChain(newChain);
|
||||
onChange(newChain.length > 0 ? newChain[newChain.length - 1].id : null);
|
||||
onChange?.(newChain.length > 0 ? newChain[newChain.length - 1].id : null);
|
||||
onChainChange?.(newChain);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -100,9 +103,10 @@ export function LocationPicker({ value, onChange, required }: LocationPickerProp
|
||||
newChain.push(loc);
|
||||
setSelectedChain(newChain);
|
||||
|
||||
onChange(id);
|
||||
onChange?.(id);
|
||||
onChainChange?.(newChain);
|
||||
},
|
||||
[selectedChain, locMap, onChange, depth],
|
||||
[selectedChain, locMap, onChange, onChainChange, depth],
|
||||
);
|
||||
|
||||
const buildOptions = (levelIdx: number) => {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { Select, SimpleGrid, Text, TextInput } from '@mantine/core';
|
||||
import type { FieldErrors, UseFormRegister, UseFormSetValue, UseFormWatch, UseFormTrigger } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { LocationPicker } from '../../location/components/LocationPicker';
|
||||
import { useGetLocationTypesQuery } from '../../location/api/location-api';
|
||||
import type { Location } from '../../location/types/location';
|
||||
|
||||
export const addressSchema = z.object({
|
||||
idType: z.string().min(1, 'Select ID type'),
|
||||
@@ -25,6 +29,13 @@ export type AddressValues = z.infer<typeof addressSchema>;
|
||||
|
||||
export const ID_TYPES = ['NID', 'VITAL', 'PASSPORT', 'DRIVERS_LICENSE'] as const;
|
||||
|
||||
const LEVEL_TO_FIELD: Record<number, keyof AddressValues> = {
|
||||
1: 'cityId',
|
||||
2: 'subcityId',
|
||||
3: 'woredaId',
|
||||
4: 'kebeleId',
|
||||
};
|
||||
|
||||
interface AddressFormContentProps {
|
||||
register: UseFormRegister<AddressValues>;
|
||||
errors: FieldErrors<AddressValues>;
|
||||
@@ -40,6 +51,38 @@ export function AddressFormContent({
|
||||
watch,
|
||||
trigger,
|
||||
}: AddressFormContentProps) {
|
||||
const { data: typesRes } = useGetLocationTypesQuery();
|
||||
const locationTypes = typesRes?.items ?? [];
|
||||
|
||||
const typeLevelMap = useMemo(() => {
|
||||
const map = new Map<string, number>();
|
||||
locationTypes.forEach((lt) => map.set(lt.id, lt.level));
|
||||
return map;
|
||||
}, [locationTypes]);
|
||||
|
||||
const leafId = watch('kebeleId') || watch('woredaId') || watch('subcityId') || watch('cityId') || undefined;
|
||||
|
||||
const handleChainChange = useCallback(
|
||||
(chain: Location[]) => {
|
||||
if (chain.length > 0 && !typeLevelMap.has(chain[0].locationTypeId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setValue('cityId', '');
|
||||
setValue('subcityId', '');
|
||||
setValue('woredaId', '');
|
||||
setValue('kebeleId', '');
|
||||
|
||||
chain.forEach((loc) => {
|
||||
const level = typeLevelMap.get(loc.locationTypeId);
|
||||
if (level && LEVEL_TO_FIELD[level]) {
|
||||
setValue(LEVEL_TO_FIELD[level], loc.id);
|
||||
}
|
||||
});
|
||||
},
|
||||
[setValue, typeLevelMap],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
|
||||
@@ -93,37 +136,11 @@ export function AddressFormContent({
|
||||
<Text fw={600} fz="sm" tt="uppercase" c="gray.6" mt="lg" mb="sm">
|
||||
Address
|
||||
</Text>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
|
||||
<TextInput
|
||||
label="Region ID"
|
||||
placeholder="Region UUID (optional)"
|
||||
{...register('regionId')}
|
||||
error={errors.regionId?.message}
|
||||
/>
|
||||
<TextInput
|
||||
label="City ID"
|
||||
placeholder="City UUID (optional)"
|
||||
{...register('cityId')}
|
||||
error={errors.cityId?.message}
|
||||
/>
|
||||
<TextInput
|
||||
label="Subcity ID"
|
||||
placeholder="Subcity UUID (optional)"
|
||||
{...register('subcityId')}
|
||||
error={errors.subcityId?.message}
|
||||
/>
|
||||
<TextInput
|
||||
label="Woreda ID"
|
||||
placeholder="Woreda UUID (optional)"
|
||||
{...register('woredaId')}
|
||||
error={errors.woredaId?.message}
|
||||
/>
|
||||
<TextInput
|
||||
label="Kebele ID"
|
||||
placeholder="Kebele UUID (optional)"
|
||||
{...register('kebeleId')}
|
||||
error={errors.kebeleId?.message}
|
||||
/>
|
||||
<LocationPicker
|
||||
value={leafId}
|
||||
onChainChange={handleChainChange}
|
||||
/>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md" mt="md">
|
||||
<TextInput
|
||||
label="Street Address"
|
||||
placeholder="Street name, house number"
|
||||
|
||||
@@ -33,9 +33,8 @@ import {
|
||||
IconUser,
|
||||
} from '@tabler/icons-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { notify } from '@ema-platform/ui';
|
||||
import { BilingualInput } from '../../../components/BilingualInput';
|
||||
import type { BilingualValue } from '../../../components/BilingualInput';
|
||||
import { notify, BilingualInput } from '@ema-platform/ui';
|
||||
import type { BilingualValue } from '@ema-platform/ui';
|
||||
import { AmharicDatePicker, toEthiopicDateLabel } from '../../../components/AmharicDatePicker';
|
||||
import { LocationPicker } from '../../location/components/LocationPicker';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user