Files
dyolink/frontend/src/components/ui/shared/CalendarDayPartsPanel.tsx

172 lines
5.5 KiB
TypeScript

'use client';
import { useLocale, useTranslations } from 'next-intl';
import { formatAppInteger, usesPersianCalendar } from '@/lib/i18n/format';
import {
formatPersianMonthLabel,
getLocalPersianParts,
jalaliDaysInMonth,
persianPartsToLocalDate,
persianYearRange,
} from '@/lib/i18n/persianCalendar';
import { startOfLocalDay } from '@/components/appointments/appointmentTime';
import { CompactSelect } from '@/components/ui/shared/CompactSelect';
const MONTH_KEYS = [
'monthJanuary',
'monthFebruary',
'monthMarch',
'monthApril',
'monthMay',
'monthJune',
'monthJuly',
'monthAugust',
'monthSeptember',
'monthOctober',
'monthNovember',
'monthDecember',
] as const;
const COMPACT_SELECT_CLASS = 'text-xs ps-2 pe-8 py-1';
function daysInMonth(year: number, month: number): number {
return new Date(year, month + 1, 0).getDate();
}
function buildLocalDay(year: number, month: number, day: number): Date {
return new Date(year, month, day, 0, 0, 0, 0);
}
function yearRange(anchor: Date): number[] {
const anchorYear = anchor.getFullYear();
const years: number[] = [];
for (let y = anchorYear - 10; y <= anchorYear + 2; y += 1) {
years.push(y);
}
return years;
}
export type CalendarDayPartsPanelProps = {
panelId: string;
value: Date;
onChange: (day: Date) => void;
closePanelOnDaySelect?: boolean;
onAfterSelect?: (day: Date) => void;
};
/** Year / month / day dropdown row — shared by schedule picker and date fields. */
export function CalendarDayPartsPanel({
panelId,
value,
onChange,
closePanelOnDaySelect = false,
onAfterSelect,
}: CalendarDayPartsPanelProps) {
const locale = useLocale();
const t = useTranslations('schedule');
const normalizedValue = startOfLocalDay(value);
const persian = usesPersianCalendar(locale);
const jalaliParts = persian ? getLocalPersianParts(normalizedValue) : null;
const gregorianYear = normalizedValue.getFullYear();
const gregorianMonth = normalizedValue.getMonth();
const gregorianDay = normalizedValue.getDate();
const years =
persian && jalaliParts ? persianYearRange(jalaliParts.year) : yearRange(normalizedValue);
const selectedYear = jalaliParts?.year ?? gregorianYear;
const selectedMonth = jalaliParts?.month ?? gregorianMonth;
const selectedDay = jalaliParts?.day ?? gregorianDay;
const dayCount = persian
? jalaliDaysInMonth(selectedYear, selectedMonth)
: daysInMonth(selectedYear, selectedMonth);
function applyParts(year: number, month: number, day: number, closePanel = false) {
const maxDay = persian ? jalaliDaysInMonth(year, month) : daysInMonth(year, month);
const clampedDay = Math.min(Math.max(1, day), maxDay);
onChange(
persian
? persianPartsToLocalDate(year, month, clampedDay)
: buildLocalDay(year, month, clampedDay),
);
if (closePanel) {
const nextDay = persian
? persianPartsToLocalDate(year, month, clampedDay)
: buildLocalDay(year, month, clampedDay);
onAfterSelect?.(nextDay);
}
}
function formatPanelYear(year: number): string {
return persian ? formatAppInteger(year, locale) : String(year);
}
function formatPanelDay(day: number): string {
return persian ? formatAppInteger(day, locale) : String(day);
}
return (
<div className="grid min-w-[17rem] grid-cols-[minmax(4.25rem,1fr)_minmax(6.75rem,1.35fr)_minmax(3.25rem,0.85fr)] gap-1.5">
<div className="min-w-0">
<label htmlFor={`${panelId}-year`} className="mb-1 block text-xs font-medium text-text-muted">
{t('year')}
</label>
<CompactSelect
id={`${panelId}-year`}
value={selectedYear}
className={COMPACT_SELECT_CLASS}
onChange={(e) => applyParts(Number(e.target.value), selectedMonth, selectedDay)}
>
{years.map((year) => (
<option key={year} value={year}>
{formatPanelYear(year)}
</option>
))}
</CompactSelect>
</div>
<div className="min-w-0">
<label htmlFor={`${panelId}-month`} className="mb-1 block text-xs font-medium text-text-muted">
{t('month')}
</label>
<CompactSelect
id={`${panelId}-month`}
value={selectedMonth}
className={COMPACT_SELECT_CLASS}
onChange={(e) => applyParts(selectedYear, Number(e.target.value), selectedDay)}
>
{persian
? Array.from({ length: 12 }, (_, i) => i + 1).map((month) => (
<option key={month} value={month}>
{formatPersianMonthLabel(selectedYear, month)}
</option>
))
: MONTH_KEYS.map((key, index) => (
<option key={key} value={index}>
{t(key)}
</option>
))}
</CompactSelect>
</div>
<div className="min-w-0">
<label htmlFor={`${panelId}-day`} className="mb-1 block text-xs font-medium text-text-muted">
{t('day')}
</label>
<CompactSelect
id={`${panelId}-day`}
value={selectedDay}
className={COMPACT_SELECT_CLASS}
onChange={(e) =>
applyParts(selectedYear, selectedMonth, Number(e.target.value), closePanelOnDaySelect)
}
>
{Array.from({ length: dayCount }, (_, i) => i + 1).map((day) => (
<option key={day} value={day}>
{formatPanelDay(day)}
</option>
))}
</CompactSelect>
</div>
</div>
);
}