feat(AmharicDatePicker): add time input functionality and update date formatting

feat(i18n): add translation for time label in Amharic and English
fix(main): ensure date styles are imported correctly
This commit is contained in:
estifanos
2026-08-05 09:48:35 +00:00
parent f06f74b9e0
commit 5fb1926ac0
4 changed files with 65 additions and 11 deletions

View File

@@ -8,6 +8,7 @@ import {
Popover,
TextInput,
} from '@mantine/core';
import { TimeInput } from '@mantine/dates';
import { useDisclosure } from '@mantine/hooks';
import { DayPicker as EthiopicDayPicker } from '@daypicker/ethiopic';
import { DayPicker as GregorianDayPicker } from '@daypicker/react';
@@ -65,17 +66,38 @@ const ETH_FORMATTERS = {
// midnight — reading back with getUTC*() (not local get*()) keeps the
// calendar day stable regardless of the runtime's timezone, avoiding the
// same off-by-one class of bug the UTC-noon workaround above exists for.
function parseISO(value?: string | null): Date | null {
function parseISO(value?: string | null, withTime = false): Date | null {
if (!value) return null;
const d = new Date(value);
if (isNaN(d.getTime())) return null;
return new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
return withTime
? d
: new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
}
function formatISO(date: Date): string {
return new Date(
Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()),
).toISOString();
function formatISO(date: Date, withTime = false): string {
return withTime
? date.toISOString()
: new Date(
Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()),
).toISOString();
}
// Combines a calendar day with a time-of-day, keeping whichever half isn't
// changing. `base` is the currently selected Date (may be null if nothing
// picked yet); `day`/`time` override only the half that's provided.
function mergeDateTime(
base: Date | null,
day?: Date,
time?: { hours: number; minutes: number },
): Date {
const result = day ? new Date(day) : new Date(base ?? new Date());
if (time) {
result.setHours(time.hours, time.minutes, 0, 0);
} else if (day && base) {
result.setHours(base.getHours(), base.getMinutes(), base.getSeconds(), 0);
}
return result;
}
export function toEthiopicDateLabel(date: Date | string): string {
@@ -108,6 +130,9 @@ export interface AmharicDatePickerProps {
size?: MantineSize;
name?: string;
onBlur?: () => void;
/** Show a time-of-day field alongside the calendar. Off by default —
* most callers only need a calendar day. */
withTime?: boolean;
}
export function AmharicDatePicker({
@@ -121,6 +146,7 @@ export function AmharicDatePicker({
size,
name,
onBlur,
withTime = false,
}: AmharicDatePickerProps) {
const { t, i18n } = useTranslation();
const [calendarType, setCalendarType] = useState<'EN' | 'AMH'>(() =>
@@ -128,9 +154,9 @@ export function AmharicDatePicker({
);
const [opened, { close, toggle }] = useDisclosure(false);
const selected = parseISO(value);
const selected = parseISO(value, withTime);
const displayValue = selected
const dateLabel = selected
? calendarType === 'EN'
? selected.toLocaleDateString('en-US', {
year: 'numeric',
@@ -139,6 +165,11 @@ export function AmharicDatePicker({
})
: toAmharicDisplay(selected)
: '';
const timeLabel =
withTime && selected
? ` ${selected.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}`
: '';
const displayValue = dateLabel + timeLabel;
return (
<Popover
@@ -199,7 +230,7 @@ export function AmharicDatePicker({
captionLayout="dropdown"
formatters={ETH_FORMATTERS}
onSelect={(date: Date | undefined) => {
onChange?.(date ? formatISO(date) : '');
onChange?.(date ? formatISO(mergeDateTime(selected, date), withTime) : '');
close();
}}
/>
@@ -212,12 +243,32 @@ export function AmharicDatePicker({
endMonth={YEAR_DROPDOWN_END}
captionLayout="dropdown"
onSelect={(date: Date | undefined) => {
onChange?.(date ? formatISO(date) : '');
onChange?.(date ? formatISO(mergeDateTime(selected, date), withTime) : '');
close();
}}
/>
)}
{withTime && (
<TimeInput
label={t('common.time')}
mt="sm"
disabled={!selected}
value={
selected
? `${String(selected.getHours()).padStart(2, '0')}:${String(selected.getMinutes()).padStart(2, '0')}`
: ''
}
onChange={(e) => {
const [h, m] = e.currentTarget.value.split(':').map(Number);
if (Number.isNaN(h) || Number.isNaN(m)) return;
onChange?.(
formatISO(mergeDateTime(selected, undefined, { hours: h, minutes: m }), true),
);
}}
/>
)}
<Group justify="space-between" mt="sm">
<Button
variant="subtle"
@@ -233,7 +284,7 @@ export function AmharicDatePicker({
variant="light"
size="xs"
onClick={() => {
onChange?.(formatISO(new Date()));
onChange?.(formatISO(new Date(), withTime));
close();
}}
>

View File

@@ -92,6 +92,7 @@ export const am: Translations = {
toggleTheme: 'ብርሃን / ጨለማ ገጽታ ቀይር',
welcome: 'እንኳን ደህና መጡ',
switchCalendar: 'የቀን መቁጠሪያ ዓይነት ቀይር',
time: 'ሰዓት',
},
auth: {

View File

@@ -90,6 +90,7 @@ export const en = {
toggleTheme: 'Toggle light / dark mode',
welcome: 'Welcome',
switchCalendar: 'Switch calendar type',
time: 'Time',
},
auth: {

View File

@@ -1,6 +1,7 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import '@mantine/core/styles.css';
import '@mantine/dates/styles.css';
import '@mantine/notifications/styles.css';
import './app/theme/portal.css';