@tuturuuu/ui 0.0.4

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.
Files changed (104) hide show
  1. package/.checksum +1 -0
  2. package/README.md +46 -0
  3. package/components.json +20 -0
  4. package/eslint.config.mjs +20 -0
  5. package/jsr.json +10 -0
  6. package/package.json +120 -0
  7. package/postcss.config.mjs +8 -0
  8. package/rollup.config.js +40 -0
  9. package/src/components/ui/accordion.tsx +70 -0
  10. package/src/components/ui/alert-dialog.tsx +156 -0
  11. package/src/components/ui/alert.tsx +58 -0
  12. package/src/components/ui/aspect-ratio.tsx +11 -0
  13. package/src/components/ui/avatar.tsx +52 -0
  14. package/src/components/ui/badge.tsx +49 -0
  15. package/src/components/ui/breadcrumb.tsx +108 -0
  16. package/src/components/ui/button.tsx +61 -0
  17. package/src/components/ui/calendar.tsx +212 -0
  18. package/src/components/ui/card.tsx +74 -0
  19. package/src/components/ui/carousel.tsx +240 -0
  20. package/src/components/ui/chart.tsx +365 -0
  21. package/src/components/ui/checkbox.tsx +31 -0
  22. package/src/components/ui/codeblock.tsx +161 -0
  23. package/src/components/ui/collapsible.tsx +33 -0
  24. package/src/components/ui/color-picker.tsx +143 -0
  25. package/src/components/ui/command.tsx +176 -0
  26. package/src/components/ui/context-menu.tsx +251 -0
  27. package/src/components/ui/custom/autosize-textarea.tsx +111 -0
  28. package/src/components/ui/custom/calendar/core.tsx +61 -0
  29. package/src/components/ui/custom/calendar/day-cell.tsx +74 -0
  30. package/src/components/ui/custom/calendar/month-header.tsx +59 -0
  31. package/src/components/ui/custom/calendar/month-view.tsx +110 -0
  32. package/src/components/ui/custom/calendar/utils.ts +76 -0
  33. package/src/components/ui/custom/calendar/year-calendar.tsx +64 -0
  34. package/src/components/ui/custom/calendar/year-view.tsx +58 -0
  35. package/src/components/ui/custom/combobox.tsx +197 -0
  36. package/src/components/ui/custom/common-footer.tsx +215 -0
  37. package/src/components/ui/custom/compared-date-range-picker.tsx +561 -0
  38. package/src/components/ui/custom/date-input.tsx +279 -0
  39. package/src/components/ui/custom/empty-card.tsx +39 -0
  40. package/src/components/ui/custom/feature-summary.tsx +135 -0
  41. package/src/components/ui/custom/file-uploader.tsx +349 -0
  42. package/src/components/ui/custom/input-field.tsx +29 -0
  43. package/src/components/ui/custom/loading-indicator.tsx +28 -0
  44. package/src/components/ui/custom/modifiable-dialog-trigger.tsx +83 -0
  45. package/src/components/ui/custom/month-picker.tsx +157 -0
  46. package/src/components/ui/custom/report-preview.tsx +175 -0
  47. package/src/components/ui/custom/search-bar.tsx +56 -0
  48. package/src/components/ui/custom/select-field.tsx +78 -0
  49. package/src/components/ui/custom/tables/data-table-column-header.tsx +72 -0
  50. package/src/components/ui/custom/tables/data-table-create-button.tsx +31 -0
  51. package/src/components/ui/custom/tables/data-table-faceted-filter.tsx +142 -0
  52. package/src/components/ui/custom/tables/data-table-pagination.tsx +243 -0
  53. package/src/components/ui/custom/tables/data-table-refresh-button.tsx +45 -0
  54. package/src/components/ui/custom/tables/data-table-toolbar.tsx +133 -0
  55. package/src/components/ui/custom/tables/data-table-view-options.tsx +112 -0
  56. package/src/components/ui/custom/tables/data-table.tsx +228 -0
  57. package/src/components/ui/custom/uploaded-files-card.tsx +50 -0
  58. package/src/components/ui/dialog.tsx +137 -0
  59. package/src/components/ui/drawer.tsx +131 -0
  60. package/src/components/ui/dropdown-menu.tsx +256 -0
  61. package/src/components/ui/form.tsx +167 -0
  62. package/src/components/ui/hover-card.tsx +41 -0
  63. package/src/components/ui/icons.tsx +506 -0
  64. package/src/components/ui/input-otp.tsx +78 -0
  65. package/src/components/ui/input.tsx +18 -0
  66. package/src/components/ui/label.tsx +23 -0
  67. package/src/components/ui/markdown.tsx +7 -0
  68. package/src/components/ui/menubar.tsx +275 -0
  69. package/src/components/ui/navigation-menu.tsx +169 -0
  70. package/src/components/ui/pagination.tsx +126 -0
  71. package/src/components/ui/popover.tsx +47 -0
  72. package/src/components/ui/progress.tsx +30 -0
  73. package/src/components/ui/radio-group.tsx +44 -0
  74. package/src/components/ui/resizable.tsx +55 -0
  75. package/src/components/ui/scroll-area.tsx +57 -0
  76. package/src/components/ui/select.tsx +180 -0
  77. package/src/components/ui/separator.tsx +27 -0
  78. package/src/components/ui/sheet.tsx +138 -0
  79. package/src/components/ui/sidebar.tsx +734 -0
  80. package/src/components/ui/skeleton.tsx +13 -0
  81. package/src/components/ui/slider.tsx +62 -0
  82. package/src/components/ui/sonner.tsx +29 -0
  83. package/src/components/ui/switch.tsx +30 -0
  84. package/src/components/ui/table.tsx +112 -0
  85. package/src/components/ui/tabs.tsx +68 -0
  86. package/src/components/ui/tag-input.tsx +141 -0
  87. package/src/components/ui/textarea.tsx +17 -0
  88. package/src/components/ui/time-picker-input.tsx +117 -0
  89. package/src/components/ui/time-picker-utils.tsx +146 -0
  90. package/src/components/ui/toast.tsx +128 -0
  91. package/src/components/ui/toaster.tsx +35 -0
  92. package/src/components/ui/toggle-group.tsx +72 -0
  93. package/src/components/ui/toggle.tsx +46 -0
  94. package/src/components/ui/tooltip.tsx +60 -0
  95. package/src/globals.css +252 -0
  96. package/src/hooks/use-callback-ref.ts +28 -0
  97. package/src/hooks/use-controllable-state.ts +68 -0
  98. package/src/hooks/use-copy-to-clipboard.ts +46 -0
  99. package/src/hooks/use-form.ts +23 -0
  100. package/src/hooks/use-forwarded-ref.ts +17 -0
  101. package/src/hooks/use-mobile.tsx +21 -0
  102. package/src/hooks/use-toast.ts +191 -0
  103. package/src/resolvers.ts +3 -0
  104. package/tsconfig.json +17 -0
@@ -0,0 +1,561 @@
1
+ 'use client';
2
+
3
+ import { Button } from '../button';
4
+ import { Calendar } from '../calendar';
5
+ import { Label } from '../label';
6
+ import { Popover, PopoverContent, PopoverTrigger } from '../popover';
7
+ import {
8
+ Select,
9
+ SelectContent,
10
+ SelectItem,
11
+ SelectTrigger,
12
+ SelectValue,
13
+ } from '../select';
14
+ import { Switch } from '../switch';
15
+ import { DateInput } from './date-input';
16
+ import { cn } from '@tuturuuu/utils/format';
17
+ import { Check, ChevronDown, ChevronUp } from 'lucide-react';
18
+ import { useEffect, useRef, useState } from 'react';
19
+
20
+ interface ComparedDateRangePickerProps {
21
+ /** Click handler for applying the updates from DateRangePicker. */
22
+ // eslint-disable-next-line no-unused-vars
23
+ onUpdate?: (values: { range: DateRange; rangeCompare?: DateRange }) => void;
24
+ /** Initial value for start date */
25
+ initialDateFrom?: Date | string;
26
+ /** Initial value for end date */
27
+ initialDateTo?: Date | string;
28
+ /** Initial value for start date for compare */
29
+ initialCompareFrom?: Date | string;
30
+ /** Initial value for end date for compare */
31
+ initialCompareTo?: Date | string;
32
+ /** Alignment of popover */
33
+ align?: 'start' | 'center' | 'end';
34
+ /** Option for locale */
35
+ locale?: string;
36
+ /** Option for showing compare feature */
37
+ showCompare?: boolean;
38
+ }
39
+
40
+ const formatDate = (date: Date, locale: string = 'en-us'): string => {
41
+ return date.toLocaleDateString(locale, {
42
+ month: 'short',
43
+ day: 'numeric',
44
+ year: 'numeric',
45
+ });
46
+ };
47
+
48
+ const getDateAdjustedForTimezone = (dateInput: Date | string): Date => {
49
+ if (typeof dateInput === 'string') {
50
+ // Split the date string to get year, month, and day parts
51
+ const parts = dateInput.split('-').map((part) => parseInt(part, 10));
52
+ // Create a new Date object using the local timezone
53
+ // Note: Month is 0-indexed, so subtract 1 from the month part
54
+ const date = new Date(parts[0]!, parts[1]! - 1, parts[2]);
55
+ return date;
56
+ } else {
57
+ // If dateInput is already a Date object, return it directly
58
+ return dateInput;
59
+ }
60
+ };
61
+
62
+ interface DateRange {
63
+ from: Date;
64
+ to: Date | undefined;
65
+ }
66
+
67
+ interface Preset {
68
+ name: string;
69
+ label: string;
70
+ }
71
+
72
+ // Define presets
73
+ const PRESETS: Preset[] = [
74
+ { name: 'today', label: 'Today' },
75
+ { name: 'yesterday', label: 'Yesterday' },
76
+ { name: 'last7', label: 'Last 7 days' },
77
+ { name: 'last14', label: 'Last 14 days' },
78
+ { name: 'last30', label: 'Last 30 days' },
79
+ { name: 'thisWeek', label: 'This Week' },
80
+ { name: 'lastWeek', label: 'Last Week' },
81
+ { name: 'thisMonth', label: 'This Month' },
82
+ { name: 'lastMonth', label: 'Last Month' },
83
+ ];
84
+
85
+ /** The DateRangePicker component allows a user to select a range of dates */
86
+ export const ComparedDateRangePicker = ({
87
+ initialDateFrom = new Date(new Date().setHours(0, 0, 0, 0)),
88
+ initialDateTo,
89
+ initialCompareFrom,
90
+ initialCompareTo,
91
+ onUpdate,
92
+ align = 'end',
93
+ locale = 'en-US',
94
+ showCompare = true,
95
+ }: ComparedDateRangePickerProps) => {
96
+ const [isOpen, setIsOpen] = useState(false);
97
+
98
+ const [range, setRange] = useState<DateRange>({
99
+ from: getDateAdjustedForTimezone(initialDateFrom),
100
+ to: initialDateTo
101
+ ? getDateAdjustedForTimezone(initialDateTo)
102
+ : getDateAdjustedForTimezone(initialDateFrom),
103
+ });
104
+ const [rangeCompare, setRangeCompare] = useState<DateRange | undefined>(
105
+ initialCompareFrom
106
+ ? {
107
+ from: new Date(new Date(initialCompareFrom).setHours(0, 0, 0, 0)),
108
+ to: initialCompareTo
109
+ ? new Date(new Date(initialCompareTo).setHours(0, 0, 0, 0))
110
+ : new Date(new Date(initialCompareFrom).setHours(0, 0, 0, 0)),
111
+ }
112
+ : undefined
113
+ );
114
+
115
+ // Refs to store the values of range and rangeCompare when the date picker is opened
116
+ const openedRangeRef = useRef<DateRange | undefined>(undefined);
117
+ const openedRangeCompareRef = useRef<DateRange | undefined>(undefined);
118
+
119
+ const [selectedPreset, setSelectedPreset] = useState<string | undefined>(
120
+ undefined
121
+ );
122
+
123
+ const [isSmallScreen, setIsSmallScreen] = useState(
124
+ typeof window !== 'undefined' ? window.innerWidth < 960 : false
125
+ );
126
+
127
+ useEffect(() => {
128
+ const handleResize = (): void => {
129
+ setIsSmallScreen(window.innerWidth < 960);
130
+ };
131
+
132
+ window.addEventListener('resize', handleResize);
133
+
134
+ // Clean up event listener on unmount
135
+ return () => {
136
+ window.removeEventListener('resize', handleResize);
137
+ };
138
+ }, []);
139
+
140
+ const getPresetRange = (presetName: string): DateRange => {
141
+ const preset = PRESETS.find(({ name }) => name === presetName);
142
+ if (!preset) throw new Error(`Unknown date range preset: ${presetName}`);
143
+ const from = new Date();
144
+ const to = new Date();
145
+ const first = from.getDate() - from.getDay();
146
+
147
+ switch (preset.name) {
148
+ case 'today':
149
+ from.setHours(0, 0, 0, 0);
150
+ to.setHours(23, 59, 59, 999);
151
+ break;
152
+ case 'yesterday':
153
+ from.setDate(from.getDate() - 1);
154
+ from.setHours(0, 0, 0, 0);
155
+ to.setDate(to.getDate() - 1);
156
+ to.setHours(23, 59, 59, 999);
157
+ break;
158
+ case 'last7':
159
+ from.setDate(from.getDate() - 6);
160
+ from.setHours(0, 0, 0, 0);
161
+ to.setHours(23, 59, 59, 999);
162
+ break;
163
+ case 'last14':
164
+ from.setDate(from.getDate() - 13);
165
+ from.setHours(0, 0, 0, 0);
166
+ to.setHours(23, 59, 59, 999);
167
+ break;
168
+ case 'last30':
169
+ from.setDate(from.getDate() - 29);
170
+ from.setHours(0, 0, 0, 0);
171
+ to.setHours(23, 59, 59, 999);
172
+ break;
173
+ case 'thisWeek':
174
+ from.setDate(first);
175
+ from.setHours(0, 0, 0, 0);
176
+ to.setHours(23, 59, 59, 999);
177
+ break;
178
+ case 'lastWeek':
179
+ from.setDate(from.getDate() - 7 - from.getDay());
180
+ to.setDate(to.getDate() - to.getDay() - 1);
181
+ from.setHours(0, 0, 0, 0);
182
+ to.setHours(23, 59, 59, 999);
183
+ break;
184
+ case 'thisMonth':
185
+ from.setDate(1);
186
+ from.setHours(0, 0, 0, 0);
187
+ to.setHours(23, 59, 59, 999);
188
+ break;
189
+ case 'lastMonth':
190
+ from.setMonth(from.getMonth() - 1);
191
+ from.setDate(1);
192
+ from.setHours(0, 0, 0, 0);
193
+ to.setDate(0);
194
+ to.setHours(23, 59, 59, 999);
195
+ break;
196
+ }
197
+
198
+ return { from, to };
199
+ };
200
+
201
+ const setPreset = (preset: string): void => {
202
+ const range = getPresetRange(preset);
203
+ setRange(range);
204
+ if (rangeCompare) {
205
+ const rangeCompare = {
206
+ from: new Date(
207
+ range.from.getFullYear() - 1,
208
+ range.from.getMonth(),
209
+ range.from.getDate()
210
+ ),
211
+ to: range.to
212
+ ? new Date(
213
+ range.to.getFullYear() - 1,
214
+ range.to.getMonth(),
215
+ range.to.getDate()
216
+ )
217
+ : undefined,
218
+ };
219
+ setRangeCompare(rangeCompare);
220
+ }
221
+ };
222
+
223
+ const checkPreset = (): void => {
224
+ for (const preset of PRESETS) {
225
+ const presetRange = getPresetRange(preset.name);
226
+
227
+ const normalizedRangeFrom = new Date(range.from);
228
+ normalizedRangeFrom.setHours(0, 0, 0, 0);
229
+ const normalizedPresetFrom = new Date(
230
+ presetRange.from.setHours(0, 0, 0, 0)
231
+ );
232
+
233
+ const normalizedRangeTo = new Date(range.to ?? 0);
234
+ normalizedRangeTo.setHours(0, 0, 0, 0);
235
+ const normalizedPresetTo = new Date(
236
+ presetRange.to?.setHours(0, 0, 0, 0) ?? 0
237
+ );
238
+
239
+ if (
240
+ normalizedRangeFrom.getTime() === normalizedPresetFrom.getTime() &&
241
+ normalizedRangeTo.getTime() === normalizedPresetTo.getTime()
242
+ ) {
243
+ setSelectedPreset(preset.name);
244
+ return;
245
+ }
246
+ }
247
+
248
+ setSelectedPreset(undefined);
249
+ };
250
+
251
+ const resetValues = (): void => {
252
+ setRange({
253
+ from:
254
+ typeof initialDateFrom === 'string'
255
+ ? getDateAdjustedForTimezone(initialDateFrom)
256
+ : initialDateFrom,
257
+ to: initialDateTo
258
+ ? typeof initialDateTo === 'string'
259
+ ? getDateAdjustedForTimezone(initialDateTo)
260
+ : initialDateTo
261
+ : typeof initialDateFrom === 'string'
262
+ ? getDateAdjustedForTimezone(initialDateFrom)
263
+ : initialDateFrom,
264
+ });
265
+ setRangeCompare(
266
+ initialCompareFrom
267
+ ? {
268
+ from:
269
+ typeof initialCompareFrom === 'string'
270
+ ? getDateAdjustedForTimezone(initialCompareFrom)
271
+ : initialCompareFrom,
272
+ to: initialCompareTo
273
+ ? typeof initialCompareTo === 'string'
274
+ ? getDateAdjustedForTimezone(initialCompareTo)
275
+ : initialCompareTo
276
+ : typeof initialCompareFrom === 'string'
277
+ ? getDateAdjustedForTimezone(initialCompareFrom)
278
+ : initialCompareFrom,
279
+ }
280
+ : undefined
281
+ );
282
+ };
283
+
284
+ useEffect(() => {
285
+ checkPreset();
286
+ }, [range]);
287
+
288
+ const PresetButton = ({
289
+ preset,
290
+ label,
291
+ isSelected,
292
+ }: {
293
+ preset: string;
294
+ label: string;
295
+ isSelected: boolean;
296
+ }): React.ReactNode => (
297
+ <Button
298
+ className={cn(isSelected && 'pointer-events-none')}
299
+ variant="ghost"
300
+ onClick={() => {
301
+ setPreset(preset);
302
+ }}
303
+ >
304
+ <>
305
+ <span className={cn('pr-2 opacity-0', isSelected && 'opacity-70')}>
306
+ <Check width={18} height={18} />
307
+ </span>
308
+ {label}
309
+ </>
310
+ </Button>
311
+ );
312
+
313
+ // Helper function to check if two date ranges are equal
314
+ const areRangesEqual = (a?: DateRange, b?: DateRange): boolean => {
315
+ if (!a || !b) return a === b; // If either is undefined, return true if both are undefined
316
+ return (
317
+ a.from.getTime() === b.from.getTime() &&
318
+ (!a.to || !b.to || a.to.getTime() === b.to.getTime())
319
+ );
320
+ };
321
+
322
+ useEffect(() => {
323
+ if (isOpen) {
324
+ openedRangeRef.current = range;
325
+ openedRangeCompareRef.current = rangeCompare;
326
+ }
327
+ }, [isOpen]);
328
+
329
+ return (
330
+ <Popover
331
+ modal={true}
332
+ open={isOpen}
333
+ onOpenChange={(open: boolean) => {
334
+ if (!open) {
335
+ resetValues();
336
+ }
337
+ setIsOpen(open);
338
+ }}
339
+ >
340
+ <PopoverTrigger asChild>
341
+ <Button size={'lg'} variant="outline">
342
+ <div className="text-right">
343
+ <div className="py-1">
344
+ <div>{`${formatDate(range.from, locale)}${
345
+ range.to != null ? ' - ' + formatDate(range.to, locale) : ''
346
+ }`}</div>
347
+ </div>
348
+ {rangeCompare != null && (
349
+ <div className="-mt-1 text-xs opacity-60">
350
+ <>
351
+ vs. {formatDate(rangeCompare.from, locale)}
352
+ {rangeCompare.to != null
353
+ ? ` - ${formatDate(rangeCompare.to, locale)}`
354
+ : ''}
355
+ </>
356
+ </div>
357
+ )}
358
+ </div>
359
+ <div className="-mr-2 scale-125 pl-1 opacity-60">
360
+ {isOpen ? <ChevronUp width={24} /> : <ChevronDown width={24} />}
361
+ </div>
362
+ </Button>
363
+ </PopoverTrigger>
364
+ <PopoverContent align={align} className="w-auto">
365
+ <div className="flex py-2">
366
+ <div className="flex">
367
+ <div className="flex flex-col">
368
+ <div className="flex flex-col items-center justify-end gap-2 px-3 pb-4 lg:flex-row lg:items-start lg:pb-0">
369
+ {showCompare && (
370
+ <div className="flex items-center space-x-2 py-1 pr-4">
371
+ <Switch
372
+ defaultChecked={Boolean(rangeCompare)}
373
+ onCheckedChange={(checked: boolean) => {
374
+ if (checked) {
375
+ if (!range.to) {
376
+ setRange({
377
+ from: range.from,
378
+ to: range.from,
379
+ });
380
+ }
381
+ setRangeCompare({
382
+ from: new Date(
383
+ range.from.getFullYear(),
384
+ range.from.getMonth(),
385
+ range.from.getDate() - 365
386
+ ),
387
+ to: range.to
388
+ ? new Date(
389
+ range.to.getFullYear() - 1,
390
+ range.to.getMonth(),
391
+ range.to.getDate()
392
+ )
393
+ : new Date(
394
+ range.from.getFullYear() - 1,
395
+ range.from.getMonth(),
396
+ range.from.getDate()
397
+ ),
398
+ });
399
+ } else {
400
+ setRangeCompare(undefined);
401
+ }
402
+ }}
403
+ id="compare-mode"
404
+ />
405
+ <Label htmlFor="compare-mode">Compare</Label>
406
+ </div>
407
+ )}
408
+ <div className="flex flex-col gap-2">
409
+ <div className="flex gap-2">
410
+ <DateInput
411
+ value={range.from}
412
+ onChange={(date) => {
413
+ const toDate =
414
+ range.to == null || date > range.to ? date : range.to;
415
+ setRange((prevRange) => ({
416
+ ...prevRange,
417
+ from: date,
418
+ to: toDate,
419
+ }));
420
+ }}
421
+ />
422
+ <div className="py-1">-</div>
423
+ <DateInput
424
+ value={range.to}
425
+ onChange={(date) => {
426
+ const fromDate = date < range.from ? date : range.from;
427
+ setRange((prevRange) => ({
428
+ ...prevRange,
429
+ from: fromDate,
430
+ to: date,
431
+ }));
432
+ }}
433
+ />
434
+ </div>
435
+ {rangeCompare != null && (
436
+ <div className="flex gap-2">
437
+ <DateInput
438
+ value={rangeCompare?.from}
439
+ onChange={(date) => {
440
+ if (rangeCompare) {
441
+ const compareToDate =
442
+ rangeCompare.to == null || date > rangeCompare.to
443
+ ? date
444
+ : rangeCompare.to;
445
+ setRangeCompare((prevRangeCompare) => ({
446
+ ...prevRangeCompare,
447
+ from: date,
448
+ to: compareToDate,
449
+ }));
450
+ } else {
451
+ setRangeCompare({
452
+ from: date,
453
+ to: new Date(),
454
+ });
455
+ }
456
+ }}
457
+ />
458
+ <div className="py-1">-</div>
459
+ <DateInput
460
+ value={rangeCompare?.to}
461
+ onChange={(date) => {
462
+ if (rangeCompare && rangeCompare.from) {
463
+ const compareFromDate =
464
+ date < rangeCompare.from
465
+ ? date
466
+ : rangeCompare.from;
467
+ setRangeCompare({
468
+ ...rangeCompare,
469
+ from: compareFromDate,
470
+ to: date,
471
+ });
472
+ }
473
+ }}
474
+ />
475
+ </div>
476
+ )}
477
+ </div>
478
+ </div>
479
+ {isSmallScreen && (
480
+ <Select
481
+ defaultValue={selectedPreset}
482
+ onValueChange={(value) => {
483
+ setPreset(value);
484
+ }}
485
+ >
486
+ <SelectTrigger className="mx-auto mb-2 w-[180px]">
487
+ <SelectValue placeholder="Select..." />
488
+ </SelectTrigger>
489
+ <SelectContent>
490
+ {PRESETS.map((preset) => (
491
+ <SelectItem key={preset.name} value={preset.name}>
492
+ {preset.label}
493
+ </SelectItem>
494
+ ))}
495
+ </SelectContent>
496
+ </Select>
497
+ )}
498
+ <div>
499
+ <Calendar
500
+ mode="range"
501
+ onSelect={(value: { from?: Date; to?: Date } | undefined) => {
502
+ if (value?.from != null) {
503
+ setRange({ from: value.from, to: value?.to });
504
+ }
505
+ }}
506
+ selected={range}
507
+ numberOfMonths={isSmallScreen ? 1 : 2}
508
+ defaultMonth={
509
+ new Date(
510
+ new Date().setMonth(
511
+ new Date().getMonth() - (isSmallScreen ? 0 : 1)
512
+ )
513
+ )
514
+ }
515
+ />
516
+ </div>
517
+ </div>
518
+ </div>
519
+ {!isSmallScreen && (
520
+ <div className="flex flex-col items-end gap-1 pr-2 pb-6 pl-6">
521
+ <div className="flex w-full flex-col items-end gap-1 pr-2 pb-6 pl-6">
522
+ {PRESETS.map((preset) => (
523
+ <PresetButton
524
+ key={preset.name}
525
+ preset={preset.name}
526
+ label={preset.label}
527
+ isSelected={selectedPreset === preset.name}
528
+ />
529
+ ))}
530
+ </div>
531
+ </div>
532
+ )}
533
+ </div>
534
+ <div className="flex justify-end gap-2 py-2 pr-4">
535
+ <Button
536
+ onClick={() => {
537
+ setIsOpen(false);
538
+ resetValues();
539
+ }}
540
+ variant="ghost"
541
+ >
542
+ Cancel
543
+ </Button>
544
+ <Button
545
+ onClick={() => {
546
+ setIsOpen(false);
547
+ if (
548
+ !areRangesEqual(range, openedRangeRef.current) ||
549
+ !areRangesEqual(rangeCompare, openedRangeCompareRef.current)
550
+ ) {
551
+ onUpdate?.({ range, rangeCompare });
552
+ }
553
+ }}
554
+ >
555
+ Update
556
+ </Button>
557
+ </div>
558
+ </PopoverContent>
559
+ </Popover>
560
+ );
561
+ };