first commit

This commit is contained in:
2026-01-19 13:08:58 +03:30
commit 850b4a3f1e
293 changed files with 51775 additions and 0 deletions

View File

@@ -0,0 +1,292 @@
import React, { useEffect, useRef, useState } from "react";
import moment from "jalali-moment";
import {
bgInputPrimaryColor,
mobileBorders,
textColor,
} from "../../data/getColorBasedOnMode";
import { getSizeStyles } from "../../data/getInputSizes";
import { checkIsMobile } from "../../utils/checkIsMobile";
interface DatePickerProps {
value?: string;
placeholderColor?: string;
onChange: (value: string) => void;
label?: string;
disabled?: boolean;
isTable?: boolean;
fullWidth?: boolean;
className?: string;
minYear?: number;
maxYear?: number;
size?: "small" | "medium" | "large";
}
const persianMonths = [
"فروردین",
"اردیبهشت",
"خرداد",
"تیر",
"مرداد",
"شهریور",
"مهر",
"آبان",
"آذر",
"دی",
"بهمن",
"اسفند",
];
const persianWeekDays = ["ش", "ی", "د", "س", "چ", "پ", "ج"];
const DatePicker: React.FC<DatePickerProps> = ({
onChange,
label = "تاریخ",
disabled = false,
isTable = false,
className = "",
value = "",
placeholderColor = "",
minYear = 1390,
maxYear = 1410,
size = "medium",
}) => {
const pickerRef = useRef<HTMLDivElement | null>(null);
const [isOpen, setIsOpen] = useState(false);
const today = moment().locale("fa");
const [selectedYear, setSelectedYear] = useState<number>(
parseInt(today.format("jYYYY"))
);
const [selectedMonthIndex, setSelectedMonthIndex] = useState<number>(
parseInt(today.format("jM")) - 1
);
const [selectedDay, setSelectedDay] = useState<number>(
parseInt(today.format("jD"))
);
const dayRef = useRef<HTMLDivElement>(null);
const monthRef = useRef<HTMLDivElement>(null);
const yearRef = useRef<HTMLDivElement>(null);
const daysInMonth = moment(
`${selectedYear}/${selectedMonthIndex + 1}/1`,
"jYYYY/jM/jD"
).jDaysInMonth();
useEffect(() => {
if (value) {
const jDate = moment(value, "YYYY-MM-DD").locale("fa");
const jYear = parseInt(jDate.format("jYYYY"));
const jMonth = parseInt(jDate.format("jM")) - 1;
const jDay = parseInt(jDate.format("jD"));
setSelectedYear(jYear);
setSelectedMonthIndex(jMonth);
setSelectedDay(jDay);
}
}, [value]);
useEffect(() => {
const daysInNewMonth = moment(
`${selectedYear}/${selectedMonthIndex + 1}/1`,
"jYYYY/jM/jD"
).jDaysInMonth();
if (selectedDay > daysInNewMonth) {
setSelectedDay(daysInNewMonth);
}
}, [selectedYear, selectedMonthIndex]);
useEffect(() => {
if (isOpen) {
const scrollToSelected = (
ref: React.RefObject<HTMLDivElement>,
selector: string
) => {
const el = ref.current?.querySelector(selector);
el?.scrollIntoView({ behavior: "auto", block: "nearest" });
};
scrollToSelected(
dayRef as React.RefObject<HTMLDivElement>,
".selected-day"
);
scrollToSelected(
monthRef as React.RefObject<HTMLDivElement>,
".selected-month"
);
scrollToSelected(
yearRef as React.RefObject<HTMLDivElement>,
".selected-year"
);
}
}, [isOpen]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
pickerRef.current &&
!pickerRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
}, []);
useEffect(() => {
const jDate = moment(
`${selectedYear}/${selectedMonthIndex + 1}/${selectedDay}`,
"jYYYY/jM/jD"
);
const gDate = jDate.locale("en").format("YYYY-MM-DD");
if (value !== gDate) {
onChange(gDate);
}
}, [selectedYear, selectedMonthIndex, selectedDay, value]);
const getDayOfWeek = (day: number) => {
const date = moment(
`${selectedYear}/${selectedMonthIndex + 1}/${day}`,
"jYYYY/jM/jD"
);
return persianWeekDays[date.day()];
};
return (
<div className={`relative w-full ${className}`} ref={pickerRef}>
<div className="flex items-center gap-2 ">
<label
onClick={() => setIsOpen(!isOpen)}
className={`text-sm text-nowrap select-none ${
disabled ? "text-gray-400" : textColor
}`}
>
{label}
</label>
<input
readOnly
value={`${selectedDay} / ${persianMonths[selectedMonthIndex]} / ${selectedYear} `}
onClick={() => setIsOpen(!isOpen)}
className={`${
isTable ? "w-full md:w-30 text-xs" : "w-full min-w-40 text-sm"
} border-1 focus:ring-dark-400 rounded-lg text-center text-dark-800 dark:text-gray-100 ${
disabled && "opacity-80"
} ${disabled ? "text-gray-400" : textColor} ${
placeholderColor
? placeholderColor
: checkIsMobile()
? "bg-gray-50 border-gray-300"
: "bg-white border-black-100"
} ${checkIsMobile() && mobileBorders} ${
getSizeStyles(size).padding
} ${bgInputPrimaryColor} ${disabled ? "text-gray-400" : textColor}`}
disabled={disabled}
/>
</div>
{isOpen && (
<div
className={`absolute z-50 mt-2 flex rounded-lg shadow-lg text-white text-sm w-56 bg-white dark:bg-dark-500 `}
>
{[
{
label: "روز",
options: Array.from({ length: daysInMonth }, (_, i) => i + 1),
selected: selectedDay,
setSelected: setSelectedDay,
ref: dayRef,
selectedClass: "selected-day",
},
{
label: "ماه",
options: persianMonths,
selected: selectedMonthIndex,
setSelected: (v: number) => setSelectedMonthIndex(v),
ref: monthRef,
selectedClass: "selected-month",
},
{
label: "سال",
options: Array.from(
{ length: maxYear - minYear + 1 },
(_, i) => minYear + i
),
selected: selectedYear,
setSelected: setSelectedYear,
ref: yearRef,
selectedClass: "selected-year",
},
].map(
(
{ label, options, selected, setSelected, ref, selectedClass },
colIndex
) => (
<div
key={label}
ref={ref}
className="flex-1 h-48 overflow-y-auto snap-y snap-mandatory text-center space-y-1 scrollbar-hide relative"
>
<ul className="relative z-20">
<li className="h-6" aria-hidden />
{options.map((opt, idx) => {
const isString = typeof opt === "string";
const isSelected =
(colIndex === 0 && opt === selected) ||
(colIndex === 1 && idx === selected) ||
(colIndex === 2 && opt === selected);
const itemClass = isSelected ? selectedClass : "";
return (
<li
key={isString ? opt : String(opt)}
className={`py-2 cursor-pointer transition ${itemClass} ${
isSelected
? "text-amber-900 dark:text-amber-200 font-bold scale-105"
: textColor
} ${
(opt === parseInt(today.format("jD")) ||
opt ===
persianMonths[parseInt(today.format("jM")) - 1] ||
opt === parseInt(today.format("jYYYY"))) &&
" decoration-cyan-200 rounded-4xl"
}`}
onClick={() =>
setSelected(colIndex === 1 ? idx : (opt as number))
}
>
{colIndex === 0 ? (
<span className="flex items-center justify-between mx-5">
<span className={`text-[10px]`}>
{getDayOfWeek((opt as number) + 1)}
</span>
<span
className={`${
opt === parseInt(today.format("jD")) &&
" underline underline-offset-4 decoration-cyan-200 rounded-4xl"
}`}
>
{opt}{" "}
</span>
</span>
) : (
opt
)}
</li>
);
})}
<li className="h-6" aria-hidden />
</ul>
</div>
)
)}
</div>
)}
</div>
);
};
export default DatePicker;