@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.
- package/.checksum +1 -0
- package/README.md +46 -0
- package/components.json +20 -0
- package/eslint.config.mjs +20 -0
- package/jsr.json +10 -0
- package/package.json +120 -0
- package/postcss.config.mjs +8 -0
- package/rollup.config.js +40 -0
- package/src/components/ui/accordion.tsx +70 -0
- package/src/components/ui/alert-dialog.tsx +156 -0
- package/src/components/ui/alert.tsx +58 -0
- package/src/components/ui/aspect-ratio.tsx +11 -0
- package/src/components/ui/avatar.tsx +52 -0
- package/src/components/ui/badge.tsx +49 -0
- package/src/components/ui/breadcrumb.tsx +108 -0
- package/src/components/ui/button.tsx +61 -0
- package/src/components/ui/calendar.tsx +212 -0
- package/src/components/ui/card.tsx +74 -0
- package/src/components/ui/carousel.tsx +240 -0
- package/src/components/ui/chart.tsx +365 -0
- package/src/components/ui/checkbox.tsx +31 -0
- package/src/components/ui/codeblock.tsx +161 -0
- package/src/components/ui/collapsible.tsx +33 -0
- package/src/components/ui/color-picker.tsx +143 -0
- package/src/components/ui/command.tsx +176 -0
- package/src/components/ui/context-menu.tsx +251 -0
- package/src/components/ui/custom/autosize-textarea.tsx +111 -0
- package/src/components/ui/custom/calendar/core.tsx +61 -0
- package/src/components/ui/custom/calendar/day-cell.tsx +74 -0
- package/src/components/ui/custom/calendar/month-header.tsx +59 -0
- package/src/components/ui/custom/calendar/month-view.tsx +110 -0
- package/src/components/ui/custom/calendar/utils.ts +76 -0
- package/src/components/ui/custom/calendar/year-calendar.tsx +64 -0
- package/src/components/ui/custom/calendar/year-view.tsx +58 -0
- package/src/components/ui/custom/combobox.tsx +197 -0
- package/src/components/ui/custom/common-footer.tsx +215 -0
- package/src/components/ui/custom/compared-date-range-picker.tsx +561 -0
- package/src/components/ui/custom/date-input.tsx +279 -0
- package/src/components/ui/custom/empty-card.tsx +39 -0
- package/src/components/ui/custom/feature-summary.tsx +135 -0
- package/src/components/ui/custom/file-uploader.tsx +349 -0
- package/src/components/ui/custom/input-field.tsx +29 -0
- package/src/components/ui/custom/loading-indicator.tsx +28 -0
- package/src/components/ui/custom/modifiable-dialog-trigger.tsx +83 -0
- package/src/components/ui/custom/month-picker.tsx +157 -0
- package/src/components/ui/custom/report-preview.tsx +175 -0
- package/src/components/ui/custom/search-bar.tsx +56 -0
- package/src/components/ui/custom/select-field.tsx +78 -0
- package/src/components/ui/custom/tables/data-table-column-header.tsx +72 -0
- package/src/components/ui/custom/tables/data-table-create-button.tsx +31 -0
- package/src/components/ui/custom/tables/data-table-faceted-filter.tsx +142 -0
- package/src/components/ui/custom/tables/data-table-pagination.tsx +243 -0
- package/src/components/ui/custom/tables/data-table-refresh-button.tsx +45 -0
- package/src/components/ui/custom/tables/data-table-toolbar.tsx +133 -0
- package/src/components/ui/custom/tables/data-table-view-options.tsx +112 -0
- package/src/components/ui/custom/tables/data-table.tsx +228 -0
- package/src/components/ui/custom/uploaded-files-card.tsx +50 -0
- package/src/components/ui/dialog.tsx +137 -0
- package/src/components/ui/drawer.tsx +131 -0
- package/src/components/ui/dropdown-menu.tsx +256 -0
- package/src/components/ui/form.tsx +167 -0
- package/src/components/ui/hover-card.tsx +41 -0
- package/src/components/ui/icons.tsx +506 -0
- package/src/components/ui/input-otp.tsx +78 -0
- package/src/components/ui/input.tsx +18 -0
- package/src/components/ui/label.tsx +23 -0
- package/src/components/ui/markdown.tsx +7 -0
- package/src/components/ui/menubar.tsx +275 -0
- package/src/components/ui/navigation-menu.tsx +169 -0
- package/src/components/ui/pagination.tsx +126 -0
- package/src/components/ui/popover.tsx +47 -0
- package/src/components/ui/progress.tsx +30 -0
- package/src/components/ui/radio-group.tsx +44 -0
- package/src/components/ui/resizable.tsx +55 -0
- package/src/components/ui/scroll-area.tsx +57 -0
- package/src/components/ui/select.tsx +180 -0
- package/src/components/ui/separator.tsx +27 -0
- package/src/components/ui/sheet.tsx +138 -0
- package/src/components/ui/sidebar.tsx +734 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +62 -0
- package/src/components/ui/sonner.tsx +29 -0
- package/src/components/ui/switch.tsx +30 -0
- package/src/components/ui/table.tsx +112 -0
- package/src/components/ui/tabs.tsx +68 -0
- package/src/components/ui/tag-input.tsx +141 -0
- package/src/components/ui/textarea.tsx +17 -0
- package/src/components/ui/time-picker-input.tsx +117 -0
- package/src/components/ui/time-picker-utils.tsx +146 -0
- package/src/components/ui/toast.tsx +128 -0
- package/src/components/ui/toaster.tsx +35 -0
- package/src/components/ui/toggle-group.tsx +72 -0
- package/src/components/ui/toggle.tsx +46 -0
- package/src/components/ui/tooltip.tsx +60 -0
- package/src/globals.css +252 -0
- package/src/hooks/use-callback-ref.ts +28 -0
- package/src/hooks/use-controllable-state.ts +68 -0
- package/src/hooks/use-copy-to-clipboard.ts +46 -0
- package/src/hooks/use-form.ts +23 -0
- package/src/hooks/use-forwarded-ref.ts +17 -0
- package/src/hooks/use-mobile.tsx +21 -0
- package/src/hooks/use-toast.ts +191 -0
- package/src/resolvers.ts +3 -0
- 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
|
+
};
|