'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 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(null); const selectedDate = value ? parseGregorian(value) : null; // Controlled month so the popover opens on the selected month (or now). const [viewMonth, setViewMonth] = useState(() => 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 (
{label && ( )} {open && (
)}
); }