Replace jalaali-react-date-picker with custom MUI-based Jalali date picker - fixes all 10 security vulnerabilities
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { TextField, Popover, IconButton, Box } from "@mui/material";
|
||||
import moment from "moment-jalaali";
|
||||
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
|
||||
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
||||
|
||||
moment.loadPersian({ dialect: "persian-modern", usePersianDigits: false });
|
||||
|
||||
const WEEKDAYS = ["ش", "ی", "د", "س", "چ", "پ", "ج"];
|
||||
const MONTHS = [
|
||||
"فروردین",
|
||||
"اردیبهشت",
|
||||
"خرداد",
|
||||
"تیر",
|
||||
"مرداد",
|
||||
"شهریور",
|
||||
"مهر",
|
||||
"آبان",
|
||||
"آذر",
|
||||
"دی",
|
||||
"بهمن",
|
||||
"اسفند",
|
||||
];
|
||||
|
||||
function JalaliDatePicker({
|
||||
value,
|
||||
onChange,
|
||||
label,
|
||||
placeholder = "انتخاب تاریخ",
|
||||
error = false,
|
||||
helperText = "",
|
||||
disabled = false,
|
||||
className = "",
|
||||
textFieldProps = {},
|
||||
disabledDates = null,
|
||||
nextIcon = null,
|
||||
prevIcon = null,
|
||||
dayLabelRender = null,
|
||||
isFirst = false,
|
||||
}) {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [selectedDate, setSelectedDate] = useState(
|
||||
value ? moment(value) : moment()
|
||||
);
|
||||
const [displayDate, setDisplayDate] = useState(
|
||||
value ? moment(value) : moment()
|
||||
);
|
||||
const nextIconRef = useRef(null);
|
||||
const prevIconRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
const newDate = moment(value);
|
||||
setSelectedDate(newDate);
|
||||
setDisplayDate(newDate);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const handleClick = (event) => {
|
||||
if (!disabled) {
|
||||
setAnchorEl(event.currentTarget);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const handleDateClick = (day) => {
|
||||
const newDate = moment(displayDate)
|
||||
.jDate(day)
|
||||
.jMonth(displayDate.jMonth())
|
||||
.jYear(displayDate.jYear());
|
||||
|
||||
if (disabledDates && disabledDates(newDate)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedDate(newDate);
|
||||
if (onChange) {
|
||||
onChange({ _d: newDate.toDate() });
|
||||
}
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handlePrevMonth = () => {
|
||||
setDisplayDate(moment(displayDate).subtract(1, "jMonth"));
|
||||
};
|
||||
|
||||
const handleNextMonth = () => {
|
||||
setDisplayDate(moment(displayDate).add(1, "jMonth"));
|
||||
};
|
||||
|
||||
const getDaysInMonth = () => {
|
||||
const year = displayDate.jYear();
|
||||
const month = displayDate.jMonth();
|
||||
const daysInMonth = moment.jDaysInMonth(year, month);
|
||||
const firstDayOfMonth = moment([year, month, 1], "jYYYY/jMM/jDD").day();
|
||||
|
||||
const days = [];
|
||||
const startOffset = (firstDayOfMonth + 1) % 7;
|
||||
|
||||
for (let i = 0; i < startOffset; i++) {
|
||||
days.push(null);
|
||||
}
|
||||
|
||||
for (let day = 1; day <= daysInMonth; day++) {
|
||||
days.push(day);
|
||||
}
|
||||
|
||||
return days;
|
||||
};
|
||||
|
||||
const formatDisplayValue = () => {
|
||||
if (!selectedDate) return "";
|
||||
return selectedDate.format("jYYYY/jMM/jDD");
|
||||
};
|
||||
|
||||
const days = getDaysInMonth();
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<TextField
|
||||
fullWidth
|
||||
value={formatDisplayValue()}
|
||||
onClick={handleClick}
|
||||
placeholder={placeholder}
|
||||
label={label}
|
||||
error={error}
|
||||
helperText={helperText}
|
||||
disabled={disabled}
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
}}
|
||||
{...textFieldProps}
|
||||
/>
|
||||
<Popover
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
sx={{
|
||||
"& .MuiPaper-root": {
|
||||
borderRadius: "8px",
|
||||
boxShadow: "0px 4px 20px rgba(0, 0, 0, 0.1)",
|
||||
overflow: "hidden",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: 320,
|
||||
p: 2,
|
||||
bgcolor: "background.paper",
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
{nextIcon ? (
|
||||
<Box
|
||||
ref={nextIconRef}
|
||||
onClick={handleNextMonth}
|
||||
sx={{ cursor: "pointer" }}
|
||||
>
|
||||
{nextIcon}
|
||||
</Box>
|
||||
) : (
|
||||
<IconButton onClick={handleNextMonth} size="small">
|
||||
<ChevronLeftIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
<Box sx={{ fontWeight: 600, fontSize: "14px" }}>
|
||||
{MONTHS[displayDate.jMonth()]} {displayDate.jYear()}
|
||||
</Box>
|
||||
|
||||
{prevIcon ? (
|
||||
<Box
|
||||
ref={prevIconRef}
|
||||
onClick={handlePrevMonth}
|
||||
sx={{ cursor: "pointer" }}
|
||||
>
|
||||
{prevIcon}
|
||||
</Box>
|
||||
) : (
|
||||
<IconButton onClick={handlePrevMonth} size="small">
|
||||
<ChevronRightIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Weekday labels */}
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(7, 1fr)",
|
||||
gap: 0.5,
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
{dayLabelRender ? (
|
||||
dayLabelRender()
|
||||
) : (
|
||||
<>
|
||||
{WEEKDAYS.map((day, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
textAlign: "center",
|
||||
fontSize: "12px",
|
||||
fontWeight: 600,
|
||||
color: "text.secondary",
|
||||
py: 0.5,
|
||||
}}
|
||||
>
|
||||
{day}
|
||||
</Box>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Days grid */}
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(7, 1fr)",
|
||||
gap: 0.5,
|
||||
}}
|
||||
>
|
||||
{days.map((day, index) => {
|
||||
if (!day) {
|
||||
return <Box key={index} />;
|
||||
}
|
||||
|
||||
const dayDate = moment(displayDate)
|
||||
.jDate(day)
|
||||
.jMonth(displayDate.jMonth())
|
||||
.jYear(displayDate.jYear());
|
||||
|
||||
const isSelected =
|
||||
selectedDate &&
|
||||
dayDate.jYear() === selectedDate.jYear() &&
|
||||
dayDate.jMonth() === selectedDate.jMonth() &&
|
||||
dayDate.jDate() === selectedDate.jDate();
|
||||
|
||||
const isToday =
|
||||
dayDate.jYear() === moment().jYear() &&
|
||||
dayDate.jMonth() === moment().jMonth() &&
|
||||
dayDate.jDate() === moment().jDate();
|
||||
|
||||
const isDisabled = disabledDates && disabledDates(dayDate);
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={index}
|
||||
onClick={() => !isDisabled && handleDateClick(day)}
|
||||
sx={{
|
||||
textAlign: "center",
|
||||
py: 1,
|
||||
cursor: isDisabled ? "not-allowed" : "pointer",
|
||||
borderRadius: "4px",
|
||||
fontSize: "14px",
|
||||
bgcolor: isSelected ? "primary.main" : "transparent",
|
||||
color: isSelected
|
||||
? "primary.contrastText"
|
||||
: isDisabled
|
||||
? "text.disabled"
|
||||
: "text.primary",
|
||||
border: isToday && !isSelected ? "1px solid" : "none",
|
||||
borderColor: "primary.main",
|
||||
opacity: isDisabled ? 0.3 : 1,
|
||||
"&:hover": {
|
||||
bgcolor: isDisabled
|
||||
? "transparent"
|
||||
: isSelected
|
||||
? "primary.dark"
|
||||
: "action.hover",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{day}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default JalaliDatePicker;
|
||||
Reference in New Issue
Block a user