feat: enhance AmharicDatePicker with Ethiopian time selection and formatting

This commit is contained in:
estifanos
2026-08-11 12:20:22 +00:00
parent 7c3a25245f
commit b2f99200e7

View File

@@ -6,6 +6,7 @@ import {
Group,
MantineSize,
Popover,
Select,
TextInput,
} from '@mantine/core';
import { TimeInput } from '@mantine/dates';
@@ -51,6 +52,40 @@ function toAmharicDisplay(date: Date): string {
}
}
export type EthPeriod = 'lelit' | 'tewat' | 'ken' | 'mata';
// Ethiopian day starts at 6am. Period is picked from the 24h hour; the
// displayed hour is the Western hour shifted 6, wrapped onto a 12-hour dial.
// Each period only spans 6 hours (not 12): ጠዋት/ማታ show 12,1..5, ቀን/ሌሊት show
// 6..11 — offering all 12 hours in every period would let a user pick e.g.
// "ጠዋት 7", which has no 06:0011:59 preimage.
const ETH_PERIODS: { value: EthPeriod; label: string; hours: number[] }[] = [
{ value: 'tewat', label: 'ጠዋት', hours: [12, 1, 2, 3, 4, 5] }, // 06:0011:59
{ value: 'ken', label: 'ቀን', hours: [6, 7, 8, 9, 10, 11] }, // 12:0017:59
{ value: 'mata', label: 'ማታ', hours: [12, 1, 2, 3, 4, 5] }, // 18:0023:59
{ value: 'lelit', label: 'ሌሊት', hours: [6, 7, 8, 9, 10, 11] }, // 00:0005:59
];
export function toEthTime(h24: number): { period: EthPeriod; hour: number } {
const period: EthPeriod =
h24 < 6 ? 'lelit' : h24 < 12 ? 'tewat' : h24 < 18 ? 'ken' : 'mata';
return { period, hour: ((h24 + 6) % 12) || 12 };
}
export function fromEthTime(period: EthPeriod, hour: number): number {
// Inverse of the shift, then re-add the 12h that %12 discarded for the
// afternoon/night pair of periods.
const pm = period === 'ken' || period === 'mata';
return ((hour + 6) % 12) + (pm ? 12 : 0);
}
function ethTimeLabel(date: Date): string {
const { period, hour } = toEthTime(date.getHours());
const minutes = String(date.getMinutes()).padStart(2, '0');
const label = ETH_PERIODS.find((p) => p.value === period)?.label ?? '';
return `${hour}:${minutes} ${label}`;
}
// react-day-picker calls these with the Gregorian Date it tracks internally;
// override so the caption/dropdown show Amharic month names instead of the
// library's Latin transliteration (triggered by numerals="latn" below).
@@ -215,7 +250,9 @@ export function AmharicDatePicker({
: '';
const timeLabel =
withTime && selected
? ` ${selected.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}`
? calendarType === 'AMH'
? ` ${ethTimeLabel(selected)}`
: ` ${selected.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}`
: '';
const displayValue = dateLabel + timeLabel;
@@ -329,7 +366,61 @@ export function AmharicDatePicker({
/>
)}
{withTime && (
{withTime && calendarType === 'AMH' && (() => {
const { period, hour } = selected ? toEthTime(selected.getHours()) : { period: 'tewat' as EthPeriod, hour: 12 };
const minute = selected ? selected.getMinutes() : 0;
const periodHours = ETH_PERIODS.find((p) => p.value === period)?.hours ?? ETH_PERIODS[0].hours;
const setTime = (h24: number, m: number) =>
onChange?.(
formatWireValue(mergeDateTime(selected, undefined, { hours: h24, minutes: m }), dateFormat, true),
);
return (
<Group grow mt="sm">
<Select
label={t('common.time')}
disabled={!selected}
value={period}
data={ETH_PERIODS.map((p) => ({ value: p.value, label: p.label }))}
allowDeselect={false}
onChange={(next) => {
if (!next) return;
// Keep the same slot index within the new period so the
// hour never lands outside its 6-hour range.
const slot = Math.max(periodHours.indexOf(hour), 0);
const nextHours = ETH_PERIODS.find((p) => p.value === next)?.hours ?? periodHours;
setTime(fromEthTime(next as EthPeriod, nextHours[slot]), minute);
}}
/>
<Select
label=" "
disabled={!selected}
value={String(hour)}
data={periodHours.map((h) => ({ value: String(h), label: String(h) }))}
allowDeselect={false}
onChange={(next) => {
if (!next) return;
setTime(fromEthTime(period, Number(next)), minute);
}}
/>
<Select
label=" "
disabled={!selected}
value={String(minute).padStart(2, '0')}
data={Array.from({ length: 60 }, (_, m) => ({
value: String(m).padStart(2, '0'),
label: String(m).padStart(2, '0'),
}))}
allowDeselect={false}
onChange={(next) => {
if (next == null) return;
setTime(fromEthTime(period, hour), Number(next));
}}
/>
</Group>
);
})()}
{withTime && calendarType === 'EN' && (
<TimeInput
label={t('common.time')}
mt="sm"