@moontra/moonui-pro 2.5.13 → 2.6.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.
@@ -1,269 +1,336 @@
1
- "use client"
1
+ "use client";
2
2
 
3
- import React from 'react'
4
- import { motion } from 'framer-motion'
5
- import { Button } from '../ui/button'
6
- import { Calendar } from '../ui/calendar'
7
- import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'
8
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
9
- import { cn } from '../../lib/utils'
10
- import { CalendarIcon, Clock, ChevronDown } from 'lucide-react'
11
- import { format, startOfDay, endOfDay, subDays, startOfWeek, endOfWeek, startOfMonth, endOfMonth } from 'date-fns'
12
- import { TimeRange } from './types'
3
+ import React from "react";
4
+ import { motion } from "framer-motion";
5
+ import { Button } from "../ui/button";
6
+ import { Calendar } from "../ui/calendar";
7
+ import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
8
+ import { cn } from "../../lib/utils";
9
+ import { CalendarIcon, ChevronDown } from "lucide-react";
10
+ import {
11
+ format,
12
+ startOfDay,
13
+ endOfDay,
14
+ subDays,
15
+ startOfWeek,
16
+ endOfWeek,
17
+ startOfMonth,
18
+ endOfMonth,
19
+ } from "date-fns";
20
+ import { TimeRange } from "./types";
13
21
 
14
22
  interface TimeRangePickerProps {
15
- value?: TimeRange
16
- onChange?: (range: TimeRange) => void
17
- className?: string
18
- showPresets?: boolean
19
- showComparison?: boolean
20
- glassmorphism?: boolean
23
+ value?: TimeRange;
24
+ onChange?: (range: TimeRange) => void;
25
+ className?: string;
26
+ showPresets?: boolean;
27
+ showComparison?: boolean;
28
+ glassmorphism?: boolean;
21
29
  }
22
30
 
23
31
  const PRESET_RANGES = [
24
- {
25
- label: 'Today',
26
- value: 'today',
27
- getRange: () => ({
28
- start: startOfDay(new Date()),
29
- end: endOfDay(new Date()),
30
- label: 'Today',
31
- preset: 'today' as const
32
- })
33
- },
34
- {
35
- label: 'Yesterday',
36
- value: 'yesterday',
37
- getRange: () => ({
38
- start: startOfDay(subDays(new Date(), 1)),
39
- end: endOfDay(subDays(new Date(), 1)),
40
- label: 'Yesterday',
41
- preset: 'yesterday' as const
42
- })
43
- },
44
- {
45
- label: 'Last 7 days',
46
- value: 'last7days',
47
- getRange: () => ({
48
- start: startOfDay(subDays(new Date(), 6)),
49
- end: endOfDay(new Date()),
50
- label: 'Last 7 days',
51
- preset: 'last7days' as const
52
- })
53
- },
54
- {
55
- label: 'Last 30 days',
56
- value: 'last30days',
57
- getRange: () => ({
58
- start: startOfDay(subDays(new Date(), 29)),
59
- end: endOfDay(new Date()),
60
- label: 'Last 30 days',
61
- preset: 'last30days' as const
62
- })
63
- },
64
- {
65
- label: 'This week',
66
- value: 'thisWeek',
67
- getRange: () => ({
68
- start: startOfWeek(new Date()),
69
- end: endOfWeek(new Date()),
70
- label: 'This week',
71
- preset: 'custom' as const
72
- })
73
- },
74
- {
75
- label: 'This month',
76
- value: 'thisMonth',
77
- getRange: () => ({
78
- start: startOfMonth(new Date()),
79
- end: endOfMonth(new Date()),
80
- label: 'This month',
81
- preset: 'thisMonth' as const
82
- })
83
- },
84
- {
85
- label: 'Last month',
86
- value: 'lastMonth',
87
- getRange: () => {
88
- const lastMonth = subDays(startOfMonth(new Date()), 1)
89
- return {
90
- start: startOfMonth(lastMonth),
91
- end: endOfMonth(lastMonth),
92
- label: 'Last month',
93
- preset: 'lastMonth' as const
94
- }
95
- }
96
- }
97
- ]
32
+ {
33
+ label: "Today",
34
+ value: "today",
35
+ getRange: () => ({
36
+ start: startOfDay(new Date()),
37
+ end: endOfDay(new Date()),
38
+ label: "Today",
39
+ preset: "today" as const,
40
+ }),
41
+ },
42
+ {
43
+ label: "Yesterday",
44
+ value: "yesterday",
45
+ getRange: () => ({
46
+ start: startOfDay(subDays(new Date(), 1)),
47
+ end: endOfDay(subDays(new Date(), 1)),
48
+ label: "Yesterday",
49
+ preset: "yesterday" as const,
50
+ }),
51
+ },
52
+ {
53
+ label: "Last 7 days",
54
+ value: "last7days",
55
+ getRange: () => ({
56
+ start: startOfDay(subDays(new Date(), 6)),
57
+ end: endOfDay(new Date()),
58
+ label: "Last 7 days",
59
+ preset: "last7days" as const,
60
+ }),
61
+ },
62
+ {
63
+ label: "Last 30 days",
64
+ value: "last30days",
65
+ getRange: () => ({
66
+ start: startOfDay(subDays(new Date(), 29)),
67
+ end: endOfDay(new Date()),
68
+ label: "Last 30 days",
69
+ preset: "last30days" as const,
70
+ }),
71
+ },
72
+ {
73
+ label: "This week",
74
+ value: "thisWeek",
75
+ getRange: () => ({
76
+ start: startOfWeek(new Date()),
77
+ end: endOfWeek(new Date()),
78
+ label: "This week",
79
+ preset: "custom" as const,
80
+ }),
81
+ },
82
+ {
83
+ label: "This month",
84
+ value: "thisMonth",
85
+ getRange: () => ({
86
+ start: startOfMonth(new Date()),
87
+ end: endOfMonth(new Date()),
88
+ label: "This month",
89
+ preset: "thisMonth" as const,
90
+ }),
91
+ },
92
+ {
93
+ label: "Last month",
94
+ value: "lastMonth",
95
+ getRange: () => {
96
+ const lastMonth = subDays(startOfMonth(new Date()), 1);
97
+ return {
98
+ start: startOfMonth(lastMonth),
99
+ end: endOfMonth(lastMonth),
100
+ label: "Last month",
101
+ preset: "lastMonth" as const,
102
+ };
103
+ },
104
+ },
105
+ ];
98
106
 
99
107
  export function TimeRangePicker({
100
- value,
101
- onChange,
102
- className,
103
- showPresets = true,
104
- showComparison = false,
105
- glassmorphism = false
108
+ value,
109
+ onChange,
110
+ className,
111
+ showPresets = true,
112
+ showComparison = false,
113
+ glassmorphism = false,
106
114
  }: TimeRangePickerProps) {
107
- const [isOpen, setIsOpen] = React.useState(false)
108
- const [customRange, setCustomRange] = React.useState<{ from?: Date; to?: Date }>({})
109
- const [comparisonEnabled, setComparisonEnabled] = React.useState(false)
115
+ const [isOpen, setIsOpen] = React.useState(false);
116
+ const [customRange, setCustomRange] = React.useState<{
117
+ from?: Date;
118
+ to?: Date;
119
+ }>({
120
+ from: undefined,
121
+ to: undefined,
122
+ });
123
+ const [comparisonEnabled, setComparisonEnabled] = React.useState(false);
110
124
 
111
- const currentRange = value || PRESET_RANGES[2].getRange() // Default: Last 7 days
125
+ const currentRange = value || PRESET_RANGES[2].getRange(); // Default: Last 7 days
112
126
 
113
- // Format tarih aralığı
114
- const formatRange = (range: TimeRange) => {
115
- if (range.preset && range.preset !== 'custom') {
116
- return range.label
117
- }
118
- return `${format(range.start, 'MMM d')} - ${format(range.end, 'MMM d, yyyy')}`
119
- }
127
+ // Format tarih aralığı
128
+ const formatRange = (range: TimeRange) => {
129
+ if (range.preset && range.preset !== "custom") {
130
+ return range.label;
131
+ }
132
+ return `${format(range.start, "MMM d")} - ${format(range.end, "MMM d, yyyy")}`;
133
+ };
120
134
 
121
- // Preset seçimi
122
- const handlePresetSelect = (preset: typeof PRESET_RANGES[0]) => {
123
- const range = preset.getRange()
124
- onChange?.(range)
125
- setIsOpen(false)
126
- }
135
+ // Preset seçimi
136
+ const handlePresetSelect = (preset: (typeof PRESET_RANGES)[0]) => {
137
+ const range = preset.getRange();
138
+ onChange?.(range);
139
+ setIsOpen(false);
140
+ };
127
141
 
128
- // Custom tarih seçimi
129
- const handleCustomRangeSelect = () => {
130
- if (customRange.from && customRange.to) {
131
- onChange?.({
132
- start: startOfDay(customRange.from),
133
- end: endOfDay(customRange.to),
134
- label: `${format(customRange.from, 'MMM d')} - ${format(customRange.to, 'MMM d, yyyy')}`,
135
- preset: 'custom'
136
- })
137
- setIsOpen(false)
138
- }
139
- }
142
+ // Custom tarih seçimi
143
+ const handleCustomRangeSelect = () => {
144
+ if (customRange.from && customRange.to) {
145
+ onChange?.({
146
+ start: startOfDay(customRange.from),
147
+ end: endOfDay(customRange.to),
148
+ label: `${format(customRange.from, "MMM d")} - ${format(customRange.to, "MMM d, yyyy")}`,
149
+ preset: "custom",
150
+ });
151
+ setIsOpen(false);
152
+ }
153
+ };
140
154
 
141
- return (
142
- <Popover open={isOpen} onOpenChange={setIsOpen}>
143
- <PopoverTrigger asChild>
144
- <Button
145
- variant="outline"
146
- className={cn(
147
- "justify-start text-left font-normal",
148
- glassmorphism && "bg-background/60 backdrop-blur-sm border-white/10",
149
- !value && "text-muted-foreground",
150
- className
151
- )}
152
- >
153
- <CalendarIcon className="mr-2 h-4 w-4" />
154
- {formatRange(currentRange)}
155
- <ChevronDown className="ml-auto h-4 w-4 opacity-50" />
156
- </Button>
157
- </PopoverTrigger>
158
- <PopoverContent
159
- className={cn(
160
- "w-auto p-0",
161
- glassmorphism && "bg-background/95 backdrop-blur-md border-white/10"
162
- )}
163
- align="end"
164
- side="bottom"
165
- sideOffset={4}
166
- >
167
- <motion.div
168
- initial={{ opacity: 0, y: -10 }}
169
- animate={{ opacity: 1, y: 0 }}
170
- transition={{ duration: 0.2 }}
171
- >
172
- <div className="flex">
173
- {/* Preset'ler */}
174
- {showPresets && (
175
- <div className="border-r p-3">
176
- <div className="flex items-center gap-2 px-2 pb-2">
177
- <Clock className="h-4 w-4 text-muted-foreground" />
178
- <h4 className="text-sm font-medium">Quick Select</h4>
179
- </div>
180
- <div className="space-y-1">
181
- {PRESET_RANGES.map((preset) => (
182
- <motion.button
183
- key={preset.value}
184
- whileHover={{ x: 2 }}
185
- whileTap={{ scale: 0.98 }}
186
- onClick={() => handlePresetSelect(preset)}
187
- className={cn(
188
- "w-full text-left px-2 py-1.5 text-sm rounded-md transition-colors",
189
- "hover:bg-muted",
190
- currentRange.preset === preset.value && "bg-primary text-primary-foreground"
191
- )}
192
- >
193
- {preset.label}
194
- </motion.button>
195
- ))}
196
- </div>
197
-
198
- {/* Karşılaştırma */}
199
- {showComparison && (
200
- <div className="mt-4 pt-4 border-t">
201
- <label className="flex items-center gap-2 px-2 cursor-pointer">
202
- <input
203
- type="checkbox"
204
- checked={comparisonEnabled}
205
- onChange={(e) => setComparisonEnabled(e.target.checked)}
206
- className="rounded"
207
- />
208
- <span className="text-sm">Compare to previous period</span>
209
- </label>
210
- </div>
155
+ return (
156
+ <Popover open={isOpen} onOpenChange={setIsOpen}>
157
+ <PopoverTrigger asChild>
158
+ <Button
159
+ variant="outline"
160
+ className={cn(
161
+ "justify-start text-left font-normal",
162
+ glassmorphism &&
163
+ "bg-background/60 backdrop-blur-sm border-white/10",
164
+ !value && "text-muted-foreground",
165
+ className
166
+ )}
167
+ >
168
+ <CalendarIcon className="mr-2 h-4 w-4" />
169
+ {formatRange(currentRange)}
170
+ <ChevronDown className="ml-auto h-4 w-4 opacity-50" />
171
+ </Button>
172
+ </PopoverTrigger>
173
+ <PopoverContent
174
+ className={cn(
175
+ "time-range-popover w-auto max-w-[95vw] sm:max-w-4xl p-0 overflow-hidden",
176
+ glassmorphism &&
177
+ "bg-background/95 backdrop-blur-md border-white/10"
211
178
  )}
212
- </div>
213
- )}
179
+ align="end"
180
+ side="bottom"
181
+ sideOffset={4}
182
+ collisionPadding={16}
183
+ >
184
+ <motion.div
185
+ initial={{ opacity: 0, y: -10 }}
186
+ animate={{ opacity: 1, y: 0 }}
187
+ transition={{ duration: 0.2 }}
188
+ className="min-w-0"
189
+ >
190
+ {/* Mobile: Stacked Layout */}
191
+ <div className="sm:hidden">
192
+ {/* Custom tarih seçici */}
193
+ <div className="p-3">
194
+ {/* Karşılaştırma */}
195
+ {showComparison && (
196
+ <div className="mt-3 pt-3 border-t">
197
+ <label className="flex items-center gap-2 cursor-pointer">
198
+ <input
199
+ type="checkbox"
200
+ checked={comparisonEnabled}
201
+ onChange={(e) =>
202
+ setComparisonEnabled(
203
+ e.target.checked
204
+ )
205
+ }
206
+ className="rounded w-3 h-3"
207
+ />
208
+ <span className="text-xs">
209
+ Compare to previous
210
+ </span>
211
+ </label>
212
+ </div>
213
+ )}
214
+ </div>
215
+ </div>
216
+
217
+ {/* Desktop: Side by Side Layout */}
218
+ <div className="hidden sm:flex">
219
+ {/* Preset'ler */}
220
+ {showPresets && (
221
+ <div className="preset-column border-r p-3 min-w-[140px]">
222
+ <div className="grid grid-cols-1 gap-1">
223
+ {PRESET_RANGES.map((preset) => (
224
+ <motion.button
225
+ key={preset.value}
226
+ whileHover={{ x: 2 }}
227
+ whileTap={{ scale: 0.98 }}
228
+ onClick={() =>
229
+ handlePresetSelect(preset)
230
+ }
231
+ className={cn(
232
+ "w-full text-left px-2 py-1.5 text-sm rounded-md transition-colors whitespace-nowrap",
233
+ "hover:bg-muted",
234
+ currentRange.preset ===
235
+ preset.value &&
236
+ "bg-primary text-primary-foreground"
237
+ )}
238
+ >
239
+ {preset.label}
240
+ </motion.button>
241
+ ))}
242
+ </div>
243
+
244
+ {/* Karşılaştırma */}
245
+ {showComparison && (
246
+ <div className="mt-4 pt-4 border-t">
247
+ <label className="flex items-center gap-2 px-2 cursor-pointer">
248
+ <input
249
+ type="checkbox"
250
+ checked={comparisonEnabled}
251
+ onChange={(e) =>
252
+ setComparisonEnabled(
253
+ e.target.checked
254
+ )
255
+ }
256
+ className="rounded w-4 h-4"
257
+ />
258
+ <span className="text-sm">
259
+ Compare to previous period
260
+ </span>
261
+ </label>
262
+ </div>
263
+ )}
264
+ </div>
265
+ )}
214
266
 
215
- {/* Custom tarih seçici */}
216
- <div className="p-3">
217
- <div className="flex items-center gap-2 px-2 pb-2">
218
- <CalendarIcon className="h-4 w-4 text-muted-foreground" />
219
- <h4 className="text-sm font-medium">Custom Range</h4>
220
- </div>
221
-
222
- <Calendar
223
- mode="range"
224
- selected={{
225
- from: customRange.from,
226
- to: customRange.to
227
- }}
228
- onSelect={(range: any) => setCustomRange(range || {})}
229
- numberOfMonths={2}
230
- className="rounded-md"
231
- />
267
+ {/* Custom tarih seçici */}
268
+ <div className="p-3 flex-1">
269
+ <div className="overflow-x-auto">
270
+ <Calendar
271
+ mode="range"
272
+ selected={{
273
+ from: customRange.from,
274
+ to: customRange.to,
275
+ }}
276
+ onSelect={(range: any) =>
277
+ setCustomRange(
278
+ range || {
279
+ from: undefined,
280
+ to: undefined,
281
+ }
282
+ )
283
+ }
284
+ numberOfMonths={2}
285
+ showOutsideDays={true}
286
+ className="rounded-md"
287
+ />
288
+ </div>
232
289
 
233
- <div className="flex items-center justify-between pt-3 px-2">
234
- <div className="text-sm text-muted-foreground">
235
- {customRange.from && customRange.to && (
236
- <span>
237
- {Math.ceil((customRange.to.getTime() - customRange.from.getTime()) / (1000 * 60 * 60 * 24))} days selected
238
- </span>
239
- )}
240
- </div>
241
- <div className="flex gap-2">
242
- <Button
243
- variant="ghost"
244
- size="sm"
245
- onClick={() => {
246
- setCustomRange({})
247
- setIsOpen(false)
248
- }}
249
- >
250
- Cancel
251
- </Button>
252
- <Button
253
- size="sm"
254
- onClick={handleCustomRangeSelect}
255
- disabled={!customRange.from || !customRange.to}
256
- >
257
- Apply
258
- </Button>
259
- </div>
260
- </div>
261
- </div>
262
- </div>
263
- </motion.div>
264
- </PopoverContent>
265
- </Popover>
266
- )
290
+ <div className="flex items-center justify-between pt-3 px-2">
291
+ <div className="text-sm text-muted-foreground">
292
+ {customRange.from && customRange.to && (
293
+ <span>
294
+ {Math.ceil(
295
+ (customRange.to.getTime() -
296
+ customRange.from.getTime()) /
297
+ (1000 * 60 * 60 * 24)
298
+ )}{" "}
299
+ days selected
300
+ </span>
301
+ )}
302
+ </div>
303
+ <div className="flex gap-2">
304
+ <Button
305
+ variant="ghost"
306
+ size="sm"
307
+ onClick={() => {
308
+ setCustomRange({
309
+ from: undefined,
310
+ to: undefined,
311
+ });
312
+ setIsOpen(false);
313
+ }}
314
+ >
315
+ Cancel
316
+ </Button>
317
+ <Button
318
+ size="sm"
319
+ onClick={handleCustomRangeSelect}
320
+ disabled={
321
+ !customRange.from || !customRange.to
322
+ }
323
+ >
324
+ Apply
325
+ </Button>
326
+ </div>
327
+ </div>
328
+ </div>
329
+ </div>
330
+ </motion.div>
331
+ </PopoverContent>
332
+ </Popover>
333
+ );
267
334
  }
268
335
 
269
- export default TimeRangePicker
336
+ export default TimeRangePicker;