feat: integrate AmharicDatePicker component across multiple pages and update translations

This commit is contained in:
estifanos
2026-07-31 10:27:50 +00:00
parent b5858c37be
commit 48bc31d321
17 changed files with 205 additions and 76 deletions

View File

@@ -1,6 +1,13 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ActionIcon, Button, Group, Popover, TextInput } from '@mantine/core';
import {
ActionIcon,
Button,
Group,
MantineSize,
Popover,
TextInput,
} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { DayPicker as EthiopicDayPicker } from '@daypicker/ethiopic';
import { DayPicker as GregorianDayPicker } from '@daypicker/react';
@@ -52,23 +59,48 @@ const ETH_FORMATTERS = {
formatMonthDropdown: (month: Date) => ethMonthName(month),
};
export function toEthiopicDateLabel(date: Date): string {
// Local-date parse/format for the "YYYY-MM-DD" wire format (matches native
// <input type="date"> semantics). Deliberately NOT `new Date(iso)` (parses as
// UTC midnight, off-by-one in negative-offset zones) and NOT
// `.toISOString()` (shifts by the local offset when formatting) — same class
// of bug the UTC-noon workaround above already had to fix once in this file.
function parseISO(value?: string | null): Date | null {
if (!value) return null;
const [y, m, d] = value.split('-').map(Number);
return y && m && d ? new Date(y, m - 1, d) : null;
}
function formatISO(date: Date): string {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
}
export function toEthiopicDateLabel(date: Date | string): string {
const d = typeof date === 'string' ? parseISO(date) : date;
if (!d) return '';
try {
const eth = toEthDateTime(date);
const eth = toEthDateTime(d);
return `EC ${eth.year}-${String(eth.month).padStart(2, '0')}-${String(eth.date).padStart(2, '0')}`;
} catch {
return date.toLocaleDateString('en-US');
return d.toLocaleDateString('en-US');
}
}
const YEAR_DROPDOWN_END = new Date(new Date().getFullYear() + 50, 11, 31);
export interface AmharicDatePickerProps {
label?: string;
value?: Date | null;
onChange?: (date: Date | null) => void;
label?: React.ReactNode;
value?: string | null;
onChange?: (value: string) => void;
required?: boolean;
placeholder?: string;
error?: React.ReactNode;
disabled?: boolean;
size?: MantineSize;
name?: string;
onBlur?: () => void;
}
export function AmharicDatePicker({
@@ -77,21 +109,28 @@ export function AmharicDatePicker({
onChange,
required,
placeholder,
error,
disabled,
size,
name,
onBlur,
}: AmharicDatePickerProps) {
const { i18n } = useTranslation();
const { t, i18n } = useTranslation();
const [calendarType, setCalendarType] = useState<'EN' | 'AMH'>(() =>
i18n.language?.startsWith('am') ? 'AMH' : 'EN',
);
const [opened, { close, toggle }] = useDisclosure(false);
const displayValue = value
const selected = parseISO(value);
const displayValue = selected
? calendarType === 'EN'
? value.toLocaleDateString('en-US', {
? selected.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
: toAmharicDisplay(value)
: toAmharicDisplay(selected)
: '';
return (
@@ -109,24 +148,32 @@ export function AmharicDatePicker({
required={required}
value={displayValue}
readOnly
disabled={disabled}
size={size}
name={name}
error={error}
placeholder={placeholder}
onClick={toggle}
onClick={() => !disabled && toggle()}
onBlur={onBlur}
leftSection={
<Button
type="button"
variant="light"
size="compact-xs"
tabIndex={0}
disabled={disabled}
onClick={(e) => {
e.stopPropagation();
setCalendarType((prev) => (prev === 'EN' ? 'AMH' : 'EN'));
}}
aria-label="Switch calendar type"
aria-label={t('common.switchCalendar')}
>
{calendarType}
</Button>
}
leftSectionWidth="calc(4.375rem * var(--mantine-scale))"
rightSection={
<ActionIcon size="md" variant="transparent" onClick={toggle}>
<ActionIcon size="md" variant="transparent" disabled={disabled} onClick={() => toggle()}>
<IconCalendarEvent size={20} />
</ActionIcon>
}
@@ -138,14 +185,14 @@ export function AmharicDatePicker({
<EthiopicDayPicker
className="amharic-daypicker"
mode="single"
selected={value ?? undefined}
defaultMonth={value ?? undefined}
selected={selected ?? undefined}
defaultMonth={selected ?? undefined}
endMonth={YEAR_DROPDOWN_END}
numerals="latn"
captionLayout="dropdown"
formatters={ETH_FORMATTERS}
onSelect={(date: Date | undefined) => {
onChange?.(date ?? null);
onChange?.(date ? formatISO(date) : '');
close();
}}
/>
@@ -153,12 +200,12 @@ export function AmharicDatePicker({
<GregorianDayPicker
className="amharic-daypicker"
mode="single"
selected={value ?? undefined}
defaultMonth={value ?? undefined}
selected={selected ?? undefined}
defaultMonth={selected ?? undefined}
endMonth={YEAR_DROPDOWN_END}
captionLayout="dropdown"
onSelect={(date: Date | undefined) => {
onChange?.(date ?? null);
onChange?.(date ? formatISO(date) : '');
close();
}}
/>
@@ -169,7 +216,7 @@ export function AmharicDatePicker({
variant="subtle"
size="xs"
onClick={() => {
onChange?.(null);
onChange?.('');
close();
}}
>
@@ -179,7 +226,7 @@ export function AmharicDatePicker({
variant="light"
size="xs"
onClick={() => {
onChange?.(new Date());
onChange?.(formatISO(new Date()));
close();
}}
>