PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.3
Yatra – Travel Booking & Tour Operator Software v3.0.3
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 2.0.10 All 83 releases
yatra / resources / js / components / ui / date-range-picker.tsx

date-range-picker.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.3, at resources/js/components/ui/date-range-picker.tsx

248 lines 7.8 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 { format, parse, isWithinInterval, isSameDay } from "date-fns";
3 import { Calendar as CalendarIcon, X } from "lucide-react";
4 import { Popover, PopoverContent, PopoverTrigger } from "./popover";
5 import { Button } from "./button";
6 import {
7 startOfMonth,
8 endOfMonth,
9 startOfWeek,
10 endOfWeek,
11 eachDayOfInterval,
12 isSameMonth,
13 addMonths,
14 subMonths,
15 isToday,
16 } from "date-fns";
17 import { ChevronLeft, ChevronRight } from "lucide-react";
18
19 export interface DateRangePickerProps {
20 dateFrom?: string; // YYYY-MM-DD format
21 dateTo?: string; // YYYY-MM-DD format
22 onDateFromChange?: (value: string) => void;
23 onDateToChange?: (value: string) => void;
24 onClear?: () => void;
25 placeholder?: string;
26 disabled?: boolean;
27 className?: string;
28 error?: boolean;
29 }
30
31 export const DateRangePicker: React.FC<DateRangePickerProps> = ({
32 dateFrom,
33 dateTo,
34 onDateFromChange,
35 onDateToChange,
36 onClear,
37 placeholder = "Select date range",
38 disabled = false,
39 className = "",
40 error = false,
41 }) => {
42 const [open, setOpen] = React.useState(false);
43 const [currentMonth, setCurrentMonth] = React.useState(new Date());
44 const [hoverDate, setHoverDate] = React.useState<Date | undefined>();
45
46 // Parse dates safely
47 let fromDate: Date | undefined = undefined;
48 let toDate: Date | undefined = undefined;
49
50 if (dateFrom && dateFrom.trim()) {
51 try {
52 const parsed = parse(dateFrom, "yyyy-MM-dd", new Date());
53 if (!isNaN(parsed.getTime())) {
54 fromDate = parsed;
55 }
56 } catch (e) {
57 fromDate = undefined;
58 }
59 }
60
61 if (dateTo && dateTo.trim()) {
62 try {
63 const parsed = parse(dateTo, "yyyy-MM-dd", new Date());
64 if (!isNaN(parsed.getTime())) {
65 toDate = parsed;
66 }
67 } catch (e) {
68 toDate = undefined;
69 }
70 }
71
72 const handleSelect = (date: Date) => {
73 if (!fromDate || (fromDate && toDate)) {
74 // Start new selection
75 onDateFromChange?.(format(date, "yyyy-MM-dd"));
76 onDateToChange?.("");
77 setHoverDate(undefined);
78 } else if (fromDate && !toDate) {
79 // Complete the range
80 if (date < fromDate) {
81 // If selected date is before start, swap them
82 onDateToChange?.(format(fromDate, "yyyy-MM-dd"));
83 onDateFromChange?.(format(date, "yyyy-MM-dd"));
84 } else {
85 onDateToChange?.(format(date, "yyyy-MM-dd"));
86 }
87 setHoverDate(undefined);
88 setOpen(false);
89 }
90 };
91
92 const handleClear = (e: React.MouseEvent) => {
93 e.stopPropagation();
94 onClear?.();
95 setHoverDate(undefined);
96 };
97
98 const isInRange = (date: Date) => {
99 if (!fromDate) return false;
100 const endDate = toDate || hoverDate;
101 if (!endDate) return false;
102
103 const start = fromDate < endDate ? fromDate : endDate;
104 const end = fromDate < endDate ? endDate : fromDate;
105
106 try {
107 return isWithinInterval(date, { start, end });
108 } catch {
109 return false;
110 }
111 };
112
113 const isRangeStart = (date: Date) => {
114 return fromDate && isSameDay(date, fromDate);
115 };
116
117 const isRangeEnd = (date: Date) => {
118 const endDate = toDate || hoverDate;
119 return endDate && isSameDay(date, endDate);
120 };
121
122 // Helper to generate calendar days
123 const getDaysForMonth = (monthDate: Date) => {
124 const monthStart = startOfMonth(monthDate);
125 const monthEnd = endOfMonth(monthDate);
126 const calendarStart = startOfWeek(monthStart, { weekStartsOn: 0 });
127 const calendarEnd = endOfWeek(monthEnd, { weekStartsOn: 0 });
128 return eachDayOfInterval({ start: calendarStart, end: calendarEnd });
129 };
130
131 const nextMonthDate = addMonths(currentMonth, 1);
132 const currentMonthDays = getDaysForMonth(currentMonth);
133 const nextMonthDays = getDaysForMonth(nextMonthDate);
134 const weekDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
135
136 // Format display value
137 let displayValue = "";
138 if (fromDate && !isNaN(fromDate.getTime())) {
139 try {
140 displayValue = format(fromDate, "MMM dd, yyyy");
141 if (toDate && !isNaN(toDate.getTime())) {
142 displayValue += " - " + format(toDate, "MMM dd, yyyy");
143 }
144 } catch (e) {
145 displayValue = "";
146 }
147 }
148
149 const hasValue = dateFrom || dateTo;
150
151 const renderMonth = (monthDate: Date, days: Date[]) => (
152 <div className="w-[280px]">
153 <div className="font-semibold text-center mb-4">
154 {format(monthDate, "MMMM yyyy")}
155 </div>
156 <div className="grid grid-cols-7 gap-1 mb-2">
157 {weekDays.map((day) => (
158 <div
159 key={day}
160 className="text-center text-xs font-medium text-gray-500 dark:text-gray-400 p-2"
161 >
162 {day}
163 </div>
164 ))}
165 </div>
166 <div className="grid grid-cols-7 gap-1">
167 {days.map((day, idx) => {
168 const isCurrentMonth = isSameMonth(day, monthDate);
169 const isSelected = isRangeStart(day) || isRangeEnd(day);
170 const inRange = isInRange(day);
171 const isTodayDate = isToday(day);
172
173 return (
174 <button
175 key={idx}
176 type="button"
177 onClick={() => handleSelect(day)}
178 onMouseEnter={() => fromDate && !toDate && setHoverDate(day)}
179 onMouseLeave={() => setHoverDate(undefined)}
180 className={`
181 p-2 text-sm rounded transition-colors relative w-9 h-9 flex items-center justify-center mx-auto
182 ${!isCurrentMonth ? "text-gray-300 dark:text-gray-700 invisible" : ""}
183 ${isSelected ? "bg-blue-600 text-white font-semibold hover:bg-blue-700 z-10" : ""}
184 ${inRange && !isSelected ? "bg-blue-100 dark:bg-blue-900/20 text-blue-900 dark:text-blue-100 rounded-none" : ""}
185 ${!isSelected && !inRange && isCurrentMonth ? "hover:bg-gray-100 dark:hover:bg-gray-700" : ""}
186 ${isTodayDate && !isSelected ? "border border-blue-500" : ""}
187 ${isRangeStart(day) && inRange ? "rounded-l-md" : ""}
188 ${isRangeEnd(day) && inRange ? "rounded-r-md" : ""}
189 `}
190 disabled={!isCurrentMonth}
191 >
192 {format(day, "d")}
193 </button>
194 );
195 })}
196 </div>
197 </div>
198 );
199
200 return (
201 <Popover open={open} onOpenChange={setOpen}>
202 <PopoverTrigger asChild>
203 <Button
204 type="button"
205 variant="outline"
206 disabled={disabled}
207 className={`w-full justify-start text-left font-normal ${error ? "border-red-500" : ""} ${!displayValue ? "text-gray-500" : ""} ${className}`}
208 >
209 <CalendarIcon className="mr-2 h-4 w-4" />
210 {displayValue || <span className="text-gray-500">{placeholder}</span>}
211 {hasValue && !disabled && (
212 <X
213 className="ml-auto h-4 w-4 opacity-50 hover:opacity-100"
214 onClick={handleClear}
215 />
216 )}
217 </Button>
218 </PopoverTrigger>
219 <PopoverContent className="w-auto p-0" align="start">
220 <div className="p-4">
221 <div className="flex items-center justify-between mb-4 px-2">
222 <button
223 type="button"
224 onClick={() => setCurrentMonth(subMonths(currentMonth, 1))}
225 className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
226 >
227 <ChevronLeft className="h-4 w-4" />
228 </button>
229 <button
230 type="button"
231 onClick={() => setCurrentMonth(addMonths(currentMonth, 1))}
232 className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
233 >
234 <ChevronRight className="h-4 w-4" />
235 </button>
236 </div>
237 <div className="flex gap-8">
238 {renderMonth(currentMonth, currentMonthDays)}
239 <div className="border-l border-gray-200 dark:border-gray-700 pl-8">
240 {renderMonth(nextMonthDate, nextMonthDays)}
241 </div>
242 </div>
243 </div>
244 </PopoverContent>
245 </Popover>
246 );
247 };
248