PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.9
Yatra – Travel Booking & Tour Operator Software v3.0.9
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / resources / js / components / ui / calendar.tsx

calendar.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.9, at resources/js/components/ui/calendar.tsx

209 lines 6.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import * as React from "react";
2 import { ChevronLeft, ChevronRight } from "lucide-react";
3 import {
4 format,
5 startOfMonth,
6 endOfMonth,
7 startOfWeek,
8 endOfWeek,
9 eachDayOfInterval,
10 isSameMonth,
11 isSameDay,
12 addMonths,
13 subMonths,
14 setMonth,
15 setYear,
16 getMonth,
17 getYear,
18 isToday,
19 } from "date-fns";
20
21 export interface CalendarProps {
22 selected?: Date;
23 onSelect?: (date: Date | undefined) => void;
24 disabled?: (date: Date) => boolean;
25 minDate?: Date;
26 maxDate?: Date;
27 className?: string;
28 }
29
30 const MONTH_LABELS = [
31 "January",
32 "February",
33 "March",
34 "April",
35 "May",
36 "June",
37 "July",
38 "August",
39 "September",
40 "October",
41 "November",
42 "December",
43 ];
44
45 export const Calendar: React.FC<CalendarProps> = ({
46 selected,
47 onSelect,
48 disabled,
49 minDate,
50 maxDate,
51 className = "",
52 }) => {
53 // Open on the selected date (e.g. a saved date of birth lands on its own
54 // year), falling back to today for a blank field.
55 const [currentMonth, setCurrentMonth] = React.useState<Date>(
56 () => selected ?? new Date(),
57 );
58
59 // When the bound value changes (popover reopened with an existing value),
60 // jump the grid to that month so the user isn't stranded on today.
61 React.useEffect(() => {
62 if (selected) setCurrentMonth(selected);
63 }, [selected]);
64
65 // Year range for the dropdown: honour min/max when provided, else a wide
66 // span that comfortably covers dates of birth (back ~120 years) and future
67 // trip dates (~10 years ahead). Listed newest-first.
68 const today = new Date();
69 const fromYear = minDate ? getYear(minDate) : getYear(today) - 120;
70 const toYear = maxDate ? getYear(maxDate) : getYear(today) + 10;
71 const years: number[] = [];
72 for (let y = toYear; y >= fromYear; y--) {
73 years.push(y);
74 }
75
76 const handleMonthSelect = (monthIndex: number) => {
77 setCurrentMonth(setMonth(currentMonth, monthIndex));
78 };
79
80 const handleYearSelect = (year: number) => {
81 setCurrentMonth(setYear(currentMonth, year));
82 };
83
84 const monthStart = startOfMonth(currentMonth);
85 const monthEnd = endOfMonth(currentMonth);
86 const calendarStart = startOfWeek(monthStart, { weekStartsOn: 0 });
87 const calendarEnd = endOfWeek(monthEnd, { weekStartsOn: 0 });
88 const days = eachDayOfInterval({ start: calendarStart, end: calendarEnd });
89
90 const isDateDisabled = (date: Date) => {
91 if (disabled && disabled(date)) return true;
92 if (minDate && date < minDate) return true;
93 if (maxDate && date > maxDate) return true;
94 return false;
95 };
96
97 const handleDateClick = (date: Date) => {
98 if (isDateDisabled(date)) return;
99 onSelect?.(date);
100 };
101
102 const previousMonth = () => {
103 setCurrentMonth(subMonths(currentMonth, 1));
104 };
105
106 const nextMonth = () => {
107 setCurrentMonth(addMonths(currentMonth, 1));
108 };
109
110 const weekDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
111
112 return (
113 <div className={`p-3 ${className}`}>
114 <div className="flex items-center justify-between gap-1 mb-4">
115 <button
116 type="button"
117 onClick={previousMonth}
118 aria-label="Previous month"
119 className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors shrink-0"
120 >
121 <ChevronLeft className="w-4 h-4" />
122 </button>
123
124 <div className="flex items-center gap-1">
125 {/* Month dropdown — jump to any month in one click */}
126 <select
127 value={getMonth(currentMonth)}
128 onChange={(e) => handleMonthSelect(Number(e.target.value))}
129 aria-label="Month"
130 className="text-sm font-semibold text-gray-900 dark:text-white bg-transparent border border-transparent hover:border-gray-300 dark:hover:border-gray-600 focus:border-blue-500 rounded-md px-1 py-0.5 cursor-pointer focus:outline-none dark:bg-gray-800"
131 >
132 {MONTH_LABELS.map((label, index) => (
133 <option key={label} value={index}>
134 {label}
135 </option>
136 ))}
137 </select>
138
139 {/* Year dropdown — jump straight to e.g. 1990 without month-stepping */}
140 <select
141 value={getYear(currentMonth)}
142 onChange={(e) => handleYearSelect(Number(e.target.value))}
143 aria-label="Year"
144 className="text-sm font-semibold text-gray-900 dark:text-white bg-transparent border border-transparent hover:border-gray-300 dark:hover:border-gray-600 focus:border-blue-500 rounded-md px-1 py-0.5 cursor-pointer focus:outline-none dark:bg-gray-800"
145 >
146 {years.map((year) => (
147 <option key={year} value={year}>
148 {year}
149 </option>
150 ))}
151 </select>
152 </div>
153
154 <button
155 type="button"
156 onClick={nextMonth}
157 aria-label="Next month"
158 className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors shrink-0"
159 >
160 <ChevronRight className="w-4 h-4" />
161 </button>
162 </div>
163
164 <div className="grid grid-cols-7 gap-1 mb-2">
165 {weekDays.map((day) => (
166 <div
167 key={day}
168 className="text-xs font-medium text-gray-500 dark:text-gray-400 text-center py-2"
169 >
170 {day}
171 </div>
172 ))}
173 </div>
174
175 <div className="grid grid-cols-7 gap-1">
176 {days.map((day, dayIdx) => {
177 const isCurrentMonth = isSameMonth(day, currentMonth);
178 const isSelected = selected && isSameDay(day, selected);
179 const isTodayDate = isToday(day);
180 const isDisabled = isDateDisabled(day);
181
182 return (
183 <button
184 key={dayIdx}
185 type="button"
186 onClick={() => handleDateClick(day)}
187 disabled={isDisabled}
188 className={`
189 h-9 w-9 text-sm rounded-md transition-colors
190 ${isCurrentMonth ? "text-gray-900 dark:text-white" : "text-gray-400 dark:text-gray-600"}
191 ${
192 isSelected
193 ? "bg-blue-600 text-white hover:bg-blue-700"
194 : isTodayDate
195 ? "bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 font-semibold"
196 : "hover:bg-gray-100 dark:hover:bg-gray-700"
197 }
198 ${isDisabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}
199 `}
200 >
201 {format(day, "d")}
202 </button>
203 );
204 })}
205 </div>
206 </div>
207 );
208 };
209