From cf705a894e16e68b2edb880f45e9711eef2b1f93 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Mon, 15 Jun 2026 15:58:23 +0330 Subject: [PATCH] feat(appointment): inline dual-month Jalali calendar for day selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The day picker used a Popover-based JalaliDatePicker that only showed two empty text inputs on the booking page — the calendar was hidden until you clicked the input, so it never matched the design. Replace it with an always-visible inline two-month calendar (InlineJalaliMonth) rendered side by side: current month on the right, next on the left (RTL), weekday headers, Fridays in red, disabled days greyed, selected day in orange, shared month navigation on the outer edges. Preserves the existing contract: setDate(unix timestamp), disabledDates[], auto-select nearest available day. Co-Authored-By: Claude Opus 4.8 --- app/component/date/datePicker/index.js | 154 ++++++++----------------- components/common/InlineJalaliMonth.js | 118 +++++++++++++++++++ 2 files changed, 166 insertions(+), 106 deletions(-) create mode 100644 components/common/InlineJalaliMonth.js diff --git a/app/component/date/datePicker/index.js b/app/component/date/datePicker/index.js index 3d1ad35..dde70e8 100644 --- a/app/component/date/datePicker/index.js +++ b/app/component/date/datePicker/index.js @@ -1,128 +1,70 @@ "use client"; -import { useEffect, useRef, useState } from "react"; -import FirstDatePicker from "./date/FirstDatePicker"; -import SecondDatePicker from "./date/SecondDatePicker"; -import { dateToTimestamp } from "@/helper"; +import { useEffect, useState } from "react"; import moment from "moment-jalaali"; import "moment-timezone"; +import { dateToTimestamp } from "@/helper"; +import InlineJalaliMonth from "@/components/common/InlineJalaliMonth"; function DatePicker({ setDate, disabledDates = [] }) { - const nextIconDatePickerF = useRef(); - const prevIconDatePickerF = useRef(); - const nextIconDatePickerS = useRef(); - const prevIconDatePickerS = useRef(); - - const [startDate, setStartDate] = useState(null); - const [endDate, setEndDate] = useState(); - const [loading, setLoading] = useState(true); + const [baseMonth, setBaseMonth] = useState(moment().tz("Asia/Tehran")); + const [selectedDate, setSelectedDate] = useState(null); const [autoSelected, setAutoSelected] = useState(false); - const handleStartDateChange = (date, e) => { - if (date) { - setDate(dateToTimestamp(date)); - setEndDate(null); - setStartDate(date); - } + const isDisabled = (date) => { + const day = moment(date).startOf("day"); + if (day.isBefore(moment().startOf("day"))) return true; + return disabledDates.some((ts) => day.isSame(moment.unix(ts).startOf("day"))); }; - const handleEndDateChange = (date) => { - if (date) { - setDate(dateToTimestamp(date)); - setEndDate(date); - setStartDate(null); - } - }; - - - const handleDisableDate = (e) => { - const date = moment(e).startOf("day"); - const today = moment().startOf("day"); - - if (date.isBefore(today)) return true; - - if ( - disabledDates.some((ts) => date.isSame(moment.unix(ts).startOf("day"))) - ) { - return true; - } - - return false; + const selectDay = (date) => { + if (isDisabled(date)) return; + setSelectedDate(date); + setDate(dateToTimestamp(date)); }; useEffect(() => { - // تنظیم loading به false بعد از یک تاخیر کوتاه - setTimeout(() => { - setLoading(false); - }, 1000); + if (autoSelected) return; - if (nextIconDatePickerS.current) { - nextIconDatePickerS.current.click(); - } - }, [nextIconDatePickerS, disabledDates]); - - // انتخاب خودکار امروز یا نزدیک‌ترین روز قابل انتخاب - useEffect(() => { - if (!loading && !autoSelected && disabledDates) { - const today = moment().tz("Asia/Tehran"); - const todayTimestamp = moment().tz("Asia/Tehran").startOf("day").unix(); - - // بررسی اینکه امروز قابل انتخاب است یا نه - const isTodayDisabled = disabledDates.some((ts) => - moment.unix(ts).startOf("day").isSame(today.startOf("day")) - ); - - if (!isTodayDisabled) { - // امروز قابل انتخاب است - setStartDate(today); - setDate(todayTimestamp); + const today = moment().tz("Asia/Tehran"); + for (let i = 0; i <= 30; i++) { + const candidate = today.clone().add(i, "days"); + if (!isDisabled(candidate)) { + setSelectedDate(candidate); + setBaseMonth(candidate.clone()); + setDate(candidate.clone().startOf("day").unix()); setAutoSelected(true); - } else { - // پیدا کردن نزدیک‌ترین روز قابل انتخاب - let nearestDate = null; - let daysToCheck = 30; // بررسی 30 روز آینده - - for (let i = 1; i <= daysToCheck; i++) { - const futureDate = moment().tz("Asia/Tehran").add(i, "days"); - const futureDateTimestamp = futureDate.clone().startOf("day").unix(); - - const isFutureDateDisabled = disabledDates.some((ts) => - moment.unix(ts).startOf("day").isSame(futureDate.startOf("day")) - ); - - if (!isFutureDateDisabled) { - nearestDate = futureDate; - setStartDate(futureDate); - setDate(futureDateTimestamp); - setAutoSelected(true); - break; - } - } + break; } } - }, [loading, disabledDates, autoSelected, setDate]); + }, [disabledDates, autoSelected, setDate]); + + const secondMonth = moment(baseMonth).add(1, "jMonth"); return ( -
- - +
+
+ setBaseMonth(moment(baseMonth).add(1, "jMonth"))} + onPrev={() => setBaseMonth(moment(baseMonth).subtract(1, "jMonth"))} + /> +
+
+ setBaseMonth(moment(baseMonth).subtract(1, "jMonth"))} + onNext={() => setBaseMonth(moment(baseMonth).add(1, "jMonth"))} + /> +
); } diff --git a/components/common/InlineJalaliMonth.js b/components/common/InlineJalaliMonth.js new file mode 100644 index 0000000..439bdb1 --- /dev/null +++ b/components/common/InlineJalaliMonth.js @@ -0,0 +1,118 @@ +"use client"; + +import moment from "moment-jalaali"; + +moment.loadPersian({ dialect: "persian-modern", usePersianDigits: false }); + +const WEEKDAYS = ["شنبه", "۱ شنبه", "۲ شنبه", "۳ شنبه", "۴ شنبه", "۵ شنبه", "جمعه"]; +const MONTHS = [ + "فروردین", + "اردیبهشت", + "خرداد", + "تیر", + "مرداد", + "شهریور", + "مهر", + "آبان", + "آذر", + "دی", + "بهمن", + "اسفند", +]; + +function buildDays(displayDate) { + const year = displayDate.jYear(); + const month = displayDate.jMonth(); + const daysInMonth = moment.jDaysInMonth(year, month); + const firstDayOfMonth = moment(`${year}/${month + 1}/1`, "jYYYY/jM/jD").day(); + const startOffset = (firstDayOfMonth + 1) % 7; + + const days = []; + for (let i = 0; i < startOffset; i++) days.push(null); + for (let day = 1; day <= daysInMonth; day++) days.push(day); + return days; +} + +function InlineJalaliMonth({ + displayDate, + selectedDate, + onSelectDay, + isDisabled, + onPrev, + onNext, + showPrev = true, + showNext = true, +}) { + const days = buildDays(displayDate); + + return ( +
+
+ +

+ {MONTHS[displayDate.jMonth()]} {displayDate.jYear()} +

+ +
+ +
+ {WEEKDAYS.map((label, idx) => ( + + {label} + + ))} +
+ +
+ {days.map((day, index) => { + if (!day) return ; + + const dayDate = moment(displayDate).jDate(day); + const isFriday = dayDate.day() === 5; + const disabled = isDisabled(dayDate); + const selected = + selectedDate && + dayDate.jYear() === selectedDate.jYear() && + dayDate.jMonth() === selectedDate.jMonth() && + dayDate.jDate() === selectedDate.jDate(); + + return ( + + ); + })} +
+
+ ); +} + +export default InlineJalaliMonth;