"use client"; import { useEffect, useId, useMemo, useRef, useState } from "react"; import { CalendarDays, ChevronLeft, ChevronRight } from "lucide-react"; import { cn } from "@/lib/utils"; import { MONTHS, WEEKDAYS, formatJalali, formatJalaliDateTime, isoToJalaliParts, jalaliMonthLength, jalaliPartsToIso, jalaliWeekdaySat0, shiftJalaliMonth, todayIso, } from "@/lib/jalali"; type DatePickerProps = { value?: string; onChange?: (iso: string) => void; onBlur?: () => void; name?: string; id?: string; disabled?: boolean; placeholder?: string; className?: string; /** Gregorian ISO min/max (optional) */ min?: string; max?: string; }; /** * Shamsi (Jalali) date picker — Design System. * `value` / `onChange` use Gregorian ISO `YYYY-MM-DD` for backend APIs. * Calendar UI never opens the native Gregorian picker. */ export function DatePicker({ value, onChange, onBlur, name, id, disabled, placeholder = "انتخاب تاریخ", className, min, max, }: DatePickerProps) { const autoId = useId(); const inputId = id || autoId; const rootRef = useRef(null); const [open, setOpen] = useState(false); const selected = value ? isoToJalaliParts(value) : null; const initial = selected || isoToJalaliParts(todayIso()); const [view, setView] = useState({ year: initial.year, month: initial.month }); useEffect(() => { if (!open) return; const parts = value ? isoToJalaliParts(value) : isoToJalaliParts(todayIso()); setView({ year: parts.year, month: parts.month }); }, [open, value]); useEffect(() => { if (!open) return; const onDoc = (e: MouseEvent) => { if (!rootRef.current?.contains(e.target as Node)) { setOpen(false); onBlur?.(); } }; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") { setOpen(false); onBlur?.(); } }; document.addEventListener("mousedown", onDoc); document.addEventListener("keydown", onKey); return () => { document.removeEventListener("mousedown", onDoc); document.removeEventListener("keydown", onKey); }; }, [open, onBlur]); const cells = useMemo(() => { const len = jalaliMonthLength(view.year, view.month); const start = jalaliWeekdaySat0(view.year, view.month, 1); const items: ({ day: number; iso: string } | null)[] = []; for (let i = 0; i < start; i++) items.push(null); for (let day = 1; day <= len; day++) { const iso = jalaliPartsToIso({ year: view.year, month: view.month, day }); items.push({ day, iso }); } return items; }, [view.year, view.month]); const minIso = min || undefined; const maxIso = max || undefined; const today = todayIso(); const pick = (iso: string) => { if (minIso && iso < minIso) return; if (maxIso && iso > maxIso) return; onChange?.(iso); setOpen(false); onBlur?.(); }; const display = value ? formatJalali(value) : ""; return (
{open ? (
{MONTHS[view.month - 1]} {view.year}
{WEEKDAYS.map((w) => (
{w}
))}
{cells.map((cell, idx) => { if (!cell) return
; const isSelected = value === cell.iso; const isToday = cell.iso === today; const disabledDay = (minIso ? cell.iso < minIso : false) || (maxIso ? cell.iso > maxIso : false); return ( ); })}
) : null}
); } /** Display-only Jalali date from Gregorian ISO. */ export function JalaliDateText({ value, className, withTime, }: { value?: string | Date | null; className?: string; withTime?: boolean; }) { return ( {withTime ? formatJalaliDateTime(value) : formatJalali(value)} ); }