167 lines
5.5 KiB
TypeScript
167 lines
5.5 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useMemo, useState } from "react";
|
||
import DatePicker from "react-multi-date-picker";
|
||
import DateObject from "react-date-object";
|
||
import persian from "react-date-object/calendars/persian";
|
||
import gregorian from "react-date-object/calendars/gregorian";
|
||
import persian_fa from "react-date-object/locales/persian_fa";
|
||
import gregorian_en from "react-date-object/locales/gregorian_en";
|
||
import {
|
||
dateValueToJalali,
|
||
formatPersianDateTime,
|
||
getDateFromJalaliDateTime,
|
||
jalaliDateTimeToUtcIso,
|
||
jalaliDateToGregorianString,
|
||
type JalaliDateParts,
|
||
} from "@/lib/persianDate";
|
||
|
||
type PersianDateFieldProps = {
|
||
label: string;
|
||
value: string;
|
||
onChange: (value: string) => void;
|
||
mode?: "date" | "datetime";
|
||
required?: boolean;
|
||
placeholder?: string;
|
||
};
|
||
|
||
function createDateObject(parts: JalaliDateParts | null) {
|
||
if (!parts) return null;
|
||
|
||
return new DateObject({
|
||
calendar: persian,
|
||
locale: persian_fa,
|
||
year: parts.year,
|
||
month: parts.month,
|
||
day: parts.day,
|
||
});
|
||
}
|
||
|
||
function getInitialTime(value: string, mode: "date" | "datetime") {
|
||
const parsedValue = dateValueToJalali(value, mode);
|
||
return parsedValue?.time || "12:00";
|
||
}
|
||
|
||
export default function PersianDateField({
|
||
label,
|
||
value,
|
||
onChange,
|
||
mode = "date",
|
||
required = false,
|
||
placeholder = "انتخاب تاریخ شمسی",
|
||
}: PersianDateFieldProps) {
|
||
const parsedValue = useMemo(() => dateValueToJalali(value, mode), [mode, value]);
|
||
const pickerValue = useMemo(() => createDateObject(parsedValue), [parsedValue]);
|
||
const [time, setTime] = useState(getInitialTime(value, mode));
|
||
|
||
useEffect(() => {
|
||
setTime(getInitialTime(value, mode));
|
||
}, [mode, value]);
|
||
|
||
function emitChange(nextDate: DateObject | null, nextTime: string) {
|
||
if (!nextDate) {
|
||
onChange("");
|
||
return;
|
||
}
|
||
|
||
const year = Number(nextDate.year);
|
||
const month = Number(nextDate.month.number);
|
||
const day = Number(nextDate.day);
|
||
|
||
if (mode === "date") {
|
||
onChange(jalaliDateToGregorianString(year, month, day));
|
||
return;
|
||
}
|
||
|
||
onChange(jalaliDateTimeToUtcIso(year, month, day, nextTime));
|
||
}
|
||
|
||
const preview = useMemo(() => {
|
||
if (!parsedValue) return "";
|
||
if (mode === "date") return "";
|
||
|
||
return formatPersianDateTime(
|
||
getDateFromJalaliDateTime(parsedValue.year, parsedValue.month, parsedValue.day, time)
|
||
);
|
||
}, [mode, parsedValue, time]);
|
||
|
||
return (
|
||
<div className="space-y-2">
|
||
<label className="block text-sm font-medium text-slate-700">{label}</label>
|
||
|
||
<DatePicker
|
||
value={pickerValue}
|
||
onChange={(selected) => emitChange((selected as DateObject | null) ?? null, time)}
|
||
calendar={persian}
|
||
locale={persian_fa}
|
||
format="YYYY/MM/DD"
|
||
calendarPosition="bottom-right"
|
||
editable={false}
|
||
required={required}
|
||
placeholder={placeholder}
|
||
inputClass="w-full rounded-2xl border border-slate-200 bg-white px-4 py-3 text-right text-slate-900 shadow-sm outline-none transition focus:border-emerald-400 focus:ring-2 focus:ring-emerald-200"
|
||
containerClassName="block w-full"
|
||
weekDays={[
|
||
["شنبه", "ش"],
|
||
["یکشنبه", "ی"],
|
||
["دوشنبه", "د"],
|
||
["سهشنبه", "س"],
|
||
["چهارشنبه", "چ"],
|
||
["پنجشنبه", "پ"],
|
||
["جمعه", "ج"],
|
||
]}
|
||
/>
|
||
|
||
{pickerValue && (
|
||
<div className="rounded-xl bg-slate-50 px-3 py-2 text-xs text-slate-500">
|
||
{mode === "date"
|
||
? `تاریخ انتخابشده: ${pickerValue.format("dddd DD MMMM YYYY")}`
|
||
: preview}
|
||
</div>
|
||
)}
|
||
|
||
{mode === "datetime" && (
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div>
|
||
<label className="mb-1 block text-xs font-medium text-slate-600">ساعت</label>
|
||
<select
|
||
value={time.split(":")[0]}
|
||
onChange={(event) => {
|
||
const nextTime = `${event.target.value}:${time.split(":")[1]}`;
|
||
setTime(nextTime);
|
||
emitChange(pickerValue, nextTime);
|
||
}}
|
||
className="w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-slate-900 outline-none transition focus:border-emerald-400 focus:ring-2 focus:ring-emerald-200"
|
||
>
|
||
{Array.from({ length: 24 }, (_, index) => String(index).padStart(2, "0")).map((hour) => (
|
||
<option key={hour} value={hour}>
|
||
{new Intl.NumberFormat("fa-IR").format(Number(hour))}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="mb-1 block text-xs font-medium text-slate-600">دقیقه</label>
|
||
<select
|
||
value={time.split(":")[1]}
|
||
onChange={(event) => {
|
||
const nextTime = `${time.split(":")[0]}:${event.target.value}`;
|
||
setTime(nextTime);
|
||
emitChange(pickerValue, nextTime);
|
||
}}
|
||
className="w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-slate-900 outline-none transition focus:border-emerald-400 focus:ring-2 focus:ring-emerald-200"
|
||
>
|
||
{Array.from({ length: 60 }, (_, index) => String(index).padStart(2, "0")).map((minute) => (
|
||
<option key={minute} value={minute}>
|
||
{new Intl.NumberFormat("fa-IR").format(Number(minute))}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|