import React, { useEffect, useRef, useState } from 'react'; import { ChevronRightIcon, ChevronLeftIcon } from '@heroicons/react/24/outline'; interface Props { value: string; // YYYY-MM-DD Gregorian onChange: (v: string) => void; onClose: () => void; /** انتخاب سال→ماه→روز را فعال می‌کند (پیش‌فرض خاموش — مناسب تاریخ تولد). */ enableYearPicker?: boolean; } const WEEK_DAYS = ['ش', 'ی', 'د', 'س', 'چ', 'پ', 'ج']; const pf = new Intl.DateTimeFormat('fa-IR-u-ca-persian', { calendar: 'persian' }); function toJalali(d: Date): { year: number; month: number; day: number } { const parts = new Intl.DateTimeFormat('en-u-ca-persian', { year: 'numeric', month: 'numeric', day: 'numeric', }).formatToParts(d); const get = (t: string) => parseInt(parts.find(p => p.type === t)?.value ?? '0', 10); return { year: get('year'), month: get('month'), day: get('day') }; } function jalaliFirstWeekday(year: number, month: number): number { // Build a Gregorian date for the 1st of this Jalali month by iterating // Use the Intl API: find Gregorian date whose Jalali = year/month/1 // Approximate: use known offset const approxGreg = jalaliToGregorian(year, month, 1); const d = new Date(approxGreg + 'T12:00:00'); // JS getDay(): 0=Sun,1=Mon,...,6=Sat → convert to Sat=0 return (d.getDay() + 1) % 7; // Sat=0, Sun=1, ..., Fri=6 } function jalaliToGregorian(jy: number, jm: number, jd: number): string { const jy2 = jy - 979; const jm2 = jm - 1; let jDay = 365 * jy2 + Math.floor(jy2 / 33) * 8 + Math.floor(((jy2 % 33) + 3) / 4); for (let i = 0; i < jm2; i++) jDay += (i < 6 ? 31 : 30); jDay += jd - 1; let gDay = jDay + 79; let gy2 = 1600 + 400 * Math.floor(gDay / 146097); gDay %= 146097; let leap = true; if (gDay >= 36525) { gDay--; const gi = Math.floor(gDay / 36524); gDay %= 36524; gy2 += gi * 100; if (gDay >= 365) { gDay++; leap = false; } } gy2 += Math.floor(gDay / 1461) * 4; gDay %= 1461; if (gDay >= 366) { leap = false; gDay--; gy2 += Math.floor(gDay / 365); gDay %= 365; } const gMonthDays = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; let gm = 0; for (; gm < 12; gm++) { if (gDay < gMonthDays[gm]) break; gDay -= gMonthDays[gm]; } return `${gy2}-${String(gm + 1).padStart(2, '0')}-${String(gDay + 1).padStart(2, '0')}`; } const JALALI_MONTHS = ['فروردین','اردیبهشت','خرداد','تیر','مرداد','شهریور','مهر','آبان','آذر','دی','بهمن','اسفند']; export default function PersianCalendar({ value, onChange, onClose, enableYearPicker = false }: Props) { const ref = useRef(null); const todayGreg = new Date().toISOString().slice(0, 10); const todayJ = toJalali(new Date(todayGreg + 'T12:00:00')); const valueJ = value ? toJalali(new Date(value + 'T12:00:00')) : todayJ; const [viewYear, setViewYear] = useState(valueJ.year); const [viewMonth, setViewMonth] = useState(valueJ.month); const [mode, setMode] = useState<'day' | 'month' | 'year'>('day'); // ابتدای بازه‌ی ۱۲تایی سال در نمای انتخاب سال const [yearRangeStart, setYearRangeStart] = useState(valueJ.year - (valueJ.year % 12)); const faYear = (y: number) => y.toLocaleString('fa-IR', { useGrouping: false }); useEffect(() => { function handler(e: MouseEvent) { if (ref.current && !ref.current.contains(e.target as Node)) onClose(); } document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, [onClose]); const daysCount = viewMonth <= 6 ? 31 : viewMonth <= 11 ? 30 : 29; const firstWd = jalaliFirstWeekday(viewYear, viewMonth); function prevMonth() { if (viewMonth === 1) { setViewYear(y => y - 1); setViewMonth(12); } else setViewMonth(m => m - 1); } function nextMonth() { if (viewMonth === 12) { setViewYear(y => y + 1); setViewMonth(1); } else setViewMonth(m => m + 1); } function selectDay(day: number) { const greg = jalaliToGregorian(viewYear, viewMonth, day); onChange(greg); onClose(); } const isToday = (day: number) => todayJ.year === viewYear && todayJ.month === viewMonth && todayJ.day === day; const isSelected = (day: number) => value !== '' && valueJ.year === viewYear && valueJ.month === viewMonth && valueJ.day === day; const cells: (number | null)[] = [...Array(firstWd).fill(null), ...Array.from({ length: daysCount }, (_, i) => i + 1)]; while (cells.length % 7 !== 0) cells.push(null); return (
{/* Header */}
{mode === 'day' && ( setMode('month') : undefined} style={{ cursor: enableYearPicker ? 'pointer' : 'default', color: enableYearPicker ? 'var(--primary)' : 'inherit' }} > {JALALI_MONTHS[viewMonth - 1]} { setYearRangeStart(viewYear - (viewYear % 12)); setMode('year'); } : undefined} style={{ cursor: enableYearPicker ? 'pointer' : 'default', color: enableYearPicker ? 'var(--primary)' : 'inherit' }} > {faYear(viewYear)} )} {mode === 'month' && انتخاب ماه {faYear(viewYear)}} {mode === 'year' && ( {faYear(yearRangeStart)} - {faYear(yearRangeStart + 11)} )}
{/* Day view */} {mode === 'day' && ( <>
{WEEK_DAYS.map(d => (
{d}
))}
{cells.map((day, i) => { if (!day) return
; const sel = isSelected(day); const tod = isToday(day) && !sel; return ( ); })}
)} {/* Month view */} {mode === 'month' && (
{JALALI_MONTHS.map((mName, idx) => { const m = idx + 1; const active = m === viewMonth; return ( ); })}
)} {/* Year view */} {mode === 'year' && (
{Array.from({ length: 12 }, (_, i) => yearRangeStart + i).map(y => { const active = y === viewYear; return ( ); })}
)}
); } const navBtnStyle: React.CSSProperties = { background: 'transparent', border: 'none', cursor: 'pointer', color: 'var(--text-2)', padding: 4, borderRadius: 'var(--r-sm)', display: 'flex', alignItems: 'center', }; const gridItemStyle: React.CSSProperties = { height: 40, borderRadius: 'var(--r-sm)', fontSize: 13, fontWeight: 500, border: 'none', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'background 0.1s', };