@moontra/moonui-pro 2.16.0 → 2.17.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.
@@ -0,0 +1,1556 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
5
+ import {
6
+ Calendar as CalendarIcon,
7
+ ChevronLeft,
8
+ ChevronRight,
9
+ Plus,
10
+ Search,
11
+ Filter,
12
+ MoreHorizontal,
13
+ Download,
14
+ Upload,
15
+ Settings,
16
+ X,
17
+ Clock,
18
+ MapPin,
19
+ Users,
20
+ Link,
21
+ Tag,
22
+ AlertCircle,
23
+ Check,
24
+ Edit,
25
+ Trash,
26
+ Copy,
27
+ Share2,
28
+ ExternalLink,
29
+ Repeat,
30
+ Bell,
31
+ Video,
32
+ Phone,
33
+ Mail,
34
+ FileText,
35
+ FileSpreadsheet,
36
+ FileCode,
37
+ User,
38
+ Briefcase,
39
+ Home,
40
+ Activity,
41
+ Cake,
42
+ Heart,
43
+ Star,
44
+ Flag,
45
+ Bookmark,
46
+ Archive,
47
+ Send,
48
+ Globe,
49
+ Zap,
50
+ Cpu,
51
+ Cloud,
52
+ Sun,
53
+ Moon,
54
+ Coffee,
55
+ Gift,
56
+ Plane,
57
+ Car,
58
+ Printer,
59
+ Music,
60
+ Camera,
61
+ Book,
62
+ Gamepad2,
63
+ Tv,
64
+ Bike,
65
+ Train,
66
+ Ship,
67
+ Rocket,
68
+ Mountain,
69
+ Trees,
70
+ CloudRain,
71
+ CloudSnow,
72
+ Sunrise,
73
+ Sunset,
74
+ Wind,
75
+ Droplets,
76
+ Thermometer,
77
+ Eye,
78
+ EyeOff,
79
+ Lock,
80
+ Unlock,
81
+ Shield,
82
+ ShieldAlert,
83
+ ShieldCheck,
84
+ CircleCheck,
85
+ CircleX,
86
+ CircleAlert,
87
+ Info,
88
+ HelpCircle,
89
+ FileImage,
90
+ FileVideo,
91
+ FileAudio,
92
+ FolderOpen,
93
+ Database,
94
+ Server,
95
+ Monitor,
96
+ Smartphone,
97
+ Tablet,
98
+ Watch,
99
+ Headphones,
100
+ Speaker,
101
+ Mic,
102
+ MicOff,
103
+ Volume2,
104
+ VolumeX,
105
+ Wifi,
106
+ WifiOff,
107
+ Bluetooth,
108
+ Battery,
109
+ BatteryLow,
110
+ Power,
111
+ Plug,
112
+ Lightbulb,
113
+ Flashlight,
114
+ Sparkles,
115
+ Stars,
116
+ Crown,
117
+ Trophy,
118
+ Medal,
119
+ Award,
120
+ Stamp,
121
+ Ticket,
122
+ Receipt,
123
+ CreditCard,
124
+ Wallet,
125
+ DollarSign,
126
+ Euro,
127
+ IndianRupee,
128
+ Bitcoin,
129
+ Coins,
130
+ PiggyBank,
131
+ Calculator,
132
+ BarChart,
133
+ LineChart,
134
+ PieChart,
135
+ TrendingUp,
136
+ TrendingDown,
137
+ Target,
138
+ Crosshair,
139
+ Compass,
140
+ Map,
141
+ Navigation,
142
+ Milestone,
143
+ Signpost,
144
+ Construction,
145
+ Hammer,
146
+ Wrench,
147
+ Paintbrush,
148
+ Palette as PaletteIcon,
149
+ Layers,
150
+ Layout,
151
+ Grid,
152
+ Columns,
153
+ Rows,
154
+ PanelLeft,
155
+ PanelRight,
156
+ PanelTop,
157
+ PanelBottom,
158
+ Sidebar,
159
+ Terminal,
160
+ Code,
161
+ CodeSquare,
162
+ Binary,
163
+ Braces,
164
+ Brackets,
165
+ Hash,
166
+ Bug,
167
+ GitBranch,
168
+ GitCommit,
169
+ GitMerge,
170
+ GitPullRequest,
171
+ Github,
172
+ Gitlab,
173
+ Package,
174
+ Box,
175
+ Archive as ArchiveIcon,
176
+ SendHorizontal,
177
+ Reply,
178
+ Forward,
179
+ Undo,
180
+ Redo,
181
+ RotateCw,
182
+ RotateCcw,
183
+ Shuffle,
184
+ Play,
185
+ Pause,
186
+ Square,
187
+ Circle,
188
+ Triangle,
189
+ Hexagon,
190
+ Diamond,
191
+ Gem,
192
+ Shapes,
193
+ Brush,
194
+ Eraser,
195
+ Pen,
196
+ PenTool,
197
+ Pencil,
198
+ Highlighter,
199
+ Type,
200
+ Bold,
201
+ Italic,
202
+ Underline,
203
+ Strikethrough,
204
+ AlignLeft,
205
+ AlignCenter,
206
+ AlignRight,
207
+ AlignJustify,
208
+ Indent,
209
+ Outdent,
210
+ ListOrdered,
211
+ ListTree,
212
+ ListChecks,
213
+ ListX,
214
+ CheckSquare,
215
+ Square as SquareIcon,
216
+ CircleDot,
217
+ CircleDashed,
218
+ CircleEllipsis,
219
+ CirclePlus,
220
+ CircleMinus,
221
+ CircleEqual,
222
+ CircleSlash,
223
+ CircleSlash2,
224
+ CircleOff,
225
+ CircleUser,
226
+ CircleUserRound,
227
+ CircleFadingPlus,
228
+ UserPlus,
229
+ UserMinus,
230
+ UserCheck,
231
+ UserX,
232
+ UsersRound,
233
+ Building,
234
+ Building2,
235
+ Warehouse,
236
+ Factory,
237
+ Store,
238
+ ShoppingBag,
239
+ Package2,
240
+ Truck,
241
+ Ship as ShipIcon,
242
+ Anchor,
243
+ Waves,
244
+ Fish,
245
+ Shell,
246
+ Bug as BugIcon,
247
+ Bird,
248
+ GraduationCap,
249
+ ClipboardList,
250
+ ShoppingCart
251
+ } from 'lucide-react'
252
+ import { cn } from '../../lib/utils'
253
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
254
+ import { Button } from '../ui/button'
255
+ import { Badge } from '../ui/badge'
256
+ import { Input } from '../ui/input'
257
+ import { Label } from '../ui/label'
258
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
259
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'
260
+ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '../ui/dialog'
261
+ import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'
262
+ import { Textarea } from '../ui/textarea'
263
+ import { Switch } from '../ui/switch'
264
+ import { Checkbox } from '../ui/checkbox'
265
+ import { RadioGroup, RadioGroupItem } from '../ui/radio-group'
266
+ import { Separator } from '../ui/separator'
267
+ import { ScrollArea } from '../ui/scroll-area'
268
+ import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'
269
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip'
270
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger } from '../ui/dropdown-menu'
271
+ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from '../ui/command'
272
+ import { motion, AnimatePresence, LayoutGroup, useMotionValue, useTransform, animate } from 'framer-motion'
273
+ import { format, startOfMonth, endOfMonth, startOfWeek, endOfWeek, addDays, addWeeks, addMonths, addYears, subMonths, subYears, isSameMonth, isSameDay, isToday, isBefore, isAfter, differenceInDays, differenceInWeeks, differenceInMonths, parseISO, startOfDay, endOfDay, isWithinInterval, eachDayOfInterval, getDay, setHours, setMinutes, startOfYear, endOfYear, eachMonthOfInterval, getDaysInMonth, getWeek, startOfISOWeek, endOfISOWeek, addHours, subHours, isSameHour, differenceInHours, differenceInMinutes, addMinutes, subMinutes, isSameMinute, setSeconds, setMilliseconds, subDays, subWeeks } from 'date-fns'
274
+ import { DragDropContext, Droppable, Draggable, DropResult, DraggableProvided, DraggableStateSnapshot, DroppableProvided } from 'react-beautiful-dnd'
275
+ import { Calendar as CalendarBase } from '../ui/calendar'
276
+ import { HexColorPicker } from 'react-colorful'
277
+
278
+ // Types
279
+ export interface CalendarEvent {
280
+ id: string
281
+ title: string
282
+ description?: string
283
+ start: Date
284
+ end: Date
285
+ allDay?: boolean
286
+ color?: string
287
+ category?: string
288
+ location?: string
289
+ attendees?: Array<{
290
+ id: string
291
+ name: string
292
+ email: string
293
+ avatar?: string
294
+ status?: 'accepted' | 'declined' | 'tentative' | 'pending'
295
+ }>
296
+ reminders?: Array<{
297
+ type: 'email' | 'notification' | 'sms'
298
+ before: number // minutes before event
299
+ }>
300
+ recurring?: {
301
+ pattern: 'daily' | 'weekly' | 'monthly' | 'yearly' | 'custom'
302
+ interval: number
303
+ endDate?: Date
304
+ endAfter?: number // number of occurrences
305
+ daysOfWeek?: number[] // 0-6 for weekly pattern
306
+ dayOfMonth?: number // for monthly pattern
307
+ monthOfYear?: number // for yearly pattern
308
+ exceptions?: Date[] // dates to skip
309
+ }
310
+ status?: 'confirmed' | 'tentative' | 'cancelled'
311
+ visibility?: 'public' | 'private' | 'confidential'
312
+ priority?: 'low' | 'medium' | 'high'
313
+ tags?: string[]
314
+ attachments?: Array<{
315
+ id: string
316
+ name: string
317
+ url: string
318
+ type: string
319
+ size: number
320
+ }>
321
+ meetingUrl?: string
322
+ phoneNumber?: string
323
+ reminder?: boolean
324
+ reminderTime?: number
325
+ notes?: string
326
+ createdAt?: Date
327
+ updatedAt?: Date
328
+ createdBy?: string
329
+ updatedBy?: string
330
+ }
331
+
332
+ export interface CalendarCategory {
333
+ id: string
334
+ name: string
335
+ color: string
336
+ icon?: React.ReactNode
337
+ }
338
+
339
+ export interface CalendarView {
340
+ id: string
341
+ name: string
342
+ type: 'day' | 'week' | 'month' | 'year' | 'agenda' | 'custom'
343
+ default?: boolean
344
+ config?: any
345
+ }
346
+
347
+ export interface CalendarProProps {
348
+ events?: CalendarEvent[]
349
+ categories?: CalendarCategory[]
350
+ views?: CalendarView[]
351
+ defaultView?: string
352
+ height?: string | number
353
+ className?: string
354
+ sidebarCollapsed?: boolean
355
+ onSidebarToggle?: (collapsed: boolean) => void
356
+ onEventClick?: (event: CalendarEvent, e: React.MouseEvent) => void
357
+ onEventCreate?: (event: Partial<CalendarEvent>) => void
358
+ onEventUpdate?: (event: CalendarEvent) => void
359
+ onEventDelete?: (eventId: string) => void
360
+ onEventDrop?: (eventId: string, start: Date, end: Date) => void
361
+ onDateSelect?: (date: Date) => void
362
+ onViewChange?: (viewId: string) => void
363
+ allowEventCreation?: boolean
364
+ allowEventDeletion?: boolean
365
+ allowEventDragging?: boolean
366
+ showWeekNumbers?: boolean
367
+ firstDayOfWeek?: 0 | 1 | 2 | 3 | 4 | 5 | 6
368
+ timeFormat?: '12h' | '24h'
369
+ locale?: string
370
+ workingHours?: { start: number; end: number }
371
+ nonWorkingDays?: number[]
372
+ holidays?: Array<{ date: Date; name: string }>
373
+ customEventRenderer?: (event: CalendarEvent, view: string) => React.ReactNode
374
+ customHeaderRenderer?: (date: Date, view: string) => React.ReactNode
375
+ customCellRenderer?: (date: Date, events: CalendarEvent[], view: string) => React.ReactNode
376
+ eventColors?: Record<string, string>
377
+ onExport?: (format: 'ics' | 'csv' | 'json', events: CalendarEvent[]) => void
378
+ onImport?: (file: File) => void
379
+ integrations?: {
380
+ google?: boolean
381
+ outlook?: boolean
382
+ apple?: boolean
383
+ }
384
+ theme?: 'light' | 'dark' | 'system'
385
+ }
386
+
387
+ // Default categories
388
+ const defaultCategories: CalendarCategory[] = [
389
+ { id: 'personal', name: 'Personal', color: '#3b82f6', icon: <User className="h-4 w-4" /> },
390
+ { id: 'work', name: 'Work', color: '#10b981', icon: <Briefcase className="h-4 w-4" /> },
391
+ { id: 'meeting', name: 'Meeting', color: '#f59e0b', icon: <Users className="h-4 w-4" /> },
392
+ { id: 'task', name: 'Task', color: '#8b5cf6', icon: <ClipboardList className="h-4 w-4" /> },
393
+ { id: 'reminder', name: 'Reminder', color: '#ef4444', icon: <Bell className="h-4 w-4" /> },
394
+ { id: 'holiday', name: 'Holiday', color: '#ec4899', icon: <CalendarIcon className="h-4 w-4" /> },
395
+ { id: 'birthday', name: 'Birthday', color: '#f472b6', icon: <Cake className="h-4 w-4" /> },
396
+ { id: 'other', name: 'Other', color: '#6b7280', icon: <Tag className="h-4 w-4" /> }
397
+ ]
398
+
399
+ // Default views
400
+ const defaultViews: CalendarView[] = [
401
+ { id: 'day', name: 'Day', type: 'day' },
402
+ { id: 'week', name: 'Week', type: 'week', default: true },
403
+ { id: 'month', name: 'Month', type: 'month' },
404
+ { id: 'year', name: 'Year', type: 'year' },
405
+ { id: 'agenda', name: 'Agenda', type: 'agenda' }
406
+ ]
407
+
408
+ export const CalendarPro = React.forwardRef<HTMLDivElement, CalendarProProps>(({
409
+ events = [],
410
+ categories = defaultCategories,
411
+ views = defaultViews,
412
+ defaultView,
413
+ height = '100%',
414
+ className,
415
+ sidebarCollapsed: controlledSidebarCollapsed,
416
+ onSidebarToggle,
417
+ onEventClick,
418
+ onEventCreate,
419
+ onEventUpdate,
420
+ onEventDelete,
421
+ onEventDrop,
422
+ onDateSelect,
423
+ onViewChange,
424
+ allowEventCreation = true,
425
+ allowEventDeletion = true,
426
+ allowEventDragging = true,
427
+ showWeekNumbers = true,
428
+ firstDayOfWeek = 1,
429
+ timeFormat = '24h',
430
+ locale = 'en-US',
431
+ workingHours = { start: 9, end: 17 },
432
+ nonWorkingDays = [0, 6],
433
+ holidays = [],
434
+ customEventRenderer,
435
+ customHeaderRenderer,
436
+ customCellRenderer,
437
+ eventColors = {},
438
+ onExport,
439
+ onImport,
440
+ integrations = {},
441
+ theme = 'system',
442
+ ...props
443
+ }, ref) => {
444
+ const [internalSidebarCollapsed, setInternalSidebarCollapsed] = useState(false)
445
+ const sidebarCollapsed = controlledSidebarCollapsed ?? internalSidebarCollapsed
446
+
447
+ const setSidebarCollapsed = useCallback((collapsed: boolean) => {
448
+ setInternalSidebarCollapsed(collapsed)
449
+ onSidebarToggle?.(collapsed)
450
+ }, [onSidebarToggle])
451
+
452
+ const [currentDate, setCurrentDate] = useState(new Date())
453
+ const [currentView, setCurrentView] = useState(() => {
454
+ if (defaultView) {
455
+ return defaultView
456
+ }
457
+ const defaultViewObj = views.find(v => v.default)
458
+ return defaultViewObj ? defaultViewObj.id : views[0].id
459
+ })
460
+
461
+ const [selectedDate, setSelectedDate] = useState<Date | null>(null)
462
+ const [selectedEvent, setSelectedEvent] = useState<CalendarEvent | null>(null)
463
+ const [isEventDialogOpen, setIsEventDialogOpen] = useState(false)
464
+ const [isCreating, setIsCreating] = useState(false)
465
+ const [newEventStart, setNewEventStart] = useState<Date | null>(null)
466
+ const [newEventEnd, setNewEventEnd] = useState<Date | null>(null)
467
+ const [searchQuery, setSearchQuery] = useState('')
468
+ const [selectedCategories, setSelectedCategories] = useState<string[]>([])
469
+ const [editingEvent, setEditingEvent] = useState<Partial<CalendarEvent>>({})
470
+ const [isDragging, setIsDragging] = useState(false)
471
+ const [draggedEvent, setDraggedEvent] = useState<CalendarEvent | null>(null)
472
+ const [dropTarget, setDropTarget] = useState<{ date: Date; time?: string } | null>(null)
473
+
474
+ const calendarRef = useRef<HTMLDivElement>(null)
475
+
476
+ // Get current view config
477
+ const currentViewConfig = useMemo(() => {
478
+ return views.find(v => v.id === currentView) || views[0]
479
+ }, [currentView, views])
480
+
481
+ // Filter events based on search and categories
482
+ const filteredEvents = useMemo(() => {
483
+ return events.filter(event => {
484
+ const matchesSearch = !searchQuery ||
485
+ event.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
486
+ event.description?.toLowerCase().includes(searchQuery.toLowerCase()) ||
487
+ event.location?.toLowerCase().includes(searchQuery.toLowerCase()) ||
488
+ event.tags?.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()))
489
+
490
+ const matchesCategory = selectedCategories.length === 0 ||
491
+ (event.category && selectedCategories.includes(event.category))
492
+
493
+ return matchesSearch && matchesCategory
494
+ })
495
+ }, [events, searchQuery, selectedCategories])
496
+
497
+ // Get events for current view
498
+ const eventsInView = useMemo(() => {
499
+ const viewType = currentViewConfig.type
500
+ let start: Date
501
+ let end: Date
502
+
503
+ switch (viewType) {
504
+ case 'day':
505
+ start = startOfDay(currentDate)
506
+ end = endOfDay(currentDate)
507
+ break
508
+ case 'week':
509
+ start = startOfWeek(currentDate, { weekStartsOn: firstDayOfWeek })
510
+ end = endOfWeek(currentDate, { weekStartsOn: firstDayOfWeek })
511
+ break
512
+ case 'month':
513
+ start = startOfMonth(currentDate)
514
+ end = endOfMonth(currentDate)
515
+ break
516
+ case 'year':
517
+ start = startOfYear(currentDate)
518
+ end = endOfYear(currentDate)
519
+ break
520
+ case 'agenda':
521
+ start = startOfDay(currentDate)
522
+ end = addDays(currentDate, 30)
523
+ break
524
+ default:
525
+ start = startOfDay(currentDate)
526
+ end = endOfDay(currentDate)
527
+ }
528
+
529
+ return filteredEvents.filter(event => {
530
+ const eventStart = new Date(event.start)
531
+ const eventEnd = new Date(event.end)
532
+ return isWithinInterval(eventStart, { start, end }) ||
533
+ isWithinInterval(eventEnd, { start, end }) ||
534
+ (isBefore(eventStart, start) && isAfter(eventEnd, end))
535
+ })
536
+ }, [filteredEvents, currentDate, currentViewConfig, firstDayOfWeek])
537
+
538
+ // Handle view change
539
+ const handleViewChange = useCallback((viewId: string) => {
540
+ setCurrentView(viewId)
541
+ onViewChange?.(viewId)
542
+ }, [onViewChange])
543
+
544
+ // Handle date navigation
545
+ const navigateDate = useCallback((direction: 'prev' | 'next') => {
546
+ const viewType = currentViewConfig.type
547
+ let newDate: Date
548
+
549
+ switch (viewType) {
550
+ case 'day':
551
+ newDate = direction === 'prev' ? subDays(currentDate, 1) : addDays(currentDate, 1)
552
+ break
553
+ case 'week':
554
+ newDate = direction === 'prev' ? subWeeks(currentDate, 1) : addWeeks(currentDate, 1)
555
+ break
556
+ case 'month':
557
+ newDate = direction === 'prev' ? subMonths(currentDate, 1) : addMonths(currentDate, 1)
558
+ break
559
+ case 'year':
560
+ newDate = direction === 'prev' ? subYears(currentDate, 1) : addYears(currentDate, 1)
561
+ break
562
+ default:
563
+ newDate = currentDate
564
+ }
565
+
566
+ setCurrentDate(newDate)
567
+ }, [currentDate, currentViewConfig])
568
+
569
+ // Handle date selection
570
+ const handleDateSelect = useCallback((date: Date) => {
571
+ setSelectedDate(date)
572
+ setCurrentDate(date)
573
+ onDateSelect?.(date)
574
+
575
+ if (allowEventCreation && currentViewConfig.type !== 'year') {
576
+ setNewEventStart(date)
577
+ setNewEventEnd(addHours(date, 1))
578
+ setIsCreating(true)
579
+ setIsEventDialogOpen(true)
580
+ }
581
+ }, [allowEventCreation, currentViewConfig, onDateSelect])
582
+
583
+ // Handle event click
584
+ const handleEventClick = useCallback((event: CalendarEvent, e: React.MouseEvent) => {
585
+ e.stopPropagation()
586
+ setSelectedEvent(event)
587
+ setEditingEvent(event)
588
+ setIsEventDialogOpen(true)
589
+ setIsCreating(false)
590
+ onEventClick?.(event, e)
591
+ }, [onEventClick])
592
+
593
+ // Handle event save
594
+ const handleEventSave = useCallback(() => {
595
+ if (isCreating && onEventCreate) {
596
+ onEventCreate(editingEvent)
597
+ } else if (selectedEvent && onEventUpdate) {
598
+ onEventUpdate({ ...selectedEvent, ...editingEvent } as CalendarEvent)
599
+ }
600
+
601
+ setIsEventDialogOpen(false)
602
+ setSelectedEvent(null)
603
+ setEditingEvent({})
604
+ setIsCreating(false)
605
+ setNewEventStart(null)
606
+ setNewEventEnd(null)
607
+ }, [isCreating, selectedEvent, editingEvent, onEventCreate, onEventUpdate])
608
+
609
+ // Handle event delete
610
+ const handleEventDelete = useCallback(() => {
611
+ if (selectedEvent && onEventDelete) {
612
+ onEventDelete(selectedEvent.id)
613
+ setIsEventDialogOpen(false)
614
+ setSelectedEvent(null)
615
+ setEditingEvent({})
616
+ }
617
+ }, [selectedEvent, onEventDelete])
618
+
619
+ // Handle drag start
620
+ const handleDragStart = useCallback((event: CalendarEvent) => {
621
+ if (!allowEventDragging) return
622
+ setIsDragging(true)
623
+ setDraggedEvent(event)
624
+ }, [allowEventDragging])
625
+
626
+ // Handle drag end
627
+ const handleDragEnd = useCallback((result: DropResult) => {
628
+ setIsDragging(false)
629
+ setDraggedEvent(null)
630
+ setDropTarget(null)
631
+
632
+ if (!result.destination || !draggedEvent || !onEventDrop) return
633
+
634
+ // Calculate new start and end dates based on drop position
635
+ // This is a simplified version - you'd need to implement proper date calculation
636
+ // based on the view and drop position
637
+ const newStart = new Date(result.destination.droppableId)
638
+ const duration = differenceInMinutes(draggedEvent.end, draggedEvent.start)
639
+ const newEnd = addMinutes(newStart, duration)
640
+
641
+ onEventDrop(draggedEvent.id, newStart, newEnd)
642
+ }, [draggedEvent, onEventDrop])
643
+
644
+ // Export calendar
645
+ const exportCalendar = useCallback((format: 'ics' | 'csv' | 'json') => {
646
+ if (onExport) {
647
+ onExport(format, eventsInView)
648
+ } else {
649
+ // Default export implementation
650
+ const data = eventsInView
651
+ let content: string
652
+ let mimeType: string
653
+ let filename: string
654
+
655
+ switch (format) {
656
+ case 'json':
657
+ content = JSON.stringify(data, null, 2)
658
+ mimeType = 'application/json'
659
+ filename = 'calendar.json'
660
+ break
661
+ case 'csv':
662
+ // Simple CSV export
663
+ const headers = ['Title', 'Start', 'End', 'Location', 'Description']
664
+ const rows = data.map(event => [
665
+ event.title,
666
+ event.start.toISOString(),
667
+ event.end.toISOString(),
668
+ event.location || '',
669
+ event.description || ''
670
+ ])
671
+ content = [headers, ...rows].map(row => row.map(cell => `"${cell}"`).join(',')).join('\n')
672
+ mimeType = 'text/csv'
673
+ filename = 'calendar.csv'
674
+ break
675
+ case 'ics':
676
+ // Simple ICS export
677
+ const icsEvents = data.map(event => {
678
+ const start = event.start.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '')
679
+ const end = event.end.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '')
680
+ return `BEGIN:VEVENT
681
+ UID:${event.id}@calendar.app
682
+ DTSTART:${start}Z
683
+ DTEND:${end}Z
684
+ SUMMARY:${event.title}
685
+ ${event.description ? `DESCRIPTION:${event.description}` : ''}
686
+ ${event.location ? `LOCATION:${event.location}` : ''}
687
+ END:VEVENT`
688
+ }).join('\n')
689
+
690
+ content = `BEGIN:VCALENDAR
691
+ VERSION:2.0
692
+ PRODID:-//Calendar App//EN
693
+ ${icsEvents}
694
+ END:VCALENDAR`
695
+ mimeType = 'text/calendar'
696
+ filename = 'calendar.ics'
697
+ break
698
+ default:
699
+ return
700
+ }
701
+
702
+ const blob = new Blob([content], { type: mimeType })
703
+ const url = URL.createObjectURL(blob)
704
+ const link = document.createElement('a')
705
+ link.href = url
706
+ link.download = filename
707
+ link.click()
708
+ URL.revokeObjectURL(url)
709
+ }
710
+ }, [eventsInView, onExport])
711
+
712
+ // Render day view
713
+ const renderDayView = () => {
714
+ const hours = Array.from({ length: 24 }, (_, i) => i)
715
+ const dayEvents = eventsInView.filter(event =>
716
+ isSameDay(new Date(event.start), currentDate)
717
+ )
718
+
719
+ return (
720
+ <div className="flex flex-1 overflow-hidden">
721
+ <div className="flex-1 overflow-auto">
722
+ <div className="min-h-full">
723
+ {/* All day events */}
724
+ <div className="border-b p-2">
725
+ <div className="text-xs text-muted-foreground mb-1">All Day</div>
726
+ <div className="space-y-1">
727
+ {dayEvents
728
+ .filter(event => event.allDay)
729
+ .map(event => (
730
+ <div
731
+ key={event.id}
732
+ className="p-2 rounded text-xs cursor-pointer hover:opacity-80"
733
+ style={{
734
+ backgroundColor: event.color || eventColors[event.category || ''] || '#3b82f6',
735
+ color: '#ffffff'
736
+ }}
737
+ onClick={(e) => handleEventClick(event, e)}
738
+ >
739
+ {event.title}
740
+ </div>
741
+ ))}
742
+ </div>
743
+ </div>
744
+
745
+ {/* Time slots */}
746
+ <div className="relative">
747
+ {hours.map(hour => (
748
+ <div key={hour} className="flex border-b" style={{ height: '60px' }}>
749
+ <div className="w-16 p-2 text-xs text-muted-foreground text-right">
750
+ {format(setHours(new Date(), hour), timeFormat === '12h' ? 'h a' : 'HH:00')}
751
+ </div>
752
+ <div className="flex-1 relative border-l">
753
+ {/* Events in this hour */}
754
+ {dayEvents
755
+ .filter(event => {
756
+ if (event.allDay) return false
757
+ const eventHour = new Date(event.start).getHours()
758
+ return eventHour === hour
759
+ })
760
+ .map(event => {
761
+ const startMinutes = new Date(event.start).getMinutes()
762
+ const duration = differenceInMinutes(new Date(event.end), new Date(event.start))
763
+ const height = (duration / 60) * 60
764
+ const top = (startMinutes / 60) * 60
765
+
766
+ return (
767
+ <div
768
+ key={event.id}
769
+ className="absolute left-0 right-0 mx-1 p-1 rounded text-xs cursor-pointer hover:opacity-80 overflow-hidden"
770
+ style={{
771
+ top: `${top}px`,
772
+ height: `${height}px`,
773
+ backgroundColor: event.color || eventColors[event.category || ''] || '#3b82f6',
774
+ color: '#ffffff',
775
+ zIndex: 10
776
+ }}
777
+ onClick={(e) => handleEventClick(event, e)}
778
+ >
779
+ <div className="font-medium">{event.title}</div>
780
+ <div className="text-xs opacity-80">
781
+ {format(new Date(event.start), 'HH:mm')} - {format(new Date(event.end), 'HH:mm')}
782
+ </div>
783
+ </div>
784
+ )
785
+ })}
786
+ </div>
787
+ </div>
788
+ ))}
789
+ </div>
790
+ </div>
791
+ </div>
792
+ </div>
793
+ )
794
+ }
795
+
796
+ // Render week view
797
+ const renderWeekView = () => {
798
+ const weekStart = startOfWeek(currentDate, { weekStartsOn: firstDayOfWeek })
799
+ const weekDays = Array.from({ length: 7 }, (_, i) => addDays(weekStart, i))
800
+ const hours = Array.from({ length: 24 }, (_, i) => i)
801
+
802
+ return (
803
+ <div className="flex flex-1 overflow-hidden">
804
+ <div className="flex-1 overflow-auto">
805
+ <div className="min-h-full">
806
+ {/* Header */}
807
+ <div className="sticky top-0 z-20 bg-background border-b">
808
+ <div className="flex">
809
+ <div className="w-16" />
810
+ {weekDays.map(day => (
811
+ <div
812
+ key={day.toISOString()}
813
+ className={cn(
814
+ "flex-1 p-2 text-center border-l",
815
+ isToday(day) && "bg-primary/10"
816
+ )}
817
+ >
818
+ <div className="text-xs text-muted-foreground">
819
+ {format(day, 'EEE')}
820
+ </div>
821
+ <div className={cn(
822
+ "text-lg font-medium",
823
+ isToday(day) && "text-primary"
824
+ )}>
825
+ {format(day, 'd')}
826
+ </div>
827
+ </div>
828
+ ))}
829
+ </div>
830
+ </div>
831
+
832
+ {/* Time grid */}
833
+ <div className="relative">
834
+ {hours.map(hour => (
835
+ <div key={hour} className="flex" style={{ height: '60px' }}>
836
+ <div className="w-16 p-2 text-xs text-muted-foreground text-right border-b">
837
+ {format(setHours(new Date(), hour), timeFormat === '12h' ? 'h a' : 'HH:00')}
838
+ </div>
839
+ {weekDays.map(day => {
840
+ const dayEvents = eventsInView.filter(event =>
841
+ isSameDay(new Date(event.start), day) && !event.allDay
842
+ )
843
+
844
+ return (
845
+ <div
846
+ key={day.toISOString()}
847
+ className={cn(
848
+ "flex-1 relative border-l border-b",
849
+ isToday(day) && "bg-primary/5"
850
+ )}
851
+ >
852
+ {dayEvents
853
+ .filter(event => {
854
+ const eventHour = new Date(event.start).getHours()
855
+ return eventHour === hour
856
+ })
857
+ .map(event => {
858
+ const startMinutes = new Date(event.start).getMinutes()
859
+ const duration = differenceInMinutes(new Date(event.end), new Date(event.start))
860
+ const height = (duration / 60) * 60
861
+ const top = (startMinutes / 60) * 60
862
+
863
+ return (
864
+ <div
865
+ key={event.id}
866
+ className="absolute left-0 right-0 mx-1 p-1 rounded text-xs cursor-pointer hover:opacity-80 overflow-hidden"
867
+ style={{
868
+ top: `${top}px`,
869
+ height: `${height}px`,
870
+ backgroundColor: event.color || eventColors[event.category || ''] || '#3b82f6',
871
+ color: '#ffffff',
872
+ zIndex: 10
873
+ }}
874
+ onClick={(e) => handleEventClick(event, e)}
875
+ >
876
+ {event.title}
877
+ </div>
878
+ )
879
+ })}
880
+ </div>
881
+ )
882
+ })}
883
+ </div>
884
+ ))}
885
+ </div>
886
+ </div>
887
+ </div>
888
+ </div>
889
+ )
890
+ }
891
+
892
+ // Render month view
893
+ const renderMonthView = () => {
894
+ const monthStart = startOfMonth(currentDate)
895
+ const monthEnd = endOfMonth(currentDate)
896
+ const startDate = startOfWeek(monthStart, { weekStartsOn: firstDayOfWeek })
897
+ const endDate = endOfWeek(monthEnd, { weekStartsOn: firstDayOfWeek })
898
+ const days = eachDayOfInterval({ start: startDate, end: endDate })
899
+ const weeks = []
900
+
901
+ for (let i = 0; i < days.length; i += 7) {
902
+ weeks.push(days.slice(i, i + 7))
903
+ }
904
+
905
+ return (
906
+ <div className="flex-1 p-4 overflow-auto">
907
+ <div className="min-h-full">
908
+ {/* Weekday headers */}
909
+ <div className="grid grid-cols-7 gap-px mb-2">
910
+ {weeks[0].map(day => (
911
+ <div
912
+ key={day.toISOString()}
913
+ className="p-2 text-center text-sm font-medium text-muted-foreground"
914
+ >
915
+ {format(day, 'EEE')}
916
+ </div>
917
+ ))}
918
+ </div>
919
+
920
+ {/* Calendar grid */}
921
+ {weeks.map((week, weekIndex) => (
922
+ <div key={weekIndex} className="grid grid-cols-7 gap-px">
923
+ {week.map(day => {
924
+ const dayEvents = eventsInView.filter(event =>
925
+ isSameDay(new Date(event.start), day)
926
+ )
927
+ const isCurrentMonth = isSameMonth(day, currentDate)
928
+
929
+ return (
930
+ <div
931
+ key={day.toISOString()}
932
+ className={cn(
933
+ "min-h-[100px] p-2 border rounded-lg cursor-pointer transition-colors",
934
+ !isCurrentMonth && "opacity-50",
935
+ isToday(day) && "bg-primary/10 border-primary",
936
+ "hover:bg-muted/50"
937
+ )}
938
+ onClick={() => handleDateSelect(day)}
939
+ >
940
+ <div className={cn(
941
+ "text-sm font-medium mb-1",
942
+ isToday(day) && "text-primary"
943
+ )}>
944
+ {format(day, 'd')}
945
+ </div>
946
+ <div className="space-y-1">
947
+ {dayEvents.slice(0, 3).map(event => {
948
+ return (
949
+ <motion.div
950
+ key={event.id}
951
+ className="text-xs p-1 rounded cursor-pointer truncate"
952
+ style={{
953
+ backgroundColor: event.color || eventColors[event.category || ''] || '#3b82f6',
954
+ color: '#ffffff'
955
+ }}
956
+ whileHover={{ scale: 1.02 }}
957
+ whileTap={{ scale: 0.98 }}
958
+ onClick={(e) => {
959
+ e.stopPropagation()
960
+ handleEventClick(event, e)
961
+ }}
962
+ >
963
+ {event.allDay ? (
964
+ event.title
965
+ ) : (
966
+ <>
967
+ {format(new Date(event.start), 'HH:mm')} {event.title}
968
+ </>
969
+ )}
970
+ </motion.div>
971
+ )
972
+ })}
973
+ {dayEvents.length > 3 && (
974
+ <div className="text-xs text-muted-foreground text-center">
975
+ +{dayEvents.length - 3} more
976
+ </div>
977
+ )}
978
+ </div>
979
+ </div>
980
+ )
981
+ })}
982
+ </div>
983
+ ))}
984
+ </div>
985
+ </div>
986
+ )
987
+ }
988
+
989
+ const renderYearView = () => {
990
+ const yearStart = startOfYear(currentDate)
991
+ const yearEnd = endOfYear(currentDate)
992
+ const months = eachMonthOfInterval({ start: yearStart, end: yearEnd })
993
+
994
+ return (
995
+ <div className="flex-1 overflow-auto p-4">
996
+ <div className="grid grid-cols-3 gap-4">
997
+ {months.map(month => {
998
+ const monthEvents = eventsInView.filter(event => {
999
+ const eventStart = new Date(event.start)
1000
+ return isSameMonth(eventStart, month)
1001
+ })
1002
+
1003
+ return (
1004
+ <Card
1005
+ key={month.toISOString()}
1006
+ className="cursor-pointer hover:shadow-lg transition-shadow"
1007
+ onClick={() => {
1008
+ setCurrentDate(month)
1009
+ handleViewChange('month')
1010
+ }}
1011
+ >
1012
+ <CardHeader className="pb-2">
1013
+ <CardTitle className="text-lg">
1014
+ {format(month, 'MMMM')}
1015
+ </CardTitle>
1016
+ <CardDescription>
1017
+ {monthEvents.length} events
1018
+ </CardDescription>
1019
+ </CardHeader>
1020
+ <CardContent>
1021
+ <div className="grid grid-cols-7 gap-1 text-xs">
1022
+ {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map(day => (
1023
+ <div key={day} className="text-center text-muted-foreground p-1">
1024
+ {day}
1025
+ </div>
1026
+ ))}
1027
+ {Array.from({ length: getDay(startOfMonth(month)) }, (_, i) => (
1028
+ <div key={`empty-${i}`} />
1029
+ ))}
1030
+ {Array.from({ length: getDaysInMonth(month) }, (_, i) => {
1031
+ const day = new Date(month.getFullYear(), month.getMonth(), i + 1)
1032
+ const hasEvents = monthEvents.some(event =>
1033
+ isSameDay(new Date(event.start), day)
1034
+ )
1035
+
1036
+ return (
1037
+ <div
1038
+ key={i}
1039
+ className={cn(
1040
+ "text-center p-1 rounded",
1041
+ isToday(day) && "bg-primary text-primary-foreground",
1042
+ hasEvents && !isToday(day) && "bg-primary/20 font-medium"
1043
+ )}
1044
+ >
1045
+ {i + 1}
1046
+ </div>
1047
+ )
1048
+ })}
1049
+ </div>
1050
+ </CardContent>
1051
+ </Card>
1052
+ )
1053
+ })}
1054
+ </div>
1055
+ </div>
1056
+ )
1057
+ }
1058
+
1059
+ const renderAgendaView = () => {
1060
+ const agendaDays = Array.from({ length: 30 }, (_, i) => addDays(currentDate, i))
1061
+
1062
+ return (
1063
+ <div className="flex-1 overflow-auto p-4">
1064
+ <div className="space-y-4">
1065
+ {agendaDays.map(day => {
1066
+ const dayEvents = eventsInView.filter(event =>
1067
+ isSameDay(new Date(event.start), day)
1068
+ ).sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime())
1069
+
1070
+ if (dayEvents.length === 0) return null
1071
+
1072
+ return (
1073
+ <Card key={day.toISOString()}>
1074
+ <CardHeader className="pb-2">
1075
+ <CardTitle className="text-lg">
1076
+ {format(day, 'EEEE, MMMM d, yyyy')}
1077
+ </CardTitle>
1078
+ <CardDescription>
1079
+ {dayEvents.length} events
1080
+ </CardDescription>
1081
+ </CardHeader>
1082
+ <CardContent>
1083
+ <div className="space-y-2">
1084
+ {dayEvents.map(event => (
1085
+ <div
1086
+ key={event.id}
1087
+ className="flex items-start gap-3 p-3 rounded-lg hover:bg-muted/50 cursor-pointer"
1088
+ onClick={(e) => handleEventClick(event, e)}
1089
+ >
1090
+ <div
1091
+ className="w-1 h-full rounded"
1092
+ style={{
1093
+ backgroundColor: event.color || eventColors[event.category || ''] || '#3b82f6'
1094
+ }}
1095
+ />
1096
+ <div className="flex-1">
1097
+ <div className="flex items-center gap-2 mb-1">
1098
+ <span className="font-medium">{event.title}</span>
1099
+ {event.priority && (
1100
+ <Badge variant={
1101
+ event.priority === 'high' ? 'destructive' :
1102
+ event.priority === 'medium' ? 'secondary' :
1103
+ 'secondary'
1104
+ }>
1105
+ {event.priority}
1106
+ </Badge>
1107
+ )}
1108
+ </div>
1109
+ <div className="text-sm text-muted-foreground">
1110
+ {event.allDay ? (
1111
+ 'All day'
1112
+ ) : (
1113
+ <>
1114
+ {format(new Date(event.start), 'HH:mm')} - {format(new Date(event.end), 'HH:mm')}
1115
+ </>
1116
+ )}
1117
+ {event.location && (
1118
+ <span className="ml-2">
1119
+ <MapPin className="inline h-3 w-3 mr-1" />
1120
+ {event.location}
1121
+ </span>
1122
+ )}
1123
+ </div>
1124
+ {event.description && (
1125
+ <p className="text-sm text-muted-foreground mt-1">
1126
+ {event.description}
1127
+ </p>
1128
+ )}
1129
+ </div>
1130
+ </div>
1131
+ ))}
1132
+ </div>
1133
+ </CardContent>
1134
+ </Card>
1135
+ )
1136
+ })}
1137
+ </div>
1138
+ </div>
1139
+ )
1140
+ }
1141
+
1142
+ return (
1143
+ <TooltipProvider>
1144
+ <div className={cn("flex h-full w-full bg-background", className)} style={{ height }}>
1145
+ {/* Sidebar */}
1146
+ <motion.div
1147
+ className={cn(
1148
+ "border-r bg-muted/30 flex flex-col flex-shrink-0",
1149
+ sidebarCollapsed ? "w-16" : "w-64"
1150
+ )}
1151
+ animate={{ width: sidebarCollapsed ? 64 : 256 }}
1152
+ transition={{ type: "spring", stiffness: 300, damping: 30 }}
1153
+ >
1154
+ {/* Mini Calendar */}
1155
+ {!sidebarCollapsed && (
1156
+ <div className="p-4">
1157
+ <CalendarBase
1158
+ mode="single"
1159
+ selected={currentDate}
1160
+ onSelect={(date) => {
1161
+ if (date instanceof Date) {
1162
+ handleDateSelect(date);
1163
+ }
1164
+ }}
1165
+ className="rounded-md border"
1166
+ />
1167
+ </div>
1168
+ )}
1169
+
1170
+ {/* Categories */}
1171
+ <div className="flex-1 overflow-auto p-4">
1172
+ {sidebarCollapsed ? (
1173
+ <div className="space-y-2">
1174
+ {categories.map(category => (
1175
+ <Tooltip key={category.id}>
1176
+ <TooltipTrigger asChild>
1177
+ <Button
1178
+ variant="ghost"
1179
+ size="icon"
1180
+ className="w-full"
1181
+ onClick={() => {
1182
+ setSelectedCategories(prev =>
1183
+ prev.includes(category.id)
1184
+ ? prev.filter(c => c !== category.id)
1185
+ : [...prev, category.id]
1186
+ )
1187
+ }}
1188
+ >
1189
+ <div
1190
+ className="w-4 h-4 rounded"
1191
+ style={{ backgroundColor: category.color }}
1192
+ />
1193
+ </Button>
1194
+ </TooltipTrigger>
1195
+ <TooltipContent side="right">
1196
+ {category.name}
1197
+ </TooltipContent>
1198
+ </Tooltip>
1199
+ ))}
1200
+ </div>
1201
+ ) : (
1202
+ <div className="space-y-2">
1203
+ <div className="flex items-center justify-between mb-2">
1204
+ <h3 className="font-medium">Categories</h3>
1205
+ <Button
1206
+ variant="ghost"
1207
+ size="sm"
1208
+ onClick={() => setSelectedCategories([])}
1209
+ >
1210
+ Clear
1211
+ </Button>
1212
+ </div>
1213
+ {categories.map(category => (
1214
+ <div
1215
+ key={category.id}
1216
+ className={cn(
1217
+ "flex items-center gap-2 p-2 rounded-lg cursor-pointer hover:bg-muted",
1218
+ selectedCategories.includes(category.id) && "bg-muted"
1219
+ )}
1220
+ onClick={() => {
1221
+ setSelectedCategories(prev =>
1222
+ prev.includes(category.id)
1223
+ ? prev.filter(c => c !== category.id)
1224
+ : [...prev, category.id]
1225
+ )
1226
+ }}
1227
+ >
1228
+ <Checkbox
1229
+ checked={selectedCategories.includes(category.id)}
1230
+ onCheckedChange={() => {}}
1231
+ />
1232
+ <div
1233
+ className="w-4 h-4 rounded"
1234
+ style={{ backgroundColor: category.color }}
1235
+ />
1236
+ {category.icon}
1237
+ <span className="text-sm">{category.name}</span>
1238
+ </div>
1239
+ ))}
1240
+ </div>
1241
+ )}
1242
+ </div>
1243
+
1244
+ {/* Sidebar Toggle */}
1245
+ <div className="p-2 border-t">
1246
+ <Button
1247
+ variant="ghost"
1248
+ size="icon"
1249
+ className="w-full"
1250
+ onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
1251
+ >
1252
+ {sidebarCollapsed ? <PanelLeft /> : <PanelRight />}
1253
+ </Button>
1254
+ </div>
1255
+ </motion.div>
1256
+
1257
+ {/* Main Content */}
1258
+ <div className="flex-1 flex flex-col overflow-hidden min-w-0">
1259
+ {/* Header */}
1260
+ <div className="border-b p-4 flex items-center justify-between">
1261
+ <div className="flex items-center gap-4">
1262
+ <div className="flex items-center gap-2">
1263
+ <Button
1264
+ variant="outline"
1265
+ size="icon"
1266
+ onClick={() => navigateDate('prev')}
1267
+ >
1268
+ <ChevronLeft className="h-4 w-4" />
1269
+ </Button>
1270
+ <Button
1271
+ variant="outline"
1272
+ size="icon"
1273
+ onClick={() => navigateDate('next')}
1274
+ >
1275
+ <ChevronRight className="h-4 w-4" />
1276
+ </Button>
1277
+ <Button
1278
+ variant="outline"
1279
+ onClick={() => setCurrentDate(new Date())}
1280
+ >
1281
+ Today
1282
+ </Button>
1283
+ </div>
1284
+
1285
+ <h2 className="text-xl font-semibold">
1286
+ {format(currentDate,
1287
+ currentViewConfig.type === 'day' ? 'EEEE, MMMM d, yyyy' :
1288
+ currentViewConfig.type === 'week' ? "'Week of' MMMM d, yyyy" :
1289
+ currentViewConfig.type === 'month' ? 'MMMM yyyy' :
1290
+ currentViewConfig.type === 'year' ? 'yyyy' :
1291
+ 'MMMM yyyy'
1292
+ )}
1293
+ </h2>
1294
+ </div>
1295
+
1296
+ <div className="flex items-center gap-2">
1297
+ {/* Search */}
1298
+ <div className="relative">
1299
+ <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
1300
+ <Input
1301
+ placeholder="Search events..."
1302
+ value={searchQuery}
1303
+ onChange={(e) => setSearchQuery(e.target.value)}
1304
+ className="pl-9 w-64"
1305
+ />
1306
+ </div>
1307
+
1308
+ {/* View Selector */}
1309
+ <Tabs value={currentView} onValueChange={handleViewChange}>
1310
+ <TabsList>
1311
+ {views.map(view => (
1312
+ <TabsTrigger key={view.id} value={view.id}>
1313
+ {view.name}
1314
+ </TabsTrigger>
1315
+ ))}
1316
+ </TabsList>
1317
+ </Tabs>
1318
+
1319
+ {/* Actions */}
1320
+ <DropdownMenu>
1321
+ <DropdownMenuTrigger asChild>
1322
+ <Button variant="outline" size="icon">
1323
+ <MoreHorizontal className="h-4 w-4" />
1324
+ </Button>
1325
+ </DropdownMenuTrigger>
1326
+ <DropdownMenuContent align="end" className="w-48">
1327
+ <DropdownMenuLabel>Actions</DropdownMenuLabel>
1328
+ <DropdownMenuSeparator />
1329
+ {allowEventCreation && (
1330
+ <DropdownMenuItem
1331
+ onClick={() => {
1332
+ setNewEventStart(new Date())
1333
+ setNewEventEnd(addHours(new Date(), 1))
1334
+ setIsCreating(true)
1335
+ setIsEventDialogOpen(true)
1336
+ }}
1337
+ >
1338
+ <Plus className="h-4 w-4 mr-2" />
1339
+ Create Event
1340
+ </DropdownMenuItem>
1341
+ )}
1342
+ <DropdownMenuSeparator />
1343
+ <DropdownMenuSub>
1344
+ <DropdownMenuSubTrigger>
1345
+ <Download className="h-4 w-4 mr-2" />
1346
+ Export
1347
+ </DropdownMenuSubTrigger>
1348
+ <DropdownMenuSubContent>
1349
+ <DropdownMenuItem onClick={() => exportCalendar('ics')}>
1350
+ <FileText className="h-4 w-4 mr-2" />
1351
+ Export as ICS
1352
+ </DropdownMenuItem>
1353
+ <DropdownMenuItem onClick={() => exportCalendar('csv')}>
1354
+ <FileSpreadsheet className="h-4 w-4 mr-2" />
1355
+ Export as CSV
1356
+ </DropdownMenuItem>
1357
+ <DropdownMenuItem onClick={() => exportCalendar('json')}>
1358
+ <FileCode className="h-4 w-4 mr-2" />
1359
+ Export as JSON
1360
+ </DropdownMenuItem>
1361
+ </DropdownMenuSubContent>
1362
+ </DropdownMenuSub>
1363
+ <DropdownMenuItem>
1364
+ <Printer className="h-4 w-4 mr-2" />
1365
+ Print
1366
+ </DropdownMenuItem>
1367
+ <DropdownMenuItem>
1368
+ <Share2 className="h-4 w-4 mr-2" />
1369
+ Share
1370
+ </DropdownMenuItem>
1371
+ <DropdownMenuSeparator />
1372
+ <DropdownMenuItem>
1373
+ <Settings className="h-4 w-4 mr-2" />
1374
+ Settings
1375
+ </DropdownMenuItem>
1376
+ </DropdownMenuContent>
1377
+ </DropdownMenu>
1378
+ </div>
1379
+ </div>
1380
+
1381
+ {/* Calendar View */}
1382
+ <LayoutGroup>
1383
+ <AnimatePresence mode="wait">
1384
+ <motion.div
1385
+ key={currentView}
1386
+ className="flex-1 overflow-hidden"
1387
+ initial={{ opacity: 0, y: 20 }}
1388
+ animate={{ opacity: 1, y: 0 }}
1389
+ exit={{ opacity: 0, y: -20 }}
1390
+ transition={{ duration: 0.2 }}
1391
+ >
1392
+ {currentViewConfig.type === 'day' && renderDayView()}
1393
+ {currentViewConfig.type === 'week' && renderWeekView()}
1394
+ {currentViewConfig.type === 'month' && renderMonthView()}
1395
+ {currentViewConfig.type === 'year' && renderYearView()}
1396
+ {currentViewConfig.type === 'agenda' && renderAgendaView()}
1397
+ </motion.div>
1398
+ </AnimatePresence>
1399
+ </LayoutGroup>
1400
+ </div>
1401
+
1402
+ {/* Event Dialog */}
1403
+ <Dialog open={isEventDialogOpen} onOpenChange={setIsEventDialogOpen}>
1404
+ <DialogContent className="sm:max-w-[600px]">
1405
+ <DialogHeader>
1406
+ <DialogTitle>
1407
+ {isCreating ? 'Create Event' : 'Edit Event'}
1408
+ </DialogTitle>
1409
+ <DialogDescription>
1410
+ {isCreating ? 'Add a new event to your calendar' : 'Update event details'}
1411
+ </DialogDescription>
1412
+ </DialogHeader>
1413
+
1414
+ <div className="space-y-4">
1415
+ <div className="space-y-2">
1416
+ <Label htmlFor="title">Title</Label>
1417
+ <Input
1418
+ id="title"
1419
+ value={editingEvent.title || ''}
1420
+ onChange={(e) => setEditingEvent({ ...editingEvent, title: e.target.value })}
1421
+ placeholder="Event title"
1422
+ />
1423
+ </div>
1424
+
1425
+ <div className="grid grid-cols-2 gap-4">
1426
+ <div className="space-y-2">
1427
+ <Label htmlFor="start">Start</Label>
1428
+ <Input
1429
+ id="start"
1430
+ type="datetime-local"
1431
+ value={editingEvent.start ? format(editingEvent.start, "yyyy-MM-dd'T'HH:mm") : ''}
1432
+ onChange={(e) => setEditingEvent({ ...editingEvent, start: new Date(e.target.value) })}
1433
+ />
1434
+ </div>
1435
+
1436
+ <div className="space-y-2">
1437
+ <Label htmlFor="end">End</Label>
1438
+ <Input
1439
+ id="end"
1440
+ type="datetime-local"
1441
+ value={editingEvent.end ? format(editingEvent.end, "yyyy-MM-dd'T'HH:mm") : ''}
1442
+ onChange={(e) => setEditingEvent({ ...editingEvent, end: new Date(e.target.value) })}
1443
+ />
1444
+ </div>
1445
+ </div>
1446
+
1447
+ <div className="flex items-center space-x-2">
1448
+ <Switch
1449
+ id="allDay"
1450
+ checked={editingEvent.allDay || false}
1451
+ onCheckedChange={(checked) => setEditingEvent({ ...editingEvent, allDay: checked })}
1452
+ />
1453
+ <Label htmlFor="allDay">All day</Label>
1454
+ </div>
1455
+
1456
+ <div className="space-y-2">
1457
+ <Label htmlFor="category">Category</Label>
1458
+ <Select
1459
+ value={editingEvent.category || ''}
1460
+ onValueChange={(value) => setEditingEvent({ ...editingEvent, category: value })}
1461
+ >
1462
+ <SelectTrigger id="category">
1463
+ <SelectValue placeholder="Select a category" />
1464
+ </SelectTrigger>
1465
+ <SelectContent>
1466
+ {categories.map(category => (
1467
+ <SelectItem key={category.id} value={category.id}>
1468
+ <div className="flex items-center gap-2">
1469
+ <div
1470
+ className="w-3 h-3 rounded"
1471
+ style={{ backgroundColor: category.color }}
1472
+ />
1473
+ {category.name}
1474
+ </div>
1475
+ </SelectItem>
1476
+ ))}
1477
+ </SelectContent>
1478
+ </Select>
1479
+ </div>
1480
+
1481
+ <div className="space-y-2">
1482
+ <Label htmlFor="location">Location</Label>
1483
+ <Input
1484
+ id="location"
1485
+ value={editingEvent.location || ''}
1486
+ onChange={(e) => setEditingEvent({ ...editingEvent, location: e.target.value })}
1487
+ placeholder="Event location"
1488
+ />
1489
+ </div>
1490
+
1491
+ <div className="space-y-2">
1492
+ <Label htmlFor="description">Description</Label>
1493
+ <Textarea
1494
+ id="description"
1495
+ value={editingEvent.description || ''}
1496
+ onChange={(e) => setEditingEvent({ ...editingEvent, description: e.target.value })}
1497
+ placeholder="Event description"
1498
+ />
1499
+ </div>
1500
+
1501
+ <div className="space-y-2">
1502
+ <Label>Priority</Label>
1503
+ <RadioGroup
1504
+ value={editingEvent.priority || 'medium'}
1505
+ onValueChange={(value) => setEditingEvent({ ...editingEvent, priority: value as any })}
1506
+ >
1507
+ <div className="flex items-center space-x-2">
1508
+ <RadioGroupItem value="low" id="low" />
1509
+ <Label htmlFor="low">Low</Label>
1510
+ </div>
1511
+ <div className="flex items-center space-x-2">
1512
+ <RadioGroupItem value="medium" id="medium" />
1513
+ <Label htmlFor="medium">Medium</Label>
1514
+ </div>
1515
+ <div className="flex items-center space-x-2">
1516
+ <RadioGroupItem value="high" id="high" />
1517
+ <Label htmlFor="high">High</Label>
1518
+ </div>
1519
+ </RadioGroup>
1520
+ </div>
1521
+ </div>
1522
+
1523
+ <DialogFooter>
1524
+ {!isCreating && allowEventDeletion && (
1525
+ <Button
1526
+ variant="destructive"
1527
+ onClick={handleEventDelete}
1528
+ >
1529
+ Delete
1530
+ </Button>
1531
+ )}
1532
+ <Button
1533
+ variant="outline"
1534
+ onClick={() => {
1535
+ setIsEventDialogOpen(false)
1536
+ setEditingEvent({})
1537
+ setSelectedEvent(null)
1538
+ setIsCreating(false)
1539
+ }}
1540
+ >
1541
+ Cancel
1542
+ </Button>
1543
+ <Button onClick={handleEventSave}>
1544
+ {isCreating ? 'Create' : 'Save'}
1545
+ </Button>
1546
+ </DialogFooter>
1547
+ </DialogContent>
1548
+ </Dialog>
1549
+ </div>
1550
+ </TooltipProvider>
1551
+ )
1552
+ })
1553
+
1554
+ CalendarPro.displayName = 'CalendarPro'
1555
+
1556
+ export default CalendarPro