@moontra/moonui-pro 2.20.1 → 2.20.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/dist/index.d.ts +691 -261
  2. package/dist/index.mjs +7418 -4934
  3. package/package.json +11 -5
  4. package/plugin/index.d.ts +86 -0
  5. package/plugin/index.js +308 -0
  6. package/scripts/postbuild.js +27 -0
  7. package/scripts/postinstall.js +176 -23
  8. package/src/__tests__/use-intersection-observer.test.tsx +0 -216
  9. package/src/__tests__/use-local-storage.test.tsx +0 -174
  10. package/src/__tests__/use-pro-access.test.tsx +0 -183
  11. package/src/components/advanced-chart/advanced-chart.test.tsx +0 -281
  12. package/src/components/advanced-chart/index.tsx +0 -1242
  13. package/src/components/advanced-forms/index.tsx +0 -426
  14. package/src/components/animated-button/index.tsx +0 -385
  15. package/src/components/calendar/event-dialog.tsx +0 -372
  16. package/src/components/calendar/index.tsx +0 -1073
  17. package/src/components/calendar-pro/index.tsx +0 -1697
  18. package/src/components/color-picker/index.tsx +0 -432
  19. package/src/components/credit-card-input/index.tsx +0 -406
  20. package/src/components/dashboard/dashboard-grid.tsx +0 -462
  21. package/src/components/dashboard/demo.tsx +0 -425
  22. package/src/components/dashboard/index.tsx +0 -1046
  23. package/src/components/dashboard/time-range-picker.tsx +0 -336
  24. package/src/components/dashboard/types.ts +0 -222
  25. package/src/components/dashboard/widgets/activity-feed.tsx +0 -344
  26. package/src/components/dashboard/widgets/chart-widget.tsx +0 -418
  27. package/src/components/dashboard/widgets/metric-card.tsx +0 -343
  28. package/src/components/data-table/data-table-bulk-actions.tsx +0 -204
  29. package/src/components/data-table/data-table-column-toggle.tsx +0 -169
  30. package/src/components/data-table/data-table-export.ts +0 -156
  31. package/src/components/data-table/data-table-filter-drawer.tsx +0 -448
  32. package/src/components/data-table/data-table.test.tsx +0 -187
  33. package/src/components/data-table/index.tsx +0 -845
  34. package/src/components/draggable-list/index.tsx +0 -100
  35. package/src/components/enhanced/badge.tsx +0 -191
  36. package/src/components/enhanced/button.tsx +0 -362
  37. package/src/components/enhanced/card.tsx +0 -266
  38. package/src/components/enhanced/dialog.tsx +0 -246
  39. package/src/components/enhanced/index.ts +0 -4
  40. package/src/components/error-boundary/index.tsx +0 -109
  41. package/src/components/file-upload/file-upload.test.tsx +0 -243
  42. package/src/components/file-upload/index.tsx +0 -1660
  43. package/src/components/floating-action-button/index.tsx +0 -206
  44. package/src/components/form-wizard/form-wizard-context.tsx +0 -307
  45. package/src/components/form-wizard/form-wizard-navigation.tsx +0 -118
  46. package/src/components/form-wizard/form-wizard-progress.tsx +0 -298
  47. package/src/components/form-wizard/form-wizard-step.tsx +0 -111
  48. package/src/components/form-wizard/index.tsx +0 -102
  49. package/src/components/form-wizard/types.ts +0 -76
  50. package/src/components/gesture-drawer/index.tsx +0 -551
  51. package/src/components/github-stars/github-api.ts +0 -426
  52. package/src/components/github-stars/hooks.ts +0 -516
  53. package/src/components/github-stars/index.tsx +0 -375
  54. package/src/components/github-stars/types.ts +0 -148
  55. package/src/components/github-stars/variants.tsx +0 -513
  56. package/src/components/health-check/index.tsx +0 -439
  57. package/src/components/hover-card-3d/index.tsx +0 -530
  58. package/src/components/index.ts +0 -128
  59. package/src/components/internal/index.ts +0 -78
  60. package/src/components/kanban/add-card-modal.tsx +0 -502
  61. package/src/components/kanban/card-detail-modal.tsx +0 -761
  62. package/src/components/kanban/index.ts +0 -13
  63. package/src/components/kanban/kanban.tsx +0 -1684
  64. package/src/components/kanban/types.ts +0 -168
  65. package/src/components/lazy-component/index.tsx +0 -823
  66. package/src/components/license-error/index.tsx +0 -29
  67. package/src/components/magnetic-button/index.tsx +0 -167
  68. package/src/components/memory-efficient-data/index.tsx +0 -1016
  69. package/src/components/moonui-quiz-form/index.tsx +0 -817
  70. package/src/components/optimized-image/index.tsx +0 -425
  71. package/src/components/performance-debugger/index.tsx +0 -589
  72. package/src/components/performance-monitor/index.tsx +0 -794
  73. package/src/components/phone-number-input/index.tsx +0 -338
  74. package/src/components/pinch-zoom/index.tsx +0 -566
  75. package/src/components/quiz-form/index.tsx +0 -479
  76. package/src/components/rich-text-editor/index-old-backup.tsx +0 -437
  77. package/src/components/rich-text-editor/index.tsx +0 -2324
  78. package/src/components/rich-text-editor/slash-commands-extension.ts +0 -220
  79. package/src/components/rich-text-editor/slash-commands.css +0 -35
  80. package/src/components/rich-text-editor/table-styles.css +0 -65
  81. package/src/components/sidebar/index.tsx +0 -865
  82. package/src/components/spotlight-card/index.tsx +0 -191
  83. package/src/components/swipeable-card/index.tsx +0 -100
  84. package/src/components/timeline/index.tsx +0 -1148
  85. package/src/components/ui/accordion.tsx +0 -73
  86. package/src/components/ui/alert-dialog.tsx +0 -141
  87. package/src/components/ui/alert.tsx +0 -141
  88. package/src/components/ui/aspect-ratio.tsx +0 -245
  89. package/src/components/ui/avatar.tsx +0 -153
  90. package/src/components/ui/badge.tsx +0 -228
  91. package/src/components/ui/breadcrumb.tsx +0 -214
  92. package/src/components/ui/button.tsx +0 -222
  93. package/src/components/ui/calendar.tsx +0 -387
  94. package/src/components/ui/card.tsx +0 -214
  95. package/src/components/ui/checkbox.tsx +0 -259
  96. package/src/components/ui/collapsible.tsx +0 -135
  97. package/src/components/ui/color-picker.tsx +0 -97
  98. package/src/components/ui/command.tsx +0 -225
  99. package/src/components/ui/dialog.tsx +0 -334
  100. package/src/components/ui/dropdown-menu.tsx +0 -218
  101. package/src/components/ui/gesture-drawer.tsx +0 -11
  102. package/src/components/ui/hover-card.tsx +0 -29
  103. package/src/components/ui/index.ts +0 -190
  104. package/src/components/ui/input.tsx +0 -222
  105. package/src/components/ui/label.tsx +0 -29
  106. package/src/components/ui/lightbox.tsx +0 -606
  107. package/src/components/ui/magnetic-button.tsx +0 -129
  108. package/src/components/ui/media-gallery.tsx +0 -612
  109. package/src/components/ui/pagination.tsx +0 -123
  110. package/src/components/ui/popover.tsx +0 -185
  111. package/src/components/ui/progress.tsx +0 -30
  112. package/src/components/ui/radio-group.tsx +0 -257
  113. package/src/components/ui/scroll-area.tsx +0 -47
  114. package/src/components/ui/select.tsx +0 -374
  115. package/src/components/ui/separator.tsx +0 -145
  116. package/src/components/ui/sheet.tsx +0 -139
  117. package/src/components/ui/skeleton.tsx +0 -20
  118. package/src/components/ui/slider.tsx +0 -354
  119. package/src/components/ui/spotlight-card.tsx +0 -119
  120. package/src/components/ui/switch.tsx +0 -86
  121. package/src/components/ui/table.tsx +0 -329
  122. package/src/components/ui/tabs.tsx +0 -198
  123. package/src/components/ui/textarea.tsx +0 -28
  124. package/src/components/ui/toast.tsx +0 -317
  125. package/src/components/ui/toggle.tsx +0 -119
  126. package/src/components/ui/tooltip.tsx +0 -151
  127. package/src/components/virtual-list/index.tsx +0 -668
  128. package/src/hooks/use-chart.ts +0 -205
  129. package/src/hooks/use-data-table.ts +0 -182
  130. package/src/hooks/use-docs-pro-access.ts +0 -13
  131. package/src/hooks/use-license-check.ts +0 -65
  132. package/src/hooks/use-subscription.ts +0 -19
  133. package/src/hooks/use-toast.ts +0 -15
  134. package/src/index.ts +0 -14
  135. package/src/lib/ai-providers.ts +0 -377
  136. package/src/lib/component-metadata.ts +0 -18
  137. package/src/lib/micro-interactions.ts +0 -255
  138. package/src/lib/paddle.ts +0 -17
  139. package/src/lib/utils.ts +0 -6
  140. package/src/patterns/login-form/index.tsx +0 -276
  141. package/src/patterns/login-form/types.ts +0 -67
  142. package/src/setupTests.ts +0 -41
  143. package/src/styles/advanced-chart.css +0 -239
  144. package/src/styles/calendar.css +0 -35
  145. package/src/styles/design-system.css +0 -363
  146. package/src/styles/index.css +0 -85
  147. package/src/styles/tailwind.css +0 -7
  148. package/src/styles/tokens.css +0 -455
  149. package/src/types/moonui.d.ts +0 -22
  150. package/src/types/next-auth.d.ts +0 -21
  151. package/src/use-intersection-observer.tsx +0 -154
  152. package/src/use-local-storage.tsx +0 -71
  153. package/src/use-paddle.ts +0 -138
  154. package/src/use-performance-optimizer.ts +0 -389
  155. package/src/use-pro-access.ts +0 -141
  156. package/src/use-scroll-animation.ts +0 -219
  157. package/src/use-subscription.ts +0 -37
  158. package/src/use-toast.ts +0 -32
  159. package/src/utils/chart-helpers.ts +0 -357
  160. package/src/utils/cn.ts +0 -6
  161. package/src/utils/data-processing.ts +0 -151
  162. package/src/utils/license-validator.tsx +0 -183
@@ -1,1697 +0,0 @@
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
- // Removed react-beautiful-dnd imports - using HTML5 drag & drop instead
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 = '100vh',
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
- // Drag state
445
- const [draggedEventId, setDraggedEventId] = useState<string | null>(null)
446
- const [dragOverInfo, setDragOverInfo] = useState<{ date: Date; hour?: number } | null>(null)
447
- const [internalSidebarCollapsed, setInternalSidebarCollapsed] = useState(false)
448
- const sidebarCollapsed = controlledSidebarCollapsed ?? internalSidebarCollapsed
449
-
450
- const setSidebarCollapsed = useCallback((collapsed: boolean) => {
451
- setInternalSidebarCollapsed(collapsed)
452
- onSidebarToggle?.(collapsed)
453
- }, [onSidebarToggle])
454
-
455
- const [currentDate, setCurrentDate] = useState(new Date())
456
- const [currentView, setCurrentView] = useState(() => {
457
- if (defaultView) {
458
- return defaultView
459
- }
460
- const defaultViewObj = views.find(v => v.default)
461
- return defaultViewObj ? defaultViewObj.id : views[0].id
462
- })
463
-
464
- const [selectedDate, setSelectedDate] = useState<Date | null>(null)
465
- const [selectedEvent, setSelectedEvent] = useState<CalendarEvent | null>(null)
466
- const [isEventDialogOpen, setIsEventDialogOpen] = useState(false)
467
- const [isCreating, setIsCreating] = useState(false)
468
- const [newEventStart, setNewEventStart] = useState<Date | null>(null)
469
- const [newEventEnd, setNewEventEnd] = useState<Date | null>(null)
470
- const [searchQuery, setSearchQuery] = useState('')
471
- const [selectedCategories, setSelectedCategories] = useState<string[]>([])
472
- const [editingEvent, setEditingEvent] = useState<Partial<CalendarEvent>>({})
473
- const [isDragging, setIsDragging] = useState(false)
474
- const [draggedEvent, setDraggedEvent] = useState<CalendarEvent | null>(null)
475
- const [dropTarget, setDropTarget] = useState<{ date: Date; time?: string } | null>(null)
476
-
477
- const calendarRef = useRef<HTMLDivElement>(null)
478
-
479
- // Get current view config
480
- const currentViewConfig = useMemo(() => {
481
- return views.find(v => v.id === currentView) || views[0]
482
- }, [currentView, views])
483
-
484
- // Filter events based on search and categories
485
- const filteredEvents = useMemo(() => {
486
- return events.filter(event => {
487
- const matchesSearch = !searchQuery ||
488
- event.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
489
- event.description?.toLowerCase().includes(searchQuery.toLowerCase()) ||
490
- event.location?.toLowerCase().includes(searchQuery.toLowerCase()) ||
491
- event.tags?.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()))
492
-
493
- const matchesCategory = selectedCategories.length === 0 ||
494
- (event.category && selectedCategories.includes(event.category))
495
-
496
- return matchesSearch && matchesCategory
497
- })
498
- }, [events, searchQuery, selectedCategories])
499
-
500
- // Get events for current view
501
- const eventsInView = useMemo(() => {
502
- const viewType = currentViewConfig.type
503
- let start: Date
504
- let end: Date
505
-
506
- switch (viewType) {
507
- case 'day':
508
- start = startOfDay(currentDate)
509
- end = endOfDay(currentDate)
510
- break
511
- case 'week':
512
- start = startOfWeek(currentDate, { weekStartsOn: firstDayOfWeek })
513
- end = endOfWeek(currentDate, { weekStartsOn: firstDayOfWeek })
514
- break
515
- case 'month':
516
- start = startOfMonth(currentDate)
517
- end = endOfMonth(currentDate)
518
- break
519
- case 'year':
520
- start = startOfYear(currentDate)
521
- end = endOfYear(currentDate)
522
- break
523
- case 'agenda':
524
- start = startOfDay(currentDate)
525
- end = addDays(currentDate, 30)
526
- break
527
- default:
528
- start = startOfDay(currentDate)
529
- end = endOfDay(currentDate)
530
- }
531
-
532
- return filteredEvents.filter(event => {
533
- const eventStart = new Date(event.start)
534
- const eventEnd = new Date(event.end)
535
- return isWithinInterval(eventStart, { start, end }) ||
536
- isWithinInterval(eventEnd, { start, end }) ||
537
- (isBefore(eventStart, start) && isAfter(eventEnd, end))
538
- })
539
- }, [filteredEvents, currentDate, currentViewConfig, firstDayOfWeek])
540
-
541
- // Handle view change
542
- const handleViewChange = useCallback((viewId: string) => {
543
- setCurrentView(viewId)
544
- onViewChange?.(viewId)
545
- }, [onViewChange])
546
-
547
- // Handle date navigation
548
- const navigateDate = useCallback((direction: 'prev' | 'next') => {
549
- const viewType = currentViewConfig.type
550
- let newDate: Date
551
-
552
- switch (viewType) {
553
- case 'day':
554
- newDate = direction === 'prev' ? subDays(currentDate, 1) : addDays(currentDate, 1)
555
- break
556
- case 'week':
557
- newDate = direction === 'prev' ? subWeeks(currentDate, 1) : addWeeks(currentDate, 1)
558
- break
559
- case 'month':
560
- newDate = direction === 'prev' ? subMonths(currentDate, 1) : addMonths(currentDate, 1)
561
- break
562
- case 'year':
563
- newDate = direction === 'prev' ? subYears(currentDate, 1) : addYears(currentDate, 1)
564
- break
565
- default:
566
- newDate = currentDate
567
- }
568
-
569
- setCurrentDate(newDate)
570
- }, [currentDate, currentViewConfig])
571
-
572
- // Handle date selection
573
- const handleDateSelect = useCallback((date: Date) => {
574
- setSelectedDate(date)
575
- setCurrentDate(date)
576
- onDateSelect?.(date)
577
-
578
- if (allowEventCreation && currentViewConfig.type !== 'year') {
579
- setNewEventStart(date)
580
- setNewEventEnd(addHours(date, 1))
581
- setIsCreating(true)
582
- setIsEventDialogOpen(true)
583
- }
584
- }, [allowEventCreation, currentViewConfig, onDateSelect])
585
-
586
- // Handle event click
587
- const handleEventClick = useCallback((event: CalendarEvent, e: React.MouseEvent) => {
588
- e.stopPropagation()
589
- setSelectedEvent(event)
590
- setEditingEvent(event)
591
- setIsEventDialogOpen(true)
592
- setIsCreating(false)
593
- onEventClick?.(event, e)
594
- }, [onEventClick])
595
-
596
- // Handle event save
597
- const handleEventSave = useCallback(() => {
598
- if (isCreating && onEventCreate) {
599
- onEventCreate(editingEvent)
600
- } else if (selectedEvent && onEventUpdate) {
601
- onEventUpdate({ ...selectedEvent, ...editingEvent } as CalendarEvent)
602
- }
603
-
604
- setIsEventDialogOpen(false)
605
- setSelectedEvent(null)
606
- setEditingEvent({})
607
- setIsCreating(false)
608
- setNewEventStart(null)
609
- setNewEventEnd(null)
610
- }, [isCreating, selectedEvent, editingEvent, onEventCreate, onEventUpdate])
611
-
612
- // Handle event delete
613
- const handleEventDelete = useCallback(() => {
614
- if (selectedEvent && onEventDelete) {
615
- onEventDelete(selectedEvent.id)
616
- setIsEventDialogOpen(false)
617
- setSelectedEvent(null)
618
- setEditingEvent({})
619
- }
620
- }, [selectedEvent, onEventDelete])
621
-
622
- // Handle drag start
623
- const handleDragStart = useCallback((e: React.DragEvent, event: CalendarEvent) => {
624
- if (!allowEventDragging) return
625
-
626
- setDraggedEventId(event.id)
627
- setIsDragging(true)
628
-
629
- // Store event data in dataTransfer
630
- e.dataTransfer.effectAllowed = 'move'
631
- e.dataTransfer.setData('text/plain', event.id)
632
-
633
- // Create drag image with preserved dimensions
634
- const dragImage = e.currentTarget.cloneNode(true) as HTMLElement
635
- const originalRect = e.currentTarget.getBoundingClientRect()
636
- dragImage.style.opacity = '0.8'
637
- dragImage.style.position = 'absolute'
638
- dragImage.style.top = '-1000px'
639
- dragImage.style.width = `${originalRect.width}px`
640
- dragImage.style.height = `${originalRect.height}px`
641
- dragImage.style.boxSizing = 'border-box'
642
- document.body.appendChild(dragImage)
643
- e.dataTransfer.setDragImage(dragImage, e.nativeEvent.offsetX, e.nativeEvent.offsetY)
644
- setTimeout(() => document.body.removeChild(dragImage), 0)
645
- }, [allowEventDragging])
646
-
647
- // Handle drag over
648
- const handleDragOver = useCallback((e: React.DragEvent, date: Date, hour?: number) => {
649
- e.preventDefault()
650
- e.dataTransfer.dropEffect = 'move'
651
- setDragOverInfo({ date, hour })
652
- }, [])
653
-
654
- // Handle drag leave
655
- const handleDragLeave = useCallback((e: React.DragEvent) => {
656
- // Only clear if we're leaving the drop zone entirely
657
- const rect = e.currentTarget.getBoundingClientRect()
658
- const x = e.clientX
659
- const y = e.clientY
660
-
661
- if (x < rect.left || x >= rect.right || y < rect.top || y >= rect.bottom) {
662
- setDragOverInfo(null)
663
- }
664
- }, [])
665
-
666
- // Handle drop
667
- const handleDrop = useCallback((e: React.DragEvent, date: Date, hour?: number) => {
668
- e.preventDefault()
669
-
670
- const eventId = e.dataTransfer.getData('text/plain')
671
- const draggedEvent = events.find(ev => ev.id === eventId)
672
-
673
- if (!draggedEvent || !onEventDrop) return
674
-
675
- // Calculate new start and end times
676
- let newStart: Date
677
- let newEnd: Date
678
-
679
- if (hour !== undefined) {
680
- // Dropped on a specific hour slot
681
- newStart = setHours(setMinutes(date, 0), hour)
682
- } else {
683
- // Dropped on a day (keep original time)
684
- const originalHours = draggedEvent.start.getHours()
685
- const originalMinutes = draggedEvent.start.getMinutes()
686
- newStart = setHours(setMinutes(date, originalMinutes), originalHours)
687
- }
688
-
689
- // Keep the same duration
690
- const duration = differenceInMinutes(draggedEvent.end, draggedEvent.start)
691
- newEnd = addMinutes(newStart, duration)
692
-
693
- // Update event
694
- onEventDrop(draggedEvent.id, newStart, newEnd)
695
-
696
- // Reset drag state
697
- setDraggedEventId(null)
698
- setDragOverInfo(null)
699
- setIsDragging(false)
700
- }, [events, onEventDrop])
701
-
702
- // Handle drag end (cleanup)
703
- const handleDragEnd = useCallback(() => {
704
- setDraggedEventId(null)
705
- setDragOverInfo(null)
706
- setIsDragging(false)
707
- }, [])
708
-
709
- // Export calendar
710
- const exportCalendar = useCallback((format: 'ics' | 'csv' | 'json') => {
711
- if (onExport) {
712
- onExport(format, eventsInView)
713
- } else {
714
- // Default export implementation
715
- const data = eventsInView
716
- let content: string
717
- let mimeType: string
718
- let filename: string
719
-
720
- switch (format) {
721
- case 'json':
722
- content = JSON.stringify(data, null, 2)
723
- mimeType = 'application/json'
724
- filename = 'calendar.json'
725
- break
726
- case 'csv':
727
- // Simple CSV export
728
- const headers = ['Title', 'Start', 'End', 'Location', 'Description']
729
- const rows = data.map(event => [
730
- event.title,
731
- event.start.toISOString(),
732
- event.end.toISOString(),
733
- event.location || '',
734
- event.description || ''
735
- ])
736
- content = [headers, ...rows].map(row => row.map(cell => `"${cell}"`).join(',')).join('\n')
737
- mimeType = 'text/csv'
738
- filename = 'calendar.csv'
739
- break
740
- case 'ics':
741
- // Simple ICS export
742
- const icsEvents = data.map(event => {
743
- const start = event.start.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '')
744
- const end = event.end.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '')
745
- return `BEGIN:VEVENT
746
- UID:${event.id}@calendar.app
747
- DTSTART:${start}Z
748
- DTEND:${end}Z
749
- SUMMARY:${event.title}
750
- ${event.description ? `DESCRIPTION:${event.description}` : ''}
751
- ${event.location ? `LOCATION:${event.location}` : ''}
752
- END:VEVENT`
753
- }).join('\n')
754
-
755
- content = `BEGIN:VCALENDAR
756
- VERSION:2.0
757
- PRODID:-//Calendar App//EN
758
- ${icsEvents}
759
- END:VCALENDAR`
760
- mimeType = 'text/calendar'
761
- filename = 'calendar.ics'
762
- break
763
- default:
764
- return
765
- }
766
-
767
- const blob = new Blob([content], { type: mimeType })
768
- const url = URL.createObjectURL(blob)
769
- const link = document.createElement('a')
770
- link.href = url
771
- link.download = filename
772
- link.click()
773
- URL.revokeObjectURL(url)
774
- }
775
- }, [eventsInView, onExport])
776
-
777
- // Render day view
778
- const renderDayView = () => {
779
- const hours = Array.from({ length: 24 }, (_, i) => i)
780
- const dayEvents = eventsInView.filter(event =>
781
- isSameDay(new Date(event.start), currentDate)
782
- )
783
-
784
- return (
785
- <div className="flex flex-1 overflow-hidden">
786
- <div className="flex-1 overflow-auto">
787
- <div className="min-h-full">
788
- {/* All day events */}
789
- <div
790
- className={cn(
791
- "border-b p-2",
792
- dragOverInfo?.date && !dragOverInfo?.hour && isSameDay(dragOverInfo.date, currentDate) && "bg-primary/10"
793
- )}
794
- onDragOver={(e) => handleDragOver(e, currentDate)}
795
- onDragLeave={handleDragLeave}
796
- onDrop={(e) => handleDrop(e, currentDate)}
797
- >
798
- <div className="text-xs text-muted-foreground mb-1">All Day</div>
799
- <div className="space-y-1">
800
- {dayEvents
801
- .filter(event => event.allDay)
802
- .map(event => (
803
- <div
804
- key={event.id}
805
- draggable={allowEventDragging}
806
- onDragStart={(e) => handleDragStart(e, event)}
807
- onDragEnd={handleDragEnd}
808
- className="p-2 rounded text-xs cursor-pointer hover:opacity-80"
809
- style={{
810
- backgroundColor: event.color || eventColors[event.category || ''] || '#3b82f6',
811
- color: '#ffffff',
812
- opacity: draggedEventId === event.id ? 0.5 : 1
813
- }}
814
- onClick={(e) => handleEventClick(event, e)}
815
- >
816
- {event.title}
817
- </div>
818
- ))}
819
- </div>
820
- </div>
821
-
822
- {/* Time slots */}
823
- <div className="relative">
824
- {hours.map(hour => (
825
- <div key={hour} className="flex border-b" style={{ height: '60px' }}>
826
- <div className="w-16 p-2 text-xs text-muted-foreground text-right">
827
- {format(setHours(new Date(), hour), timeFormat === '12h' ? 'h a' : 'HH:00')}
828
- </div>
829
- <div
830
- className={cn(
831
- "flex-1 relative border-l cursor-pointer hover:bg-muted/20",
832
- dragOverInfo?.date && dragOverInfo?.hour === hour && isSameDay(dragOverInfo.date, currentDate) && "bg-primary/10"
833
- )}
834
- onDragOver={(e) => handleDragOver(e, currentDate, hour)}
835
- onDragLeave={handleDragLeave}
836
- onDrop={(e) => handleDrop(e, currentDate, hour)}
837
- onClick={() => {
838
- if (allowEventCreation) {
839
- const clickedTime = setHours(setMinutes(currentDate, 0), hour)
840
- setNewEventStart(clickedTime)
841
- setNewEventEnd(addHours(clickedTime, 1))
842
- setEditingEvent({
843
- title: '',
844
- start: clickedTime,
845
- end: addHours(clickedTime, 1),
846
- allDay: false
847
- })
848
- setIsCreating(true)
849
- setIsEventDialogOpen(true)
850
- }
851
- }}
852
- >
853
- {/* Events in this hour */}
854
- {dayEvents
855
- .filter(event => {
856
- if (event.allDay) return false
857
- const eventHour = new Date(event.start).getHours()
858
- return eventHour === hour
859
- })
860
- .map(event => {
861
- const startMinutes = new Date(event.start).getMinutes()
862
- const duration = differenceInMinutes(new Date(event.end), new Date(event.start))
863
- const height = (duration / 60) * 60
864
- const top = (startMinutes / 60) * 60
865
-
866
- return (
867
- <div
868
- key={event.id}
869
- draggable={allowEventDragging}
870
- onDragStart={(e) => handleDragStart(e, event)}
871
- onDragEnd={handleDragEnd}
872
- className="absolute left-0 right-0 mx-1 p-1 rounded text-xs cursor-pointer hover:opacity-80 overflow-hidden"
873
- style={{
874
- top: `${top}px`,
875
- height: `${height}px`,
876
- backgroundColor: event.color || eventColors[event.category || ''] || '#3b82f6',
877
- color: '#ffffff',
878
- zIndex: 10,
879
- opacity: draggedEventId === event.id ? 0.5 : 1,
880
- cursor: allowEventDragging ? 'move' : 'pointer'
881
- }}
882
- onClick={(e) => {
883
- e.stopPropagation()
884
- handleEventClick(event, e)
885
- }}
886
- >
887
- <div className="font-medium">{event.title}</div>
888
- <div className="text-xs opacity-80">
889
- {format(new Date(event.start), 'HH:mm')} - {format(new Date(event.end), 'HH:mm')}
890
- </div>
891
- </div>
892
- )
893
- })}
894
- </div>
895
- </div>
896
- ))}
897
- </div>
898
- </div>
899
- </div>
900
- </div>
901
- )
902
- }
903
-
904
- // Render week view
905
- const renderWeekView = () => {
906
- const weekStart = startOfWeek(currentDate, { weekStartsOn: firstDayOfWeek })
907
- const weekDays = Array.from({ length: 7 }, (_, i) => addDays(weekStart, i))
908
- const hours = Array.from({ length: 24 }, (_, i) => i)
909
-
910
- return (
911
- <div className="flex flex-1 overflow-hidden">
912
- <div className="flex-1 overflow-auto">
913
- <div className="min-h-full">
914
- {/* Header */}
915
- <div className="sticky top-0 z-20 bg-background border-b">
916
- <div className="flex">
917
- <div className="w-16" />
918
- {weekDays.map(day => (
919
- <div
920
- key={day.toISOString()}
921
- className={cn(
922
- "flex-1 p-2 text-center border-l",
923
- isToday(day) && "bg-primary/10"
924
- )}
925
- >
926
- <div className="text-xs text-muted-foreground">
927
- {format(day, 'EEE')}
928
- </div>
929
- <div className={cn(
930
- "text-lg font-medium",
931
- isToday(day) && "text-primary"
932
- )}>
933
- {format(day, 'd')}
934
- </div>
935
- </div>
936
- ))}
937
- </div>
938
- </div>
939
-
940
- {/* Time grid */}
941
- <div className="relative">
942
- {hours.map(hour => (
943
- <div key={hour} className="flex" style={{ height: '60px' }}>
944
- <div className="w-16 p-2 text-xs text-muted-foreground text-right border-b">
945
- {format(setHours(new Date(), hour), timeFormat === '12h' ? 'h a' : 'HH:00')}
946
- </div>
947
- {weekDays.map(day => {
948
- const dayEvents = eventsInView.filter(event =>
949
- isSameDay(new Date(event.start), day) && !event.allDay
950
- )
951
- return (
952
- <div
953
- key={day.toISOString()}
954
- className={cn(
955
- "flex-1 relative border-l border-b cursor-pointer hover:bg-muted/20",
956
- isToday(day) && "bg-primary/5",
957
- dragOverInfo?.date && dragOverInfo?.hour === hour && isSameDay(dragOverInfo.date, day) && "bg-primary/10"
958
- )}
959
- onDragOver={(e) => handleDragOver(e, day, hour)}
960
- onDragLeave={handleDragLeave}
961
- onDrop={(e) => handleDrop(e, day, hour)}
962
- onClick={() => {
963
- if (allowEventCreation) {
964
- const clickedTime = setHours(setMinutes(day, 0), hour)
965
- setNewEventStart(clickedTime)
966
- setNewEventEnd(addHours(clickedTime, 1))
967
- setEditingEvent({
968
- title: '',
969
- start: clickedTime,
970
- end: addHours(clickedTime, 1),
971
- allDay: false
972
- })
973
- setIsCreating(true)
974
- setIsEventDialogOpen(true)
975
- }
976
- }}
977
- >
978
- {dayEvents
979
- .filter(event => {
980
- const eventHour = new Date(event.start).getHours()
981
- return eventHour === hour
982
- })
983
- .map(event => {
984
- const startMinutes = new Date(event.start).getMinutes()
985
- const duration = differenceInMinutes(new Date(event.end), new Date(event.start))
986
- const height = (duration / 60) * 60
987
- const top = (startMinutes / 60) * 60
988
-
989
- return (
990
- <div
991
- key={event.id}
992
- draggable={allowEventDragging}
993
- onDragStart={(e) => handleDragStart(e, event)}
994
- onDragEnd={handleDragEnd}
995
- className="absolute left-0 right-0 mx-1 p-1 rounded text-xs cursor-pointer hover:opacity-80 overflow-hidden"
996
- style={{
997
- top: `${top}px`,
998
- height: `${height}px`,
999
- backgroundColor: event.color || eventColors[event.category || ''] || '#3b82f6',
1000
- color: '#ffffff',
1001
- zIndex: 10,
1002
- opacity: draggedEventId === event.id ? 0.5 : 1,
1003
- cursor: allowEventDragging ? 'move' : 'pointer'
1004
- }}
1005
- onClick={(e) => {
1006
- e.stopPropagation()
1007
- handleEventClick(event, e)
1008
- }}
1009
- >
1010
- {event.title}
1011
- </div>
1012
- )
1013
- })}
1014
- </div>
1015
- )
1016
- })}
1017
- </div>
1018
- ))}
1019
- </div>
1020
- </div>
1021
- </div>
1022
- </div>
1023
- )
1024
- }
1025
-
1026
- // Render month view
1027
- const renderMonthView = () => {
1028
- const monthStart = startOfMonth(currentDate)
1029
- const monthEnd = endOfMonth(currentDate)
1030
- const startDate = startOfWeek(monthStart, { weekStartsOn: firstDayOfWeek })
1031
- const endDate = endOfWeek(monthEnd, { weekStartsOn: firstDayOfWeek })
1032
- const days = eachDayOfInterval({ start: startDate, end: endDate })
1033
- const weeks = []
1034
-
1035
- for (let i = 0; i < days.length; i += 7) {
1036
- weeks.push(days.slice(i, i + 7))
1037
- }
1038
-
1039
- return (
1040
- <div className="flex-1 p-4 overflow-auto">
1041
- <div className="min-h-full">
1042
- {/* Weekday headers */}
1043
- <div className="grid grid-cols-7 gap-px mb-2">
1044
- {weeks[0].map(day => (
1045
- <div
1046
- key={day.toISOString()}
1047
- className="p-2 text-center text-sm font-medium text-muted-foreground"
1048
- >
1049
- {format(day, 'EEE')}
1050
- </div>
1051
- ))}
1052
- </div>
1053
-
1054
- {/* Calendar grid */}
1055
- {weeks.map((week, weekIndex) => (
1056
- <div key={weekIndex} className="grid grid-cols-7 gap-px">
1057
- {week.map(day => {
1058
- const dayEvents = eventsInView.filter(event =>
1059
- isSameDay(new Date(event.start), day)
1060
- )
1061
- const isCurrentMonth = isSameMonth(day, currentDate)
1062
-
1063
- return (
1064
- <div
1065
- key={day.toISOString()}
1066
- className={cn(
1067
- "min-h-[100px] p-2 border rounded-lg cursor-pointer transition-colors",
1068
- !isCurrentMonth && "opacity-50",
1069
- isToday(day) && "bg-primary/10 border-primary",
1070
- "hover:bg-muted/50",
1071
- dragOverInfo?.date && !dragOverInfo?.hour && isSameDay(dragOverInfo.date, day) && "bg-primary/20 border-primary"
1072
- )}
1073
- onDragOver={(e) => handleDragOver(e, day)}
1074
- onDragLeave={handleDragLeave}
1075
- onDrop={(e) => handleDrop(e, day)}
1076
- onClick={() => handleDateSelect(day)}
1077
- >
1078
- <div className={cn(
1079
- "text-sm font-medium mb-1",
1080
- isToday(day) && "text-primary"
1081
- )}>
1082
- {format(day, 'd')}
1083
- </div>
1084
- <div className="space-y-1">
1085
- {dayEvents.slice(0, 3).map(event => {
1086
- return (
1087
- <div
1088
- key={event.id}
1089
- draggable={allowEventDragging}
1090
- onDragStart={(e) => handleDragStart(e, event)}
1091
- onDragEnd={handleDragEnd}
1092
- className="text-xs p-1 rounded cursor-pointer truncate"
1093
- style={{
1094
- backgroundColor: event.color || eventColors[event.category || ''] || '#3b82f6',
1095
- color: '#ffffff',
1096
- opacity: draggedEventId === event.id ? 0.5 : 1,
1097
- cursor: allowEventDragging ? 'move' : 'pointer'
1098
- }}
1099
- onClick={(e) => {
1100
- e.stopPropagation()
1101
- handleEventClick(event, e)
1102
- }}
1103
- >
1104
- {event.allDay ? (
1105
- event.title
1106
- ) : (
1107
- <>
1108
- {format(new Date(event.start), 'HH:mm')} {event.title}
1109
- </>
1110
- )}
1111
- </div>
1112
- )
1113
- })}
1114
- {dayEvents.length > 3 && (
1115
- <div className="text-xs text-muted-foreground text-center">
1116
- +{dayEvents.length - 3} more
1117
- </div>
1118
- )}
1119
- </div>
1120
- </div>
1121
- )
1122
- })}
1123
- </div>
1124
- ))}
1125
- </div>
1126
- </div>
1127
- )
1128
- }
1129
-
1130
- const renderYearView = () => {
1131
- const yearStart = startOfYear(currentDate)
1132
- const yearEnd = endOfYear(currentDate)
1133
- const months = eachMonthOfInterval({ start: yearStart, end: yearEnd })
1134
-
1135
- return (
1136
- <div className="flex-1 overflow-auto p-4">
1137
- <div className="grid grid-cols-3 gap-4">
1138
- {months.map(month => {
1139
- const monthEvents = eventsInView.filter(event => {
1140
- const eventStart = new Date(event.start)
1141
- return isSameMonth(eventStart, month)
1142
- })
1143
-
1144
- return (
1145
- <Card
1146
- key={month.toISOString()}
1147
- className="cursor-pointer hover:shadow-lg transition-shadow"
1148
- onClick={() => {
1149
- setCurrentDate(month)
1150
- handleViewChange('month')
1151
- }}
1152
- >
1153
- <CardHeader className="pb-2">
1154
- <CardTitle className="text-lg">
1155
- {format(month, 'MMMM')}
1156
- </CardTitle>
1157
- <CardDescription>
1158
- {monthEvents.length} events
1159
- </CardDescription>
1160
- </CardHeader>
1161
- <CardContent>
1162
- <div className="grid grid-cols-7 gap-1 text-xs">
1163
- {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day, index) => (
1164
- <div key={`${day}-${index}`} className="text-center text-muted-foreground p-1">
1165
- {day}
1166
- </div>
1167
- ))}
1168
- {Array.from({ length: getDay(startOfMonth(month)) }, (_, i) => (
1169
- <div key={`empty-${i}`} />
1170
- ))}
1171
- {Array.from({ length: getDaysInMonth(month) }, (_, i) => {
1172
- const day = new Date(month.getFullYear(), month.getMonth(), i + 1)
1173
- const hasEvents = monthEvents.some(event =>
1174
- isSameDay(new Date(event.start), day)
1175
- )
1176
-
1177
- return (
1178
- <div
1179
- key={i}
1180
- className={cn(
1181
- "text-center p-1 rounded",
1182
- isToday(day) && "bg-primary text-primary-foreground",
1183
- hasEvents && !isToday(day) && "bg-primary/20 font-medium"
1184
- )}
1185
- >
1186
- {i + 1}
1187
- </div>
1188
- )
1189
- })}
1190
- </div>
1191
- </CardContent>
1192
- </Card>
1193
- )
1194
- })}
1195
- </div>
1196
- </div>
1197
- )
1198
- }
1199
-
1200
- const renderAgendaView = () => {
1201
- const agendaDays = Array.from({ length: 30 }, (_, i) => addDays(currentDate, i))
1202
-
1203
- return (
1204
- <div className="flex-1 overflow-auto p-4">
1205
- <div className="space-y-4">
1206
- {agendaDays.map(day => {
1207
- const dayEvents = eventsInView.filter(event =>
1208
- isSameDay(new Date(event.start), day)
1209
- ).sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime())
1210
-
1211
- if (dayEvents.length === 0) return null
1212
-
1213
- return (
1214
- <Card key={day.toISOString()}>
1215
- <CardHeader className="pb-2">
1216
- <CardTitle className="text-lg">
1217
- {format(day, 'EEEE, MMMM d, yyyy')}
1218
- </CardTitle>
1219
- <CardDescription>
1220
- {dayEvents.length} events
1221
- </CardDescription>
1222
- </CardHeader>
1223
- <CardContent>
1224
- <div className="space-y-2">
1225
- {dayEvents.map(event => (
1226
- <div
1227
- key={event.id}
1228
- className="flex items-start gap-3 p-3 rounded-lg hover:bg-muted/50 cursor-pointer"
1229
- onClick={(e) => handleEventClick(event, e)}
1230
- >
1231
- <div
1232
- className="w-1 h-full rounded"
1233
- style={{
1234
- backgroundColor: event.color || eventColors[event.category || ''] || '#3b82f6'
1235
- }}
1236
- />
1237
- <div className="flex-1">
1238
- <div className="flex items-center gap-2 mb-1">
1239
- <span className="font-medium">{event.title}</span>
1240
- {event.priority && (
1241
- <Badge variant={
1242
- event.priority === 'high' ? 'destructive' :
1243
- event.priority === 'medium' ? 'secondary' :
1244
- 'secondary'
1245
- }>
1246
- {event.priority}
1247
- </Badge>
1248
- )}
1249
- </div>
1250
- <div className="text-sm text-muted-foreground">
1251
- {event.allDay ? (
1252
- 'All day'
1253
- ) : (
1254
- <>
1255
- {format(new Date(event.start), 'HH:mm')} - {format(new Date(event.end), 'HH:mm')}
1256
- </>
1257
- )}
1258
- {event.location && (
1259
- <span className="ml-2">
1260
- <MapPin className="inline h-3 w-3 mr-1" />
1261
- {event.location}
1262
- </span>
1263
- )}
1264
- </div>
1265
- {event.description && (
1266
- <p className="text-sm text-muted-foreground mt-1">
1267
- {event.description}
1268
- </p>
1269
- )}
1270
- </div>
1271
- </div>
1272
- ))}
1273
- </div>
1274
- </CardContent>
1275
- </Card>
1276
- )
1277
- })}
1278
- </div>
1279
- </div>
1280
- )
1281
- }
1282
-
1283
- return (
1284
- <TooltipProvider>
1285
- <div className={cn("flex h-full w-full bg-background", className)} style={{ height }}>
1286
- {/* Sidebar */}
1287
- <motion.div
1288
- className={cn(
1289
- "border-r bg-muted/30 flex flex-col flex-shrink-0",
1290
- sidebarCollapsed ? "w-16" : "w-64"
1291
- )}
1292
- animate={{ width: sidebarCollapsed ? 64 : 256 }}
1293
- transition={{ type: "spring", stiffness: 300, damping: 30 }}
1294
- >
1295
- {/* Mini Calendar */}
1296
- {!sidebarCollapsed && (
1297
- <div className="p-4">
1298
- <CalendarBase
1299
- mode="single"
1300
- selected={currentDate}
1301
- onSelect={(date) => {
1302
- if (date instanceof Date) {
1303
- handleDateSelect(date);
1304
- }
1305
- }}
1306
- className="rounded-md border"
1307
- />
1308
- </div>
1309
- )}
1310
-
1311
- {/* Categories */}
1312
- <div className="flex-1 overflow-auto p-4">
1313
- {sidebarCollapsed ? (
1314
- <div className="space-y-2">
1315
- {categories.map(category => (
1316
- <Tooltip key={category.id}>
1317
- <TooltipTrigger asChild>
1318
- <Button
1319
- variant="ghost"
1320
- size="icon"
1321
- className="w-full"
1322
- onClick={() => {
1323
- setSelectedCategories(prev =>
1324
- prev.includes(category.id)
1325
- ? prev.filter(c => c !== category.id)
1326
- : [...prev, category.id]
1327
- )
1328
- }}
1329
- >
1330
- <div
1331
- className="w-4 h-4 rounded"
1332
- style={{ backgroundColor: category.color }}
1333
- />
1334
- </Button>
1335
- </TooltipTrigger>
1336
- <TooltipContent side="right">
1337
- {category.name}
1338
- </TooltipContent>
1339
- </Tooltip>
1340
- ))}
1341
- </div>
1342
- ) : (
1343
- <div className="space-y-2">
1344
- <div className="flex items-center justify-between mb-2">
1345
- <h3 className="font-medium">Categories</h3>
1346
- <Button
1347
- variant="ghost"
1348
- size="sm"
1349
- onClick={() => setSelectedCategories([])}
1350
- >
1351
- Clear
1352
- </Button>
1353
- </div>
1354
- {categories.map(category => (
1355
- <div
1356
- key={category.id}
1357
- className={cn(
1358
- "flex items-center gap-2 p-2 rounded-lg cursor-pointer hover:bg-muted",
1359
- selectedCategories.includes(category.id) && "bg-muted"
1360
- )}
1361
- onClick={() => {
1362
- setSelectedCategories(prev =>
1363
- prev.includes(category.id)
1364
- ? prev.filter(c => c !== category.id)
1365
- : [...prev, category.id]
1366
- )
1367
- }}
1368
- >
1369
- <Checkbox
1370
- checked={selectedCategories.includes(category.id)}
1371
- onCheckedChange={() => {}}
1372
- />
1373
- <div
1374
- className="w-4 h-4 rounded"
1375
- style={{ backgroundColor: category.color }}
1376
- />
1377
- {category.icon}
1378
- <span className="text-sm">{category.name}</span>
1379
- </div>
1380
- ))}
1381
- </div>
1382
- )}
1383
- </div>
1384
-
1385
- {/* Sidebar Toggle */}
1386
- <div className="p-2 border-t">
1387
- <Button
1388
- variant="ghost"
1389
- size="icon"
1390
- className="w-full"
1391
- onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
1392
- >
1393
- {sidebarCollapsed ? <PanelLeft /> : <PanelRight />}
1394
- </Button>
1395
- </div>
1396
- </motion.div>
1397
-
1398
- {/* Main Content */}
1399
- <div className="flex-1 flex flex-col overflow-hidden min-w-0">
1400
- {/* Header */}
1401
- <div className="border-b p-4 flex items-center justify-between">
1402
- <div className="flex items-center gap-4">
1403
- <div className="flex items-center gap-2">
1404
- <Button
1405
- variant="outline"
1406
- size="icon"
1407
- onClick={() => navigateDate('prev')}
1408
- >
1409
- <ChevronLeft className="h-4 w-4" />
1410
- </Button>
1411
- <Button
1412
- variant="outline"
1413
- size="icon"
1414
- onClick={() => navigateDate('next')}
1415
- >
1416
- <ChevronRight className="h-4 w-4" />
1417
- </Button>
1418
- <Button
1419
- variant="outline"
1420
- onClick={() => setCurrentDate(new Date())}
1421
- >
1422
- Today
1423
- </Button>
1424
- </div>
1425
-
1426
- <h2 className="text-xl font-semibold">
1427
- {format(currentDate,
1428
- currentViewConfig.type === 'day' ? 'EEEE, MMMM d, yyyy' :
1429
- currentViewConfig.type === 'week' ? "'Week of' MMMM d, yyyy" :
1430
- currentViewConfig.type === 'month' ? 'MMMM yyyy' :
1431
- currentViewConfig.type === 'year' ? 'yyyy' :
1432
- 'MMMM yyyy'
1433
- )}
1434
- </h2>
1435
- </div>
1436
-
1437
- <div className="flex items-center gap-2">
1438
- {/* Search */}
1439
- <div className="relative">
1440
- <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
1441
- <Input
1442
- placeholder="Search events..."
1443
- value={searchQuery}
1444
- onChange={(e) => setSearchQuery(e.target.value)}
1445
- className="pl-9 w-64"
1446
- />
1447
- </div>
1448
-
1449
- {/* View Selector */}
1450
- <Tabs value={currentView} onValueChange={handleViewChange}>
1451
- <TabsList>
1452
- {views.map(view => (
1453
- <TabsTrigger key={view.id} value={view.id}>
1454
- {view.name}
1455
- </TabsTrigger>
1456
- ))}
1457
- </TabsList>
1458
- </Tabs>
1459
-
1460
- {/* Actions */}
1461
- <DropdownMenu>
1462
- <DropdownMenuTrigger asChild>
1463
- <Button variant="outline" size="icon">
1464
- <MoreHorizontal className="h-4 w-4" />
1465
- </Button>
1466
- </DropdownMenuTrigger>
1467
- <DropdownMenuContent align="end" className="w-48">
1468
- <DropdownMenuLabel>Actions</DropdownMenuLabel>
1469
- <DropdownMenuSeparator />
1470
- {allowEventCreation && (
1471
- <DropdownMenuItem
1472
- onClick={() => {
1473
- setNewEventStart(new Date())
1474
- setNewEventEnd(addHours(new Date(), 1))
1475
- setIsCreating(true)
1476
- setIsEventDialogOpen(true)
1477
- }}
1478
- >
1479
- <Plus className="h-4 w-4 mr-2" />
1480
- Create Event
1481
- </DropdownMenuItem>
1482
- )}
1483
- <DropdownMenuSeparator />
1484
- <DropdownMenuSub>
1485
- <DropdownMenuSubTrigger>
1486
- <Download className="h-4 w-4 mr-2" />
1487
- Export
1488
- </DropdownMenuSubTrigger>
1489
- <DropdownMenuSubContent>
1490
- <DropdownMenuItem onClick={() => exportCalendar('ics')}>
1491
- <FileText className="h-4 w-4 mr-2" />
1492
- Export as ICS
1493
- </DropdownMenuItem>
1494
- <DropdownMenuItem onClick={() => exportCalendar('csv')}>
1495
- <FileSpreadsheet className="h-4 w-4 mr-2" />
1496
- Export as CSV
1497
- </DropdownMenuItem>
1498
- <DropdownMenuItem onClick={() => exportCalendar('json')}>
1499
- <FileCode className="h-4 w-4 mr-2" />
1500
- Export as JSON
1501
- </DropdownMenuItem>
1502
- </DropdownMenuSubContent>
1503
- </DropdownMenuSub>
1504
- <DropdownMenuItem>
1505
- <Printer className="h-4 w-4 mr-2" />
1506
- Print
1507
- </DropdownMenuItem>
1508
- <DropdownMenuItem>
1509
- <Share2 className="h-4 w-4 mr-2" />
1510
- Share
1511
- </DropdownMenuItem>
1512
- <DropdownMenuSeparator />
1513
- <DropdownMenuItem>
1514
- <Settings className="h-4 w-4 mr-2" />
1515
- Settings
1516
- </DropdownMenuItem>
1517
- </DropdownMenuContent>
1518
- </DropdownMenu>
1519
- </div>
1520
- </div>
1521
-
1522
- {/* Calendar View */}
1523
- <LayoutGroup>
1524
- <AnimatePresence mode="wait">
1525
- <motion.div
1526
- key={currentView}
1527
- className="flex-1 overflow-hidden"
1528
- initial={{ opacity: 0, y: 20 }}
1529
- animate={{ opacity: 1, y: 0 }}
1530
- exit={{ opacity: 0, y: -20 }}
1531
- transition={{ duration: 0.2 }}
1532
- >
1533
- {currentViewConfig.type === 'day' && renderDayView()}
1534
- {currentViewConfig.type === 'week' && renderWeekView()}
1535
- {currentViewConfig.type === 'month' && renderMonthView()}
1536
- {currentViewConfig.type === 'year' && renderYearView()}
1537
- {currentViewConfig.type === 'agenda' && renderAgendaView()}
1538
- </motion.div>
1539
- </AnimatePresence>
1540
- </LayoutGroup>
1541
- </div>
1542
-
1543
- {/* Event Dialog */}
1544
- <Dialog open={isEventDialogOpen} onOpenChange={setIsEventDialogOpen}>
1545
- <DialogContent className="sm:max-w-[600px]">
1546
- <DialogHeader>
1547
- <DialogTitle>
1548
- {isCreating ? 'Create Event' : 'Edit Event'}
1549
- </DialogTitle>
1550
- <DialogDescription>
1551
- {isCreating ? 'Add a new event to your calendar' : 'Update event details'}
1552
- </DialogDescription>
1553
- </DialogHeader>
1554
-
1555
- <div className="space-y-4">
1556
- <div className="space-y-2">
1557
- <Label htmlFor="title">Title</Label>
1558
- <Input
1559
- id="title"
1560
- value={editingEvent.title || ''}
1561
- onChange={(e) => setEditingEvent({ ...editingEvent, title: e.target.value })}
1562
- placeholder="Event title"
1563
- />
1564
- </div>
1565
-
1566
- <div className="grid grid-cols-2 gap-4">
1567
- <div className="space-y-2">
1568
- <Label htmlFor="start">Start</Label>
1569
- <Input
1570
- id="start"
1571
- type="datetime-local"
1572
- value={editingEvent.start ? format(editingEvent.start, "yyyy-MM-dd'T'HH:mm") : ''}
1573
- onChange={(e) => setEditingEvent({ ...editingEvent, start: new Date(e.target.value) })}
1574
- />
1575
- </div>
1576
-
1577
- <div className="space-y-2">
1578
- <Label htmlFor="end">End</Label>
1579
- <Input
1580
- id="end"
1581
- type="datetime-local"
1582
- value={editingEvent.end ? format(editingEvent.end, "yyyy-MM-dd'T'HH:mm") : ''}
1583
- onChange={(e) => setEditingEvent({ ...editingEvent, end: new Date(e.target.value) })}
1584
- />
1585
- </div>
1586
- </div>
1587
-
1588
- <div className="flex items-center space-x-2">
1589
- <Switch
1590
- id="allDay"
1591
- checked={editingEvent.allDay || false}
1592
- onCheckedChange={(checked) => setEditingEvent({ ...editingEvent, allDay: checked })}
1593
- />
1594
- <Label htmlFor="allDay">All day</Label>
1595
- </div>
1596
-
1597
- <div className="space-y-2">
1598
- <Label htmlFor="category">Category</Label>
1599
- <Select
1600
- value={editingEvent.category || ''}
1601
- onValueChange={(value) => setEditingEvent({ ...editingEvent, category: value })}
1602
- >
1603
- <SelectTrigger id="category">
1604
- <SelectValue placeholder="Select a category" />
1605
- </SelectTrigger>
1606
- <SelectContent>
1607
- {categories.map(category => (
1608
- <SelectItem key={category.id} value={category.id}>
1609
- <div className="flex items-center gap-2">
1610
- <div
1611
- className="w-3 h-3 rounded"
1612
- style={{ backgroundColor: category.color }}
1613
- />
1614
- {category.name}
1615
- </div>
1616
- </SelectItem>
1617
- ))}
1618
- </SelectContent>
1619
- </Select>
1620
- </div>
1621
-
1622
- <div className="space-y-2">
1623
- <Label htmlFor="location">Location</Label>
1624
- <Input
1625
- id="location"
1626
- value={editingEvent.location || ''}
1627
- onChange={(e) => setEditingEvent({ ...editingEvent, location: e.target.value })}
1628
- placeholder="Event location"
1629
- />
1630
- </div>
1631
-
1632
- <div className="space-y-2">
1633
- <Label htmlFor="description">Description</Label>
1634
- <Textarea
1635
- id="description"
1636
- value={editingEvent.description || ''}
1637
- onChange={(e) => setEditingEvent({ ...editingEvent, description: e.target.value })}
1638
- placeholder="Event description"
1639
- />
1640
- </div>
1641
-
1642
- <div className="space-y-2">
1643
- <Label>Priority</Label>
1644
- <RadioGroup
1645
- value={editingEvent.priority || 'medium'}
1646
- onValueChange={(value) => setEditingEvent({ ...editingEvent, priority: value as any })}
1647
- >
1648
- <div className="flex items-center space-x-2">
1649
- <RadioGroupItem value="low" id="low" />
1650
- <Label htmlFor="low">Low</Label>
1651
- </div>
1652
- <div className="flex items-center space-x-2">
1653
- <RadioGroupItem value="medium" id="medium" />
1654
- <Label htmlFor="medium">Medium</Label>
1655
- </div>
1656
- <div className="flex items-center space-x-2">
1657
- <RadioGroupItem value="high" id="high" />
1658
- <Label htmlFor="high">High</Label>
1659
- </div>
1660
- </RadioGroup>
1661
- </div>
1662
- </div>
1663
-
1664
- <DialogFooter>
1665
- {!isCreating && allowEventDeletion && (
1666
- <Button
1667
- variant="destructive"
1668
- onClick={handleEventDelete}
1669
- >
1670
- Delete
1671
- </Button>
1672
- )}
1673
- <Button
1674
- variant="outline"
1675
- onClick={() => {
1676
- setIsEventDialogOpen(false)
1677
- setEditingEvent({})
1678
- setSelectedEvent(null)
1679
- setIsCreating(false)
1680
- }}
1681
- >
1682
- Cancel
1683
- </Button>
1684
- <Button onClick={handleEventSave}>
1685
- {isCreating ? 'Create' : 'Save'}
1686
- </Button>
1687
- </DialogFooter>
1688
- </DialogContent>
1689
- </Dialog>
1690
- </div>
1691
- </TooltipProvider>
1692
- )
1693
- })
1694
-
1695
- CalendarPro.displayName = 'CalendarPro'
1696
-
1697
- export default CalendarPro