Files

155 lines
5.8 KiB
TypeScript

'use client';
import { useEffect, useRef, useState } from 'react';
import { DayPicker } from '@daypicker/persian';
import '@daypicker/react/style.css';
import { CalendarDaysIcon, XMarkIcon } from '@heroicons/react/24/outline';
interface PersianDatePickerProps {
/** Gregorian YYYY-MM-DD, or '' when empty. */
value: string;
/** Called with the selected Gregorian YYYY-MM-DD, or '' when cleared. */
onChange: (value: string) => void;
label?: string;
placeholder?: string;
id?: string;
}
// Parses a Gregorian "YYYY-MM-DD" into a local Date (pinned at noon to avoid
// any date shift from timezone offsets).
const parseGregorian = (value: string): Date | null => {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
if (!match) return null;
const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]), 12, 0, 0);
return Number.isNaN(date.getTime()) ? null : date;
};
const toGregorianValue = (date: Date): string => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
// Renders the given Gregorian date as a Persian (Jalali) string with Persian
// digits, e.g. "۱۴۰۴/۰۵/۱۱".
const jalaliFormat = new Intl.DateTimeFormat('fa-IR-u-ca-persian', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
});
const formatJalali = (date: Date): string => jalaliFormat.format(date);
/**
* A Persian (Jalali) date input built on @daypicker/persian.
*
* The calendar operates in the Jalali calendar, but the public value is a
* Gregorian "YYYY-MM-DD" string — the same format the native <input
* type="date"> produced — so the accounting filters keep working unchanged.
*
* Note: with the default dateLib (no `timeZone` prop), @daypicker/persian
* works with plain JS `Date` objects whose native getters already return the
* Gregorian date; the Jalali conversion happens inside the calendar library.
* That's why we can read `getFullYear()/getMonth()/getDate()` straight off the
* picked date.
*/
export default function PersianDatePicker({
value,
onChange,
label,
placeholder,
id,
}: PersianDatePickerProps) {
const [open, setOpen] = useState(false);
const rootRef = useRef<HTMLDivElement>(null);
const selectedDate = value ? parseGregorian(value) : null;
// Controlled month so the popover opens on the selected month (or now).
const [viewMonth, setViewMonth] = useState<Date>(() => selectedDate ?? new Date());
// Close on outside click or Escape.
useEffect(() => {
if (!open) return;
const handlePointerDown = (event: MouseEvent) => {
if (rootRef.current && !rootRef.current.contains(event.target as Node)) {
setOpen(false);
}
};
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') setOpen(false);
};
document.addEventListener('mousedown', handlePointerDown);
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('mousedown', handlePointerDown);
document.removeEventListener('keydown', handleKeyDown);
};
}, [open]);
const handleSelect = (date: Date | undefined) => {
if (date) onChange(toGregorianValue(date));
setOpen(false);
};
const handleClear = () => {
onChange('');
setOpen(false);
};
const toggle = () => {
setViewMonth(selectedDate ?? new Date());
setOpen((prev) => !prev);
};
return (
<div ref={rootRef} className="relative">
{label && (
<label htmlFor={id} className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
{label}
</label>
)}
<button
type="button"
id={id}
onClick={toggle}
aria-haspopup="dialog"
aria-expanded={open}
className={`flex w-full items-center justify-between gap-2 px-3 py-2.5 text-right text-gray-900 dark:text-gray-100 border border-gray-200 dark:border-gray-700 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent sm:text-sm dark:bg-gray-800/80 transition-colors ${
selectedDate
? 'text-gray-900 dark:text-gray-100'
: 'text-gray-400 dark:text-gray-500'
}`}
>
<span className="truncate">{selectedDate ? formatJalali(selectedDate) : placeholder || 'انتخاب تاریخ'}</span>
{selectedDate ? (
<XMarkIcon
className="h-4 w-4 shrink-0 text-gray-400 hover:text-red-500 transition-colors"
onClick={(event) => {
event.stopPropagation();
handleClear();
}}
aria-label="پاک کردن تاریخ"
/>
) : (
<CalendarDaysIcon className="h-4 w-4 shrink-0 text-gray-400" />
)}
</button>
{open && (
<div className="absolute right-0 z-50 mt-2 bg-white dark:bg-gray-900 ring-1 ring-gray-200 dark:ring-gray-800 rounded-2xl shadow-xl p-2 max-w-[calc(100vw-3rem)] overflow-x-auto">
<DayPicker
mode="single"
selected={selectedDate ?? undefined}
onSelect={handleSelect}
month={viewMonth}
onMonthChange={setViewMonth}
/>
</div>
)}
</div>
);
}