@moontra/moonui-pro 2.5.14 → 2.6.1

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,280 +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)
110
- const [isMobile, setIsMobile] = React.useState(false)
111
-
112
- React.useEffect(() => {
113
- const checkMobile = () => setIsMobile(window.innerWidth < 640)
114
- checkMobile()
115
- window.addEventListener('resize', checkMobile)
116
- return () => window.removeEventListener('resize', checkMobile)
117
- }, [])
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);
118
124
 
119
- const currentRange = value || PRESET_RANGES[2].getRange() // Default: Last 7 days
125
+ const currentRange = value || PRESET_RANGES[2].getRange(); // Default: Last 7 days
120
126
 
121
- // Format tarih aralığı
122
- const formatRange = (range: TimeRange) => {
123
- if (range.preset && range.preset !== 'custom') {
124
- return range.label
125
- }
126
- return `${format(range.start, 'MMM d')} - ${format(range.end, 'MMM d, yyyy')}`
127
- }
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
+ };
128
134
 
129
- // Preset seçimi
130
- const handlePresetSelect = (preset: typeof PRESET_RANGES[0]) => {
131
- const range = preset.getRange()
132
- onChange?.(range)
133
- setIsOpen(false)
134
- }
135
+ // Preset seçimi
136
+ const handlePresetSelect = (preset: (typeof PRESET_RANGES)[0]) => {
137
+ const range = preset.getRange();
138
+ onChange?.(range);
139
+ setIsOpen(false);
140
+ };
135
141
 
136
- // Custom tarih seçimi
137
- const handleCustomRangeSelect = () => {
138
- if (customRange.from && customRange.to) {
139
- onChange?.({
140
- start: startOfDay(customRange.from),
141
- end: endOfDay(customRange.to),
142
- label: `${format(customRange.from, 'MMM d')} - ${format(customRange.to, 'MMM d, yyyy')}`,
143
- preset: 'custom'
144
- })
145
- setIsOpen(false)
146
- }
147
- }
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
+ };
148
154
 
149
- return (
150
- <Popover open={isOpen} onOpenChange={setIsOpen}>
151
- <PopoverTrigger asChild>
152
- <Button
153
- variant="outline"
154
- className={cn(
155
- "justify-start text-left font-normal",
156
- glassmorphism && "bg-background/60 backdrop-blur-sm border-white/10",
157
- !value && "text-muted-foreground",
158
- className
159
- )}
160
- >
161
- <CalendarIcon className="mr-2 h-4 w-4" />
162
- {formatRange(currentRange)}
163
- <ChevronDown className="ml-auto h-4 w-4 opacity-50" />
164
- </Button>
165
- </PopoverTrigger>
166
- <PopoverContent
167
- className={cn(
168
- "time-range-popover w-auto max-w-[95vw] p-0 overflow-hidden",
169
- glassmorphism && "bg-background/95 backdrop-blur-md border-white/10"
170
- )}
171
- align="end"
172
- side="bottom"
173
- sideOffset={4}
174
- collisionPadding={8}
175
- >
176
- <motion.div
177
- initial={{ opacity: 0, y: -10 }}
178
- animate={{ opacity: 1, y: 0 }}
179
- transition={{ duration: 0.2 }}
180
- >
181
- <div className="flex flex-col sm:flex-row">
182
- {/* Preset'ler */}
183
- {showPresets && (
184
- <div className="preset-column border-b sm:border-b-0 sm:border-r p-3 min-w-[140px]">
185
- <div className="flex items-center gap-2 px-2 pb-2">
186
- <Clock className="h-4 w-4 text-muted-foreground" />
187
- <h4 className="text-sm font-medium">Quick Select</h4>
188
- </div>
189
- <div className="space-y-1">
190
- {PRESET_RANGES.map((preset) => (
191
- <motion.button
192
- key={preset.value}
193
- whileHover={{ x: 2 }}
194
- whileTap={{ scale: 0.98 }}
195
- onClick={() => handlePresetSelect(preset)}
196
- className={cn(
197
- "w-full text-left px-2 py-1.5 text-sm rounded-md transition-colors whitespace-nowrap",
198
- "hover:bg-muted",
199
- currentRange.preset === preset.value && "bg-primary text-primary-foreground"
200
- )}
201
- >
202
- {preset.label}
203
- </motion.button>
204
- ))}
205
- </div>
206
-
207
- {/* Karşılaştırma */}
208
- {showComparison && (
209
- <div className="mt-4 pt-4 border-t">
210
- <label className="flex items-center gap-2 px-2 cursor-pointer">
211
- <input
212
- type="checkbox"
213
- checked={comparisonEnabled}
214
- onChange={(e) => setComparisonEnabled(e.target.checked)}
215
- className="rounded"
216
- />
217
- <span className="text-sm">Compare to previous period</span>
218
- </label>
219
- </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"
220
178
  )}
221
- </div>
222
- )}
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
+ )}
223
266
 
224
- {/* Custom tarih seçici */}
225
- <div className="p-3">
226
- <div className="flex items-center gap-2 px-2 pb-2">
227
- <CalendarIcon className="h-4 w-4 text-muted-foreground" />
228
- <h4 className="text-sm font-medium">Custom Range</h4>
229
- </div>
230
-
231
- <div className="overflow-x-auto">
232
- <Calendar
233
- mode="range"
234
- selected={{
235
- from: customRange.from,
236
- to: customRange.to
237
- }}
238
- onSelect={(range: any) => setCustomRange(range || {})}
239
- numberOfMonths={isMobile ? 1 : 2}
240
- className="rounded-md"
241
- />
242
- </div>
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>
243
289
 
244
- <div className="flex items-center justify-between pt-3 px-2">
245
- <div className="text-sm text-muted-foreground">
246
- {customRange.from && customRange.to && (
247
- <span>
248
- {Math.ceil((customRange.to.getTime() - customRange.from.getTime()) / (1000 * 60 * 60 * 24))} days selected
249
- </span>
250
- )}
251
- </div>
252
- <div className="flex gap-2">
253
- <Button
254
- variant="ghost"
255
- size="sm"
256
- onClick={() => {
257
- setCustomRange({})
258
- setIsOpen(false)
259
- }}
260
- >
261
- Cancel
262
- </Button>
263
- <Button
264
- size="sm"
265
- onClick={handleCustomRangeSelect}
266
- disabled={!customRange.from || !customRange.to}
267
- >
268
- Apply
269
- </Button>
270
- </div>
271
- </div>
272
- </div>
273
- </div>
274
- </motion.div>
275
- </PopoverContent>
276
- </Popover>
277
- )
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
+ );
278
334
  }
279
335
 
280
- export default TimeRangePicker
336
+ export default TimeRangePicker;