first commit
This commit is contained in:
292
src/components/date-picker/DatePicker.tsx
Normal file
292
src/components/date-picker/DatePicker.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user