pejay-ui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +0 -0
- package/bin/cli.js +379 -0
- package/package.json +52 -0
- package/registry.json +350 -0
- package/templates/button/Button.tsx +156 -0
- package/templates/button/index.ts +2 -0
- package/templates/button/tooltip.tsx +124 -0
- package/templates/form/amount-input.tsx +252 -0
- package/templates/form/checkbox-group.tsx +235 -0
- package/templates/form/checkbox.tsx +148 -0
- package/templates/form/date-picker.tsx +647 -0
- package/templates/form/date-range-picker.tsx +1039 -0
- package/templates/form/email-input.tsx +55 -0
- package/templates/form/file-input.tsx +380 -0
- package/templates/form/index.ts +22 -0
- package/templates/form/input.tsx +255 -0
- package/templates/form/number-input.tsx +186 -0
- package/templates/form/password-input.tsx +233 -0
- package/templates/form/phone-input.tsx +82 -0
- package/templates/form/radio-group.tsx +191 -0
- package/templates/form/radio.tsx +157 -0
- package/templates/form/range-slider.tsx +210 -0
- package/templates/form/switch.tsx +134 -0
- package/templates/form/textarea.tsx +253 -0
- package/templates/form/time-picker.tsx +435 -0
- package/templates/form/time-range-picker.tsx +526 -0
- package/templates/form/url-input.tsx +81 -0
- package/templates/select-dropdown/index.ts +4 -0
- package/templates/select-dropdown/multiselect-input.tsx +687 -0
- package/templates/select-dropdown/select-input.tsx +565 -0
- package/utils/cn.ts +6 -0
|
@@ -0,0 +1,1039 @@
|
|
|
1
|
+
import React, { useState, useMemo } from "react";
|
|
2
|
+
import { cn } from "@/utils/cn";
|
|
3
|
+
import {
|
|
4
|
+
Calendar as CalendarIcon,
|
|
5
|
+
ChevronLeft,
|
|
6
|
+
ChevronRight,
|
|
7
|
+
ChevronDown,
|
|
8
|
+
PanelLeftClose,
|
|
9
|
+
PanelLeft,
|
|
10
|
+
} from "lucide-react";
|
|
11
|
+
import { SelectInput } from "../select-dropdown/select-input";
|
|
12
|
+
const DateUtils = {};
|
|
13
|
+
import {
|
|
14
|
+
useFloating,
|
|
15
|
+
autoUpdate,
|
|
16
|
+
offset,
|
|
17
|
+
flip,
|
|
18
|
+
shift,
|
|
19
|
+
size,
|
|
20
|
+
useClick,
|
|
21
|
+
useDismiss,
|
|
22
|
+
useRole,
|
|
23
|
+
useInteractions,
|
|
24
|
+
FloatingPortal,
|
|
25
|
+
FloatingFocusManager,
|
|
26
|
+
} from "@floating-ui/react";
|
|
27
|
+
|
|
28
|
+
interface DateRange {
|
|
29
|
+
from: Date | undefined;
|
|
30
|
+
to: Date | undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type StaticPresetId =
|
|
34
|
+
| "yesterday"
|
|
35
|
+
| "today"
|
|
36
|
+
| "tomorrow"
|
|
37
|
+
| "last-week"
|
|
38
|
+
| "this-week"
|
|
39
|
+
| "next-week"
|
|
40
|
+
| "last-month"
|
|
41
|
+
| "this-month"
|
|
42
|
+
| "next-month"
|
|
43
|
+
| "last-year"
|
|
44
|
+
| "this-year"
|
|
45
|
+
| "next-year";
|
|
46
|
+
|
|
47
|
+
type DynamicPresetId =
|
|
48
|
+
| `last-${number}-weeks`
|
|
49
|
+
| `last-${number}-months`
|
|
50
|
+
| `last-${number}-years`
|
|
51
|
+
| `next-${number}-weeks`
|
|
52
|
+
| `next-${number}-months`
|
|
53
|
+
| `next-${number}-years`;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Available static and dynamic presets for the DateRangePicker sidebar.
|
|
57
|
+
*
|
|
58
|
+
* Static presets:
|
|
59
|
+
* - "today", "yesterday", "tomorrow"
|
|
60
|
+
* - "this-week", "last-week", "next-week"
|
|
61
|
+
* - "this-month", "last-month", "next-month"
|
|
62
|
+
* - "this-year", "last-year", "next-year"
|
|
63
|
+
*
|
|
64
|
+
* Dynamic presets (replace 'X' with a number):
|
|
65
|
+
* - "last-X-weeks", "next-X-weeks" (Max X: 52)
|
|
66
|
+
* - "last-X-months", "next-X-months" (Max X: 12)
|
|
67
|
+
* - "last-X-years", "next-X-years" (Max X: 10)
|
|
68
|
+
*/
|
|
69
|
+
type PresetId = StaticPresetId | DynamicPresetId | string;
|
|
70
|
+
|
|
71
|
+
interface DateRangePickerProps {
|
|
72
|
+
label?: string;
|
|
73
|
+
description?: string;
|
|
74
|
+
error?: string;
|
|
75
|
+
value?: DateRange;
|
|
76
|
+
onChange?: (range: DateRange) => void;
|
|
77
|
+
presets?: PresetId[];
|
|
78
|
+
minYear?: number;
|
|
79
|
+
maxYear?: number;
|
|
80
|
+
placeholder?: string;
|
|
81
|
+
defaultToToday?: boolean;
|
|
82
|
+
formatStr?: string;
|
|
83
|
+
disableBefore?: Date;
|
|
84
|
+
disableAfter?: Date;
|
|
85
|
+
isTypeable?: boolean;
|
|
86
|
+
typeableFormat?: string;
|
|
87
|
+
variant?: "rounded" | "curved" | "square";
|
|
88
|
+
labelPlacement?: "top" | "left" | "right";
|
|
89
|
+
labelWidth?: string;
|
|
90
|
+
"labelAlign-X"?: "left" | "center" | "right";
|
|
91
|
+
"labelAlign-Y"?: "top" | "middle" | "bottom";
|
|
92
|
+
className?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const DateRangePicker = ({
|
|
96
|
+
label,
|
|
97
|
+
description,
|
|
98
|
+
error,
|
|
99
|
+
value,
|
|
100
|
+
onChange,
|
|
101
|
+
presets: enabledPresets,
|
|
102
|
+
minYear = 1900,
|
|
103
|
+
maxYear = 2100,
|
|
104
|
+
placeholder,
|
|
105
|
+
defaultToToday = false,
|
|
106
|
+
formatStr = "dd/mm/yyyy",
|
|
107
|
+
disableBefore,
|
|
108
|
+
disableAfter,
|
|
109
|
+
isTypeable = false,
|
|
110
|
+
typeableFormat = "dd/mm/yyyy",
|
|
111
|
+
variant = "curved",
|
|
112
|
+
labelPlacement = "top",
|
|
113
|
+
labelWidth = "w-32",
|
|
114
|
+
"labelAlign-X": labelAlignX,
|
|
115
|
+
"labelAlign-Y": labelAlignY = "middle",
|
|
116
|
+
className,
|
|
117
|
+
}: DateRangePickerProps) => {
|
|
118
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
119
|
+
const [range, setRange] = useState<DateRange>(
|
|
120
|
+
value || { from: undefined, to: undefined },
|
|
121
|
+
);
|
|
122
|
+
const [inputValue, setInputValue] = useState("");
|
|
123
|
+
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
124
|
+
const [cursorPos, setCursorPos] = useState<number | null>(null);
|
|
125
|
+
const [hoverDate, setHoverDate] = useState<Date | undefined>(undefined);
|
|
126
|
+
const [viewDate, setViewDate] = useState<Date>(range.from || new Date());
|
|
127
|
+
const [activeBox, setActiveBox] = useState<"from" | "to">("from");
|
|
128
|
+
const [isPanelVisible, setIsPanelVisible] = useState(true);
|
|
129
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
130
|
+
|
|
131
|
+
/* Core Utilities from root.config */
|
|
132
|
+
const {
|
|
133
|
+
format: baseFormat = (d: Date) => d.toDateString(),
|
|
134
|
+
addMonths = (d: Date, n: number) =>
|
|
135
|
+
new Date(d.getFullYear(), d.getMonth() + n, 1),
|
|
136
|
+
subMonths = (d: Date, n: number) =>
|
|
137
|
+
new Date(d.getFullYear(), d.getMonth() - n, 1),
|
|
138
|
+
startOfMonth = (d: Date) => new Date(d.getFullYear(), d.getMonth(), 1),
|
|
139
|
+
endOfMonth = (d: Date) => new Date(d.getFullYear(), d.getMonth() + 1, 0),
|
|
140
|
+
startOfWeek = (d: Date) => {
|
|
141
|
+
const date = new Date(d);
|
|
142
|
+
const day = date.getDay();
|
|
143
|
+
const diff = date.getDate() - day;
|
|
144
|
+
return new Date(date.setDate(diff));
|
|
145
|
+
},
|
|
146
|
+
endOfWeek = (d: Date) => {
|
|
147
|
+
const date = new Date(d);
|
|
148
|
+
const day = date.getDay();
|
|
149
|
+
const diff = date.getDate() + (6 - day);
|
|
150
|
+
return new Date(date.setDate(diff));
|
|
151
|
+
},
|
|
152
|
+
eachDayOfInterval = ({ start, end }: { start: Date; end: Date }) => {
|
|
153
|
+
const days = [];
|
|
154
|
+
let current = new Date(start);
|
|
155
|
+
while (current <= end) {
|
|
156
|
+
days.push(new Date(current));
|
|
157
|
+
current.setDate(current.getDate() + 1);
|
|
158
|
+
}
|
|
159
|
+
return days;
|
|
160
|
+
},
|
|
161
|
+
isSameDay = (d1: Date, d2: Date) => d1.toDateString() === d2.toDateString(),
|
|
162
|
+
addDays = (d: Date, n: number) => {
|
|
163
|
+
const r = new Date(d);
|
|
164
|
+
r.setDate(r.getDate() + n);
|
|
165
|
+
return r;
|
|
166
|
+
},
|
|
167
|
+
subDays = (d: Date, n: number) => {
|
|
168
|
+
const r = new Date(d);
|
|
169
|
+
r.setDate(r.getDate() - n);
|
|
170
|
+
return r;
|
|
171
|
+
},
|
|
172
|
+
startOfYear = (d: Date) => new Date(d.getFullYear(), 0, 1),
|
|
173
|
+
endOfYear = (d: Date) => new Date(d.getFullYear(), 11, 31),
|
|
174
|
+
addYears = (d: Date, n: number) =>
|
|
175
|
+
new Date(d.getFullYear() + n, d.getMonth(), d.getDate()),
|
|
176
|
+
subYears = (d: Date, n: number) =>
|
|
177
|
+
new Date(d.getFullYear() - n, d.getMonth(), d.getDate()),
|
|
178
|
+
} = DateUtils as any;
|
|
179
|
+
|
|
180
|
+
const format = (d: Date, fmtStr: string) => {
|
|
181
|
+
const normalized = fmtStr.replace(/mm/g, "MM");
|
|
182
|
+
return baseFormat(d, normalized);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/* Local helper functions for missing exports */
|
|
186
|
+
const isBeforeDate = (d1: Date, d2: Date) => d1.getTime() < d2.getTime();
|
|
187
|
+
const isAfterDate = (d1: Date, d2: Date) => d1.getTime() > d2.getTime();
|
|
188
|
+
|
|
189
|
+
const isDateDisabled = (date: Date) => {
|
|
190
|
+
if (
|
|
191
|
+
disableBefore &&
|
|
192
|
+
isBeforeDate(date, disableBefore) &&
|
|
193
|
+
!isSameDay(date, disableBefore)
|
|
194
|
+
)
|
|
195
|
+
return true;
|
|
196
|
+
if (
|
|
197
|
+
disableAfter &&
|
|
198
|
+
isAfterDate(date, disableAfter) &&
|
|
199
|
+
!isSameDay(date, disableAfter)
|
|
200
|
+
)
|
|
201
|
+
return true;
|
|
202
|
+
return false;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
React.useEffect(() => {
|
|
206
|
+
if (range.from && range.to && isTypeable && !isFocused) {
|
|
207
|
+
setInputValue(
|
|
208
|
+
`${format(range.from, typeableFormat)} to ${format(range.to, typeableFormat)}`,
|
|
209
|
+
);
|
|
210
|
+
} else if (!range.from && !range.to && !isFocused) {
|
|
211
|
+
setInputValue("");
|
|
212
|
+
}
|
|
213
|
+
}, [range, isTypeable, typeableFormat, isFocused, format]);
|
|
214
|
+
|
|
215
|
+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
216
|
+
let rawVal = e.target.value;
|
|
217
|
+
let cPos = e.target.selectionStart || 0;
|
|
218
|
+
let nativeEvent = e.nativeEvent as InputEvent;
|
|
219
|
+
let isDeleting = nativeEvent.inputType === "deleteContentBackward";
|
|
220
|
+
|
|
221
|
+
const rawBeforeCursor = rawVal.slice(0, cPos);
|
|
222
|
+
const valueCharsBefore = (rawBeforeCursor.match(/[0-9]/g) || []).length;
|
|
223
|
+
|
|
224
|
+
let digits = rawVal.replace(/\D/g, "").slice(0, 16);
|
|
225
|
+
let oldDigits = inputValue.replace(/\D/g, "");
|
|
226
|
+
|
|
227
|
+
/* If they backspaced a delimiter, force remove a digit */
|
|
228
|
+
if (isDeleting && oldDigits.length === digits.length && digits.length > 0) {
|
|
229
|
+
/* Find which digit to remove based on cursor position.
|
|
230
|
+
A simple fallback: just remove the last digit if we can't pinpoint it,
|
|
231
|
+
but actually, since they deleted a delimiter, the mask will just put it back.
|
|
232
|
+
To actually delete the digit before the delimiter, we can just slice the digits. */
|
|
233
|
+
digits = digits.slice(0, -1);
|
|
234
|
+
cPos--;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let masked = "";
|
|
238
|
+
|
|
239
|
+
/* Date 1 */
|
|
240
|
+
if (digits.length > 0) masked += digits.slice(0, 2);
|
|
241
|
+
if (digits.length > 2) masked += "/" + digits.slice(2, 4);
|
|
242
|
+
if (digits.length > 4) masked += "/" + digits.slice(4, 8);
|
|
243
|
+
|
|
244
|
+
if (digits.length > 8) masked += " to ";
|
|
245
|
+
|
|
246
|
+
/* Date 2 */
|
|
247
|
+
if (digits.length > 8) masked += digits.slice(8, 10);
|
|
248
|
+
if (digits.length > 10) masked += "/" + digits.slice(10, 12);
|
|
249
|
+
if (digits.length > 12) masked += "/" + digits.slice(12, 16);
|
|
250
|
+
|
|
251
|
+
/* Calculate full mask for cursor */
|
|
252
|
+
let baseTemplate = typeableFormat.replace(/[a-zA-Z]/g, "_");
|
|
253
|
+
let template = `${baseTemplate} to ${baseTemplate}`;
|
|
254
|
+
let fullMask = "";
|
|
255
|
+
for (let i = 0; i < template.length; i++) {
|
|
256
|
+
if (masked[i]) fullMask += masked[i];
|
|
257
|
+
else fullMask += template[i];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let newCPos = 0;
|
|
261
|
+
let count = 0;
|
|
262
|
+
for (let i = 0; i < fullMask.length; i++) {
|
|
263
|
+
if (/[0-9]/.test(fullMask[i])) count++;
|
|
264
|
+
if (count === valueCharsBefore) {
|
|
265
|
+
newCPos = i + 1;
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (valueCharsBefore === 0) newCPos = 0;
|
|
270
|
+
else if (count < valueCharsBefore) newCPos = fullMask.length;
|
|
271
|
+
|
|
272
|
+
if (!isDeleting) {
|
|
273
|
+
while (fullMask[newCPos] && /[^0-9A-P_]/.test(fullMask[newCPos])) {
|
|
274
|
+
newCPos++;
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
while (
|
|
278
|
+
newCPos > 0 &&
|
|
279
|
+
fullMask[newCPos - 1] &&
|
|
280
|
+
/[^0-9A-P_]/.test(fullMask[newCPos - 1])
|
|
281
|
+
) {
|
|
282
|
+
newCPos--;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
setCursorPos(newCPos);
|
|
287
|
+
setInputValue(masked);
|
|
288
|
+
|
|
289
|
+
if (digits.length === 16) {
|
|
290
|
+
const p1 = masked.split(" to ")[0];
|
|
291
|
+
const p2 = masked.split(" to ")[1];
|
|
292
|
+
if (p1 && p2) {
|
|
293
|
+
const d1 = new Date(p1);
|
|
294
|
+
const d2 = new Date(p2);
|
|
295
|
+
if (!isNaN(d1.getTime()) && !isNaN(d2.getTime())) {
|
|
296
|
+
setRange({ from: d1, to: d2 });
|
|
297
|
+
onChange?.({ from: d1, to: d2 });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
} else if (digits.length === 0) {
|
|
301
|
+
setRange({ from: undefined, to: undefined });
|
|
302
|
+
onChange?.({ from: undefined, to: undefined });
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const getFullMaskedValue = () => {
|
|
307
|
+
if (!isFocused && !inputValue) return "";
|
|
308
|
+
let current = inputValue;
|
|
309
|
+
let baseTemplate = typeableFormat.replace(/[a-zA-Z]/g, "_");
|
|
310
|
+
let template = `${baseTemplate} to ${baseTemplate}`;
|
|
311
|
+
|
|
312
|
+
let res = "";
|
|
313
|
+
for (let i = 0; i < template.length; i++) {
|
|
314
|
+
if (current[i]) res += current[i];
|
|
315
|
+
else res += template[i];
|
|
316
|
+
}
|
|
317
|
+
return res;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const handleFocus = () => {
|
|
321
|
+
setIsFocused(true);
|
|
322
|
+
const pos = inputValue.length;
|
|
323
|
+
setTimeout(() => inputRef.current?.setSelectionRange(pos, pos), 0);
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
327
|
+
if (e.ctrlKey || e.metaKey || e.altKey || e.key.length > 1) return;
|
|
328
|
+
if (!/[0-9 \-:/TO]/.test(e.key.toUpperCase())) {
|
|
329
|
+
e.preventDefault();
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
React.useLayoutEffect(() => {
|
|
334
|
+
if (isFocused && inputRef.current && cursorPos !== null) {
|
|
335
|
+
inputRef.current.setSelectionRange(cursorPos, cursorPos);
|
|
336
|
+
}
|
|
337
|
+
}, [inputValue, isFocused, cursorPos]);
|
|
338
|
+
|
|
339
|
+
const { refs, floatingStyles, context } = useFloating({
|
|
340
|
+
open: !isTypeable && isOpen,
|
|
341
|
+
onOpenChange: setIsOpen,
|
|
342
|
+
middleware: [
|
|
343
|
+
offset(10),
|
|
344
|
+
flip({ fallbackAxisSideDirection: "start" }),
|
|
345
|
+
shift(),
|
|
346
|
+
size({
|
|
347
|
+
apply({ availableHeight, elements }) {
|
|
348
|
+
Object.assign(elements.floating.style, {
|
|
349
|
+
maxHeight: `${Math.max(400, availableHeight - 20)}px`,
|
|
350
|
+
});
|
|
351
|
+
},
|
|
352
|
+
}),
|
|
353
|
+
],
|
|
354
|
+
whileElementsMounted: autoUpdate,
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const click = useClick(context, { enabled: !isTypeable });
|
|
358
|
+
const dismiss = useDismiss(context, { enabled: !isTypeable });
|
|
359
|
+
const role = useRole(context);
|
|
360
|
+
const { getReferenceProps, getFloatingProps } = useInteractions([
|
|
361
|
+
click,
|
|
362
|
+
dismiss,
|
|
363
|
+
role,
|
|
364
|
+
]);
|
|
365
|
+
|
|
366
|
+
const calendarDays = useMemo(() => {
|
|
367
|
+
try {
|
|
368
|
+
const start = startOfWeek(startOfMonth(viewDate));
|
|
369
|
+
const end = endOfWeek(endOfMonth(viewDate));
|
|
370
|
+
return eachDayOfInterval({ start, end }) as Date[];
|
|
371
|
+
} catch (e) {
|
|
372
|
+
return [] as Date[];
|
|
373
|
+
}
|
|
374
|
+
}, [
|
|
375
|
+
viewDate,
|
|
376
|
+
startOfWeek,
|
|
377
|
+
startOfMonth,
|
|
378
|
+
endOfWeek,
|
|
379
|
+
endOfMonth,
|
|
380
|
+
eachDayOfInterval,
|
|
381
|
+
]);
|
|
382
|
+
|
|
383
|
+
const allStaticPresets = [
|
|
384
|
+
{
|
|
385
|
+
id: "yesterday",
|
|
386
|
+
label: "Yesterday",
|
|
387
|
+
getValue: () => {
|
|
388
|
+
const d = subDays(new Date(), 1);
|
|
389
|
+
return { from: d, to: d };
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
id: "today",
|
|
394
|
+
label: "Today",
|
|
395
|
+
getValue: () => ({ from: new Date(), to: new Date() }),
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
id: "tomorrow",
|
|
399
|
+
label: "Tomorrow",
|
|
400
|
+
getValue: () => {
|
|
401
|
+
const d = addDays(new Date(), 1);
|
|
402
|
+
return { from: d, to: d };
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
id: "last-week",
|
|
407
|
+
label: "Last Week",
|
|
408
|
+
getValue: () => {
|
|
409
|
+
const d = subDays(new Date(), 7);
|
|
410
|
+
return { from: startOfWeek(d), to: endOfWeek(d) };
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
id: "this-week",
|
|
415
|
+
label: "This Week",
|
|
416
|
+
getValue: () => ({
|
|
417
|
+
from: startOfWeek(new Date()),
|
|
418
|
+
to: endOfWeek(new Date()),
|
|
419
|
+
}),
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
id: "next-week",
|
|
423
|
+
label: "Next Week",
|
|
424
|
+
getValue: () => {
|
|
425
|
+
const d = addDays(new Date(), 7);
|
|
426
|
+
return { from: startOfWeek(d), to: endOfWeek(d) };
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
id: "last-month",
|
|
431
|
+
label: "Last Month",
|
|
432
|
+
getValue: () => {
|
|
433
|
+
const d = subMonths(new Date(), 1);
|
|
434
|
+
return { from: startOfMonth(d), to: endOfMonth(d) };
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
id: "this-month",
|
|
439
|
+
label: "This Month",
|
|
440
|
+
getValue: () => ({
|
|
441
|
+
from: startOfMonth(new Date()),
|
|
442
|
+
to: endOfMonth(new Date()),
|
|
443
|
+
}),
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
id: "next-month",
|
|
447
|
+
label: "Next Month",
|
|
448
|
+
getValue: () => {
|
|
449
|
+
const d = addMonths(new Date(), 1);
|
|
450
|
+
return { from: startOfMonth(d), to: endOfMonth(d) };
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
id: "last-year",
|
|
455
|
+
label: "Last Year",
|
|
456
|
+
getValue: () => {
|
|
457
|
+
const d = subYears(new Date(), 1);
|
|
458
|
+
return { from: startOfYear(d), to: endOfYear(d) };
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
id: "this-year",
|
|
463
|
+
label: "This Year",
|
|
464
|
+
getValue: () => ({
|
|
465
|
+
from: startOfYear(new Date()),
|
|
466
|
+
to: endOfYear(new Date()),
|
|
467
|
+
}),
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
id: "next-year",
|
|
471
|
+
label: "Next Year",
|
|
472
|
+
getValue: () => {
|
|
473
|
+
const d = addYears(new Date(), 1);
|
|
474
|
+
return { from: startOfYear(d), to: endOfYear(d) };
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
];
|
|
478
|
+
|
|
479
|
+
const presets = useMemo(() => {
|
|
480
|
+
if (!enabledPresets) return [];
|
|
481
|
+
return enabledPresets
|
|
482
|
+
.map(id => {
|
|
483
|
+
const staticMatch = allStaticPresets.find(p => p.id === id);
|
|
484
|
+
if (staticMatch) return staticMatch;
|
|
485
|
+
const parts = id.split("-");
|
|
486
|
+
if (
|
|
487
|
+
parts.length === 3 &&
|
|
488
|
+
(parts[0] === "last" || parts[0] === "next")
|
|
489
|
+
) {
|
|
490
|
+
const isPast = parts[0] === "last";
|
|
491
|
+
const val = parseInt(parts[1]);
|
|
492
|
+
const unit = parts[2];
|
|
493
|
+
const today = new Date();
|
|
494
|
+
if (!isNaN(val)) {
|
|
495
|
+
if (unit === "weeks") {
|
|
496
|
+
const capped = Math.min(val, 52);
|
|
497
|
+
return {
|
|
498
|
+
id,
|
|
499
|
+
label: `${isPast ? "Last" : "Next"} ${capped} Weeks`,
|
|
500
|
+
getValue: () =>
|
|
501
|
+
isPast
|
|
502
|
+
? { from: subDays(today, capped * 7), to: today }
|
|
503
|
+
: { from: today, to: addDays(today, capped * 7) },
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
if (unit === "months") {
|
|
507
|
+
const capped = Math.min(val, 12);
|
|
508
|
+
return {
|
|
509
|
+
id,
|
|
510
|
+
label: `${isPast ? "Last" : "Next"} ${capped} Months`,
|
|
511
|
+
getValue: () =>
|
|
512
|
+
isPast
|
|
513
|
+
? { from: subMonths(today, capped), to: today }
|
|
514
|
+
: { from: today, to: addMonths(today, capped) },
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
if (unit === "years") {
|
|
518
|
+
const capped = Math.min(val, 10);
|
|
519
|
+
return {
|
|
520
|
+
id,
|
|
521
|
+
label: `${isPast ? "Last" : "Next"} ${capped} Years`,
|
|
522
|
+
getValue: () =>
|
|
523
|
+
isPast
|
|
524
|
+
? { from: subYears(today, capped), to: today }
|
|
525
|
+
: { from: today, to: addYears(today, capped) },
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return null;
|
|
531
|
+
})
|
|
532
|
+
.filter(p => p !== null) as {
|
|
533
|
+
id: string;
|
|
534
|
+
label: string;
|
|
535
|
+
getValue: () => DateRange;
|
|
536
|
+
}[];
|
|
537
|
+
}, [enabledPresets]);
|
|
538
|
+
|
|
539
|
+
const showSidebar = presets.length > 0;
|
|
540
|
+
|
|
541
|
+
const isPresetActive = (p: { getValue: () => DateRange }) => {
|
|
542
|
+
const pRange = p.getValue();
|
|
543
|
+
if (!range.from || !pRange.from || !range.to || !pRange.to) return false;
|
|
544
|
+
return isSameDay(range.from, pRange.from) && isSameDay(range.to, pRange.to);
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
const handleDateSelect = (date: Date) => {
|
|
548
|
+
if (activeBox === "from") {
|
|
549
|
+
setRange({
|
|
550
|
+
from: date,
|
|
551
|
+
to: range.to && isAfterDate(date, range.to) ? undefined : range.to,
|
|
552
|
+
});
|
|
553
|
+
setActiveBox("to");
|
|
554
|
+
} else {
|
|
555
|
+
if (range.from && isBeforeDate(date, range.from)) {
|
|
556
|
+
setRange({ from: date, to: range.from });
|
|
557
|
+
} else {
|
|
558
|
+
setRange({ ...range, to: date });
|
|
559
|
+
}
|
|
560
|
+
setActiveBox("from");
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const handleBoxClick = (box: "from" | "to") => {
|
|
565
|
+
setActiveBox(box);
|
|
566
|
+
const dateToView = box === "from" ? range.from : range.to;
|
|
567
|
+
if (dateToView) setViewDate(dateToView);
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
const isInRange = (date: Date) => {
|
|
571
|
+
if (range.from && range.to) {
|
|
572
|
+
const start = isBeforeDate(range.from, range.to) ? range.from : range.to;
|
|
573
|
+
const end = isBeforeDate(range.from, range.to) ? range.to : range.from;
|
|
574
|
+
return (
|
|
575
|
+
(isAfterDate(date, start) && isBeforeDate(date, end)) ||
|
|
576
|
+
isSameDay(date, start) ||
|
|
577
|
+
isSameDay(date, end)
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
if (range.from && hoverDate) {
|
|
581
|
+
const start = isBeforeDate(hoverDate, range.from)
|
|
582
|
+
? hoverDate
|
|
583
|
+
: range.from;
|
|
584
|
+
const end = isBeforeDate(hoverDate, range.from) ? range.from : hoverDate;
|
|
585
|
+
return (
|
|
586
|
+
(isAfterDate(date, start) && isBeforeDate(date, end)) ||
|
|
587
|
+
isSameDay(date, start) ||
|
|
588
|
+
isSameDay(date, end)
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
return false;
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
const months = [
|
|
595
|
+
"Jan",
|
|
596
|
+
"Feb",
|
|
597
|
+
"Mar",
|
|
598
|
+
"Apr",
|
|
599
|
+
"May",
|
|
600
|
+
"Jun",
|
|
601
|
+
"Jul",
|
|
602
|
+
"Aug",
|
|
603
|
+
"Sep",
|
|
604
|
+
"Oct",
|
|
605
|
+
"Nov",
|
|
606
|
+
"Dec",
|
|
607
|
+
];
|
|
608
|
+
const years = Array.from(
|
|
609
|
+
{ length: maxYear - minYear + 1 },
|
|
610
|
+
(_, i) => minYear + i,
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
const monthOptions = useMemo(
|
|
614
|
+
() =>
|
|
615
|
+
months.map((m, i) => ({
|
|
616
|
+
id: m,
|
|
617
|
+
label: m,
|
|
618
|
+
key: i.toString(),
|
|
619
|
+
})),
|
|
620
|
+
[months],
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
const yearOptions = useMemo(
|
|
624
|
+
() =>
|
|
625
|
+
years.map(y => ({
|
|
626
|
+
id: y.toString(),
|
|
627
|
+
label: y.toString(),
|
|
628
|
+
key: y.toString(),
|
|
629
|
+
})),
|
|
630
|
+
[years],
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
const getDisplayText = () => {
|
|
634
|
+
if (range.from) {
|
|
635
|
+
if (range.to && isSameDay(range.from, range.to))
|
|
636
|
+
return format(range.from, formatStr || "dd/mm/yyyy");
|
|
637
|
+
|
|
638
|
+
const isDiffYear =
|
|
639
|
+
range.to && range.from.getFullYear() !== range.to.getFullYear();
|
|
640
|
+
const shortF = formatStr || "MMM d";
|
|
641
|
+
|
|
642
|
+
if (isDiffYear && range.to) {
|
|
643
|
+
return `${format(range.from, "MMM d, yyyy")} - ${format(range.to, "MMM d, yyyy")}`;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const yearF = formatStr ? "" : format(range.to || range.from, ", yyyy");
|
|
647
|
+
return `${format(range.from, shortF)} - ${range.to ? format(range.to, shortF) : "..."}${yearF}`;
|
|
648
|
+
}
|
|
649
|
+
if (placeholder) return placeholder;
|
|
650
|
+
if (defaultToToday) {
|
|
651
|
+
const today = new Date();
|
|
652
|
+
return `${format(today, "MMM d")} - ${format(today, "MMM d, yyyy")}`;
|
|
653
|
+
}
|
|
654
|
+
return "";
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
const isSideLabel = labelPlacement === "left" || labelPlacement === "right";
|
|
658
|
+
const xAlignment =
|
|
659
|
+
labelAlignX ||
|
|
660
|
+
(labelPlacement === "left"
|
|
661
|
+
? "left"
|
|
662
|
+
: labelPlacement === "right"
|
|
663
|
+
? "right"
|
|
664
|
+
: "left");
|
|
665
|
+
const yAlignmentClass =
|
|
666
|
+
labelAlignY === "top"
|
|
667
|
+
? "items-start"
|
|
668
|
+
: labelAlignY === "bottom"
|
|
669
|
+
? "items-end"
|
|
670
|
+
: "items-center";
|
|
671
|
+
|
|
672
|
+
const selectionRadius =
|
|
673
|
+
variant === "square"
|
|
674
|
+
? "rounded-none"
|
|
675
|
+
: variant === "curved"
|
|
676
|
+
? "rounded-lg"
|
|
677
|
+
: "rounded-full";
|
|
678
|
+
|
|
679
|
+
const borderRadius =
|
|
680
|
+
variant === "square"
|
|
681
|
+
? "rounded-none"
|
|
682
|
+
: variant === "curved"
|
|
683
|
+
? "rounded-lg"
|
|
684
|
+
: "rounded-full";
|
|
685
|
+
|
|
686
|
+
return (
|
|
687
|
+
<div
|
|
688
|
+
className={cn(
|
|
689
|
+
"flex w-full",
|
|
690
|
+
labelPlacement === "top" && "flex-col gap-1.5",
|
|
691
|
+
labelPlacement === "left" && cn("flex-row gap-4", yAlignmentClass),
|
|
692
|
+
labelPlacement === "right" &&
|
|
693
|
+
cn("flex-row-reverse gap-4", yAlignmentClass),
|
|
694
|
+
className,
|
|
695
|
+
)}
|
|
696
|
+
>
|
|
697
|
+
{label && (
|
|
698
|
+
<div
|
|
699
|
+
className={cn(
|
|
700
|
+
"flex flex-col",
|
|
701
|
+
isSideLabel ? "shrink-0" : "w-full",
|
|
702
|
+
labelAlignY === "top" && isSideLabel && "mt-2.5",
|
|
703
|
+
)}
|
|
704
|
+
>
|
|
705
|
+
<div
|
|
706
|
+
className={cn(
|
|
707
|
+
isSideLabel ? labelWidth : "w-full",
|
|
708
|
+
"flex flex-col",
|
|
709
|
+
xAlignment === "left" && "items-start text-left",
|
|
710
|
+
xAlignment === "right" && "items-end text-right",
|
|
711
|
+
xAlignment === "center" && "items-center text-center",
|
|
712
|
+
)}
|
|
713
|
+
>
|
|
714
|
+
<span className="text-sm font-medium text-black">{label}</span>
|
|
715
|
+
{description && (
|
|
716
|
+
<span className="text-[11px] text-black font-medium mt-0.5">
|
|
717
|
+
{description}
|
|
718
|
+
</span>
|
|
719
|
+
)}
|
|
720
|
+
</div>
|
|
721
|
+
</div>
|
|
722
|
+
)}
|
|
723
|
+
|
|
724
|
+
<div className="flex-1 relative group">
|
|
725
|
+
{isTypeable ? (
|
|
726
|
+
<div className="relative flex items-center">
|
|
727
|
+
<input
|
|
728
|
+
ref={inputRef}
|
|
729
|
+
type="text"
|
|
730
|
+
value={getFullMaskedValue()}
|
|
731
|
+
onChange={handleInputChange}
|
|
732
|
+
onKeyDown={handleKeyDown}
|
|
733
|
+
onFocus={handleFocus}
|
|
734
|
+
onBlur={() => setIsFocused(false)}
|
|
735
|
+
placeholder={`${typeableFormat.toLowerCase()} to ${typeableFormat.toLowerCase()}`}
|
|
736
|
+
className={cn(
|
|
737
|
+
"flex items-center w-full pl-10 pr-2 h-9 border-[1.5px] border-black transition-all duration-200 bg-white text-md text-black outline-none placeholder:text-black/40 placeholder:text-sm placeholder:font-medium font-medium",
|
|
738
|
+
borderRadius,
|
|
739
|
+
isFocused
|
|
740
|
+
? "border-sky-500 ring-4 ring-sky-500/10 shadow-lg"
|
|
741
|
+
: "hover:border-gray-800",
|
|
742
|
+
error && "border-red-500 ring-4 ring-red-500/10 text-red-500",
|
|
743
|
+
)}
|
|
744
|
+
/>
|
|
745
|
+
<CalendarIcon
|
|
746
|
+
size={16}
|
|
747
|
+
className={cn(
|
|
748
|
+
"absolute left-3 transition-colors",
|
|
749
|
+
error ? "text-red-500" : "text-black",
|
|
750
|
+
)}
|
|
751
|
+
/>
|
|
752
|
+
</div>
|
|
753
|
+
) : (
|
|
754
|
+
<button
|
|
755
|
+
type="button"
|
|
756
|
+
ref={refs.setReference}
|
|
757
|
+
{...getReferenceProps()}
|
|
758
|
+
onFocus={() => setIsFocused(true)}
|
|
759
|
+
onBlur={() => setIsFocused(false)}
|
|
760
|
+
className={cn(
|
|
761
|
+
"flex items-center pr-2 w-full h-9 border-[1.5px] border-black transition-all duration-200 bg-white font-medium text-black cursor-pointer",
|
|
762
|
+
borderRadius,
|
|
763
|
+
isOpen
|
|
764
|
+
? "border-sky-500 ring-4 ring-sky-500/10"
|
|
765
|
+
: "hover:border-gray-800",
|
|
766
|
+
error && "border-red-500 ring-4 ring-red-500/10",
|
|
767
|
+
)}
|
|
768
|
+
>
|
|
769
|
+
<div className="flex items-center pl-2.25 pr-2 shrink-0">
|
|
770
|
+
<CalendarIcon size={16} className="text-black" />
|
|
771
|
+
</div>
|
|
772
|
+
<span
|
|
773
|
+
className={cn(
|
|
774
|
+
"text-md flex-1 text-left truncate text-black",
|
|
775
|
+
!range.from && !defaultToToday && "text-gray-400",
|
|
776
|
+
)}
|
|
777
|
+
>
|
|
778
|
+
{range.from ? getDisplayText() : placeholder || ""}
|
|
779
|
+
</span>
|
|
780
|
+
<ChevronDown
|
|
781
|
+
size={14}
|
|
782
|
+
className={cn(
|
|
783
|
+
"text-black transition-transform duration-200",
|
|
784
|
+
isOpen && "rotate-180",
|
|
785
|
+
)}
|
|
786
|
+
/>
|
|
787
|
+
</button>
|
|
788
|
+
)}
|
|
789
|
+
|
|
790
|
+
{isOpen && !isTypeable && (
|
|
791
|
+
<FloatingPortal>
|
|
792
|
+
<FloatingFocusManager context={context} modal={false}>
|
|
793
|
+
<div
|
|
794
|
+
ref={refs.setFloating}
|
|
795
|
+
style={floatingStyles}
|
|
796
|
+
{...getFloatingProps()}
|
|
797
|
+
className="z-[9999] flex bg-white border-[1.5px] border-black rounded-xl shadow-xl animate-in fade-in duration-200 max-w-[95vw] overflow-hidden"
|
|
798
|
+
>
|
|
799
|
+
{/* Sidebar Panel */}
|
|
800
|
+
{showSidebar && isPanelVisible && (
|
|
801
|
+
<div className="w-44 shrink-0 border-r border-black bg-black flex flex-col min-h-0">
|
|
802
|
+
<div className="flex items-center justify-between p-4 pb-2 border-b border-white/50">
|
|
803
|
+
<span className="text-xs font-semibold text-white uppercase">
|
|
804
|
+
Presets
|
|
805
|
+
</span>
|
|
806
|
+
</div>
|
|
807
|
+
<div className="flex-1 overflow-y-auto custom-scrollbar p-2.5 flex flex-col gap-0.5">
|
|
808
|
+
{presets.map(p => {
|
|
809
|
+
const active = isPresetActive(p);
|
|
810
|
+
return (
|
|
811
|
+
<button
|
|
812
|
+
key={p.id}
|
|
813
|
+
onClick={() => {
|
|
814
|
+
const r = p.getValue();
|
|
815
|
+
setRange(r);
|
|
816
|
+
if (r.from) setViewDate(r.from);
|
|
817
|
+
setActiveBox("from");
|
|
818
|
+
}}
|
|
819
|
+
className={cn(
|
|
820
|
+
"px-3 py-2 text-xs font-semibold text-left rounded-lg transition-all cursor-pointer truncate",
|
|
821
|
+
active
|
|
822
|
+
? "bg-white text-black z-10"
|
|
823
|
+
: "text-white hover:bg-white/15 ",
|
|
824
|
+
)}
|
|
825
|
+
>
|
|
826
|
+
{p.label}
|
|
827
|
+
</button>
|
|
828
|
+
);
|
|
829
|
+
})}
|
|
830
|
+
</div>
|
|
831
|
+
</div>
|
|
832
|
+
)}
|
|
833
|
+
|
|
834
|
+
{/* Main Content */}
|
|
835
|
+
<div
|
|
836
|
+
className={cn(
|
|
837
|
+
"flex flex-col min-w-[340px] flex-1 min-h-0 bg-white",
|
|
838
|
+
)}
|
|
839
|
+
>
|
|
840
|
+
<div className="flex items-center gap-2 p-4 pb-0">
|
|
841
|
+
{showSidebar && (
|
|
842
|
+
<button
|
|
843
|
+
onClick={() => setIsPanelVisible(!isPanelVisible)}
|
|
844
|
+
className="p-1.5 bg-black/10 hover:bg-sky-500/10 hover:text-sky-500 rounded-lg text-black cursor-pointer transition-colors shrink-0"
|
|
845
|
+
>
|
|
846
|
+
{isPanelVisible ? (
|
|
847
|
+
<PanelLeftClose size={16} />
|
|
848
|
+
) : (
|
|
849
|
+
<PanelLeft size={16} />
|
|
850
|
+
)}
|
|
851
|
+
</button>
|
|
852
|
+
)}
|
|
853
|
+
<button
|
|
854
|
+
onClick={() => handleBoxClick("from")}
|
|
855
|
+
className={cn(
|
|
856
|
+
"flex-1 p-1.5 border-[1.5px] transition-all rounded-lg text-center font-bold uppercase text-[11px] cursor-pointer truncate",
|
|
857
|
+
activeBox === "from"
|
|
858
|
+
? "border-sky-500 bg-sky-500/10 text-sky-600"
|
|
859
|
+
: "border-black hover:border-gray-800 text-black",
|
|
860
|
+
)}
|
|
861
|
+
>
|
|
862
|
+
{range.from
|
|
863
|
+
? format(range.from, formatStr || "dd/mm/yyyy")
|
|
864
|
+
: "START"}
|
|
865
|
+
</button>
|
|
866
|
+
<button
|
|
867
|
+
onClick={() => handleBoxClick("to")}
|
|
868
|
+
className={cn(
|
|
869
|
+
"flex-1 p-1.5 border-[1.5px] transition-all rounded-lg text-center font-bold uppercase text-[11px] cursor-pointer truncate",
|
|
870
|
+
activeBox === "to"
|
|
871
|
+
? "border-sky-500 bg-sky-500/10 text-sky-600"
|
|
872
|
+
: "border-black hover:border-gray-800 text-black",
|
|
873
|
+
)}
|
|
874
|
+
>
|
|
875
|
+
{range.to
|
|
876
|
+
? format(range.to, formatStr || "dd/mm/yyyy")
|
|
877
|
+
: "END"}
|
|
878
|
+
</button>
|
|
879
|
+
</div>
|
|
880
|
+
|
|
881
|
+
<div className="flex-1 overflow-y-auto custom-scrollbar">
|
|
882
|
+
<div className="flex items-center justify-between p-4">
|
|
883
|
+
<button
|
|
884
|
+
onClick={e => {
|
|
885
|
+
e.stopPropagation();
|
|
886
|
+
setViewDate(subMonths(viewDate, 1));
|
|
887
|
+
}}
|
|
888
|
+
className="h-9 w-9 flex items-center justify-center rounded-lg cursor-pointer text-black bg-black/10 hover:bg-sky-500/10 hover:text-sky-500 transition-all duration-150"
|
|
889
|
+
>
|
|
890
|
+
<ChevronLeft size={16} strokeWidth={2.5} />
|
|
891
|
+
</button>
|
|
892
|
+
<div className="flex items-center gap-2">
|
|
893
|
+
<SelectInput
|
|
894
|
+
options={monthOptions}
|
|
895
|
+
value={viewDate.getMonth().toString()}
|
|
896
|
+
onChange={key => {
|
|
897
|
+
setViewDate(
|
|
898
|
+
new Date(
|
|
899
|
+
viewDate.getFullYear(),
|
|
900
|
+
parseInt(key),
|
|
901
|
+
1,
|
|
902
|
+
),
|
|
903
|
+
);
|
|
904
|
+
}}
|
|
905
|
+
width="w-24"
|
|
906
|
+
/>
|
|
907
|
+
<SelectInput
|
|
908
|
+
options={yearOptions}
|
|
909
|
+
value={viewDate.getFullYear().toString()}
|
|
910
|
+
onChange={key => {
|
|
911
|
+
setViewDate(
|
|
912
|
+
new Date(parseInt(key), viewDate.getMonth(), 1),
|
|
913
|
+
);
|
|
914
|
+
}}
|
|
915
|
+
width="w-24"
|
|
916
|
+
/>
|
|
917
|
+
</div>
|
|
918
|
+
<button
|
|
919
|
+
onClick={e => {
|
|
920
|
+
e.stopPropagation();
|
|
921
|
+
setViewDate(addMonths(viewDate, 1));
|
|
922
|
+
}}
|
|
923
|
+
className="h-9 w-9 flex items-center justify-center rounded-lg cursor-pointer text-black bg-black/10 hover:bg-sky-500/10 hover:text-sky-500 transition-all duration-150"
|
|
924
|
+
>
|
|
925
|
+
<ChevronRight size={16} strokeWidth={2.5} />
|
|
926
|
+
</button>
|
|
927
|
+
</div>
|
|
928
|
+
|
|
929
|
+
<div className="px-4 pb-4">
|
|
930
|
+
<div className="grid grid-cols-7 gap-px">
|
|
931
|
+
{["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"].map(d => (
|
|
932
|
+
<div
|
|
933
|
+
key={d}
|
|
934
|
+
className="h-8 flex items-center justify-center text-xs font-semibold text-black uppercase"
|
|
935
|
+
>
|
|
936
|
+
{d}
|
|
937
|
+
</div>
|
|
938
|
+
))}
|
|
939
|
+
</div>
|
|
940
|
+
<div className="grid grid-cols-7">
|
|
941
|
+
{calendarDays.map((date: Date, i: number) => {
|
|
942
|
+
const active = isInRange(date);
|
|
943
|
+
const isStart =
|
|
944
|
+
range.from && isSameDay(date, range.from);
|
|
945
|
+
const isEnd = range.to && isSameDay(date, range.to);
|
|
946
|
+
const isOutside =
|
|
947
|
+
format(date, "M") !== format(viewDate, "M");
|
|
948
|
+
const disabled = isDateDisabled(date);
|
|
949
|
+
const isToday = isSameDay(date, new Date());
|
|
950
|
+
|
|
951
|
+
return (
|
|
952
|
+
<div
|
|
953
|
+
key={i}
|
|
954
|
+
className="h-9 relative flex items-center justify-center"
|
|
955
|
+
onMouseEnter={() =>
|
|
956
|
+
!range.to && setHoverDate(date)
|
|
957
|
+
}
|
|
958
|
+
onMouseLeave={() => setHoverDate(undefined)}
|
|
959
|
+
>
|
|
960
|
+
{active && !isOutside && (
|
|
961
|
+
<div
|
|
962
|
+
className={cn(
|
|
963
|
+
"absolute inset-0 bg-black/10 z-0",
|
|
964
|
+
isStart &&
|
|
965
|
+
cn(
|
|
966
|
+
selectionRadius === "rounded-full"
|
|
967
|
+
? "rounded-l-full"
|
|
968
|
+
: selectionRadius === "rounded-lg"
|
|
969
|
+
? "rounded-l-lg"
|
|
970
|
+
: "rounded-none",
|
|
971
|
+
"ml-1",
|
|
972
|
+
),
|
|
973
|
+
isEnd &&
|
|
974
|
+
cn(
|
|
975
|
+
selectionRadius === "rounded-full"
|
|
976
|
+
? "rounded-r-full"
|
|
977
|
+
: selectionRadius === "rounded-lg"
|
|
978
|
+
? "rounded-r-lg"
|
|
979
|
+
: "rounded-none",
|
|
980
|
+
"mr-1",
|
|
981
|
+
),
|
|
982
|
+
!isStart && !isEnd && "mx-0",
|
|
983
|
+
)}
|
|
984
|
+
/>
|
|
985
|
+
)}
|
|
986
|
+
<button
|
|
987
|
+
disabled={disabled}
|
|
988
|
+
onClick={() => handleDateSelect(date)}
|
|
989
|
+
className={cn(
|
|
990
|
+
"w-7 h-7 text-xs font-semibold relative z-10 transition-all",
|
|
991
|
+
selectionRadius,
|
|
992
|
+
isStart || isEnd
|
|
993
|
+
? "bg-black text-white shadow-lg scale-110"
|
|
994
|
+
: !isOutside
|
|
995
|
+
? "text-black hover:bg-black/10"
|
|
996
|
+
: "text-black/50",
|
|
997
|
+
isToday &&
|
|
998
|
+
!(isStart || isEnd) &&
|
|
999
|
+
"bg-sky-500/10 text-sky-500",
|
|
1000
|
+
disabled
|
|
1001
|
+
? "opacity-20 cursor-not-allowed"
|
|
1002
|
+
: "cursor-pointer",
|
|
1003
|
+
)}
|
|
1004
|
+
>
|
|
1005
|
+
{format(date, "d")}
|
|
1006
|
+
</button>
|
|
1007
|
+
</div>
|
|
1008
|
+
);
|
|
1009
|
+
})}
|
|
1010
|
+
</div>
|
|
1011
|
+
</div>
|
|
1012
|
+
</div>
|
|
1013
|
+
|
|
1014
|
+
<div className="shrink-0 p-4 pt-0 flex items-center justify-end gap-3 bg-white z-50">
|
|
1015
|
+
<button
|
|
1016
|
+
onClick={() => setIsOpen(false)}
|
|
1017
|
+
className="px-4 py-2 text-[12px] font-semibold uppercase text-black/50 hover:text-black hover:bg-black/10 rounded-full cursor-pointer"
|
|
1018
|
+
>
|
|
1019
|
+
Cancel
|
|
1020
|
+
</button>
|
|
1021
|
+
<button
|
|
1022
|
+
onClick={() => {
|
|
1023
|
+
onChange?.(range);
|
|
1024
|
+
setIsOpen(false);
|
|
1025
|
+
}}
|
|
1026
|
+
className="px-8 py-2 bg-black text-white rounded-full text-[12px] font-semibold uppercase transition-all cursor-pointer "
|
|
1027
|
+
>
|
|
1028
|
+
Apply
|
|
1029
|
+
</button>
|
|
1030
|
+
</div>
|
|
1031
|
+
</div>
|
|
1032
|
+
</div>
|
|
1033
|
+
</FloatingFocusManager>
|
|
1034
|
+
</FloatingPortal>
|
|
1035
|
+
)}
|
|
1036
|
+
</div>
|
|
1037
|
+
</div>
|
|
1038
|
+
);
|
|
1039
|
+
};
|