improvement: rtl direction, solar calendar and persian formatting added for persian users.
This commit is contained in:
166
frontend/src/components/ui/shared/CalendarDayPartsPanel.tsx
Normal file
166
frontend/src/components/ui/shared/CalendarDayPartsPanel.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
'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;
|
||||
|
||||
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 grid-cols-3 gap-2">
|
||||
<div>
|
||||
<label htmlFor={`${panelId}-year`} className="mb-1 block text-xs font-medium text-text-muted">
|
||||
{t('year')}
|
||||
</label>
|
||||
<CompactSelect
|
||||
id={`${panelId}-year`}
|
||||
value={selectedYear}
|
||||
onChange={(e) => applyParts(Number(e.target.value), selectedMonth, selectedDay)}
|
||||
>
|
||||
{years.map((year) => (
|
||||
<option key={year} value={year}>
|
||||
{formatPanelYear(year)}
|
||||
</option>
|
||||
))}
|
||||
</CompactSelect>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor={`${panelId}-month`} className="mb-1 block text-xs font-medium text-text-muted">
|
||||
{t('month')}
|
||||
</label>
|
||||
<CompactSelect
|
||||
id={`${panelId}-month`}
|
||||
value={selectedMonth}
|
||||
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>
|
||||
<label htmlFor={`${panelId}-day`} className="mb-1 block text-xs font-medium text-text-muted">
|
||||
{t('day')}
|
||||
</label>
|
||||
<CompactSelect
|
||||
id={`${panelId}-day`}
|
||||
value={selectedDay}
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user