@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,1073 +0,0 @@
1
- "use client"
2
-
3
- import React from 'react'
4
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
5
- import { Button } from '../ui/button'
6
- import { MoonUIBadgePro as Badge } from '../ui/badge'
7
- import { Input } from '../ui/input'
8
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
9
- import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'
10
- import {
11
- ChevronLeft,
12
- ChevronRight,
13
- Calendar as CalendarIcon,
14
- Plus,
15
- Edit,
16
- Trash2,
17
- Clock,
18
- MapPin,
19
- User,
20
- Lock,
21
- Sparkles,
22
- Search,
23
- Filter,
24
- Download,
25
- Upload,
26
- Grid3X3,
27
- List,
28
- CalendarDays,
29
- Repeat,
30
- Bell,
31
- ChevronDown,
32
- MoreHorizontal,
33
- Tag,
34
- Users,
35
- Video,
36
- Phone,
37
- Globe,
38
- Zap,
39
- Sun,
40
- Moon,
41
- Palette
42
- } from 'lucide-react'
43
- import { cn } from '../../lib/utils'
44
- import { EventDialog } from './event-dialog'
45
- import { motion, AnimatePresence } from 'framer-motion'
46
-
47
- export interface CalendarEvent {
48
- id: string
49
- title: string
50
- description?: string
51
- date: Date
52
- startTime?: string
53
- endTime?: string
54
- location?: string
55
- attendees?: string[]
56
- color?: string
57
- type?: 'meeting' | 'task' | 'reminder' | 'event' | 'birthday' | 'holiday' | 'conference'
58
- recurring?: {
59
- pattern: 'daily' | 'weekly' | 'monthly' | 'yearly'
60
- interval: number
61
- endDate?: Date
62
- daysOfWeek?: number[] // For weekly pattern
63
- dayOfMonth?: number // For monthly pattern
64
- }
65
- reminder?: {
66
- type: 'email' | 'notification' | 'sms'
67
- before: number // minutes before event
68
- }
69
- priority?: 'low' | 'medium' | 'high'
70
- status?: 'confirmed' | 'tentative' | 'cancelled'
71
- attachments?: string[]
72
- tags?: string[]
73
- isAllDay?: boolean
74
- isPrivate?: boolean
75
- meetingLink?: string
76
- phoneNumber?: string
77
- timeZone?: string
78
- }
79
-
80
- interface CalendarProps {
81
- events?: CalendarEvent[]
82
- onEventClick?: (event: CalendarEvent) => void
83
- onEventAdd?: (eventData: Omit<CalendarEvent, 'id'> & { id?: string }) => void
84
- onEventEdit?: (eventData: Omit<CalendarEvent, 'id'> & { id: string }) => void
85
- onEventDelete?: (eventId: string) => void
86
- onDateChange?: (date: Date) => void
87
- onViewChange?: (view: 'month' | 'week' | 'day' | 'year' | 'agenda') => void
88
- onEventDrop?: (event: CalendarEvent, newDate: Date) => void
89
- className?: string
90
- showWeekends?: boolean
91
- showEventDetails?: boolean
92
- disabled?: boolean
93
- minDate?: Date
94
- maxDate?: Date
95
- highlightToday?: boolean
96
- height?: number
97
- defaultView?: 'month' | 'week' | 'day' | 'year' | 'agenda'
98
- enableDragDrop?: boolean
99
- enableSearch?: boolean
100
- enableFilters?: boolean
101
- enableExport?: boolean
102
- enableImport?: boolean
103
- enableRecurringEvents?: boolean
104
- enableReminders?: boolean
105
- eventCategories?: Array<{ value: string; label: string; color: string }>
106
- workingHours?: { start: string; end: string }
107
- holidays?: Array<{ date: Date; name: string }>
108
- locale?: string
109
- timeFormat?: '12h' | '24h'
110
- firstDayOfWeek?: 0 | 1 | 2 | 3 | 4 | 5 | 6
111
- theme?: 'light' | 'dark' | 'auto' | 'custom'
112
- customTheme?: {
113
- primary: string
114
- secondary: string
115
- accent: string
116
- background: string
117
- foreground: string
118
- }
119
- compactMode?: boolean
120
- // For NPM package usage - allows external control of pro access
121
- showProUpgrade?: boolean
122
- }
123
-
124
- const DAYS_OF_WEEK = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
125
- const MONTHS = [
126
- 'January', 'February', 'March', 'April', 'May', 'June',
127
- 'July', 'August', 'September', 'October', 'November', 'December'
128
- ]
129
-
130
- const EVENT_COLORS = {
131
- meeting: 'bg-blue-500',
132
- task: 'bg-green-500',
133
- reminder: 'bg-yellow-500',
134
- event: 'bg-purple-500',
135
- birthday: 'bg-pink-500',
136
- holiday: 'bg-orange-500',
137
- conference: 'bg-indigo-500'
138
- }
139
-
140
- const PRIORITY_COLORS = {
141
- low: 'border-l-4 border-l-muted-foreground',
142
- medium: 'border-l-4 border-l-yellow-500',
143
- high: 'border-l-4 border-l-red-500'
144
- }
145
-
146
- const STATUS_STYLES = {
147
- confirmed: '',
148
- tentative: 'opacity-70 border-dashed',
149
- cancelled: 'opacity-50 line-through'
150
- }
151
-
152
- export function Calendar({
153
- events = [],
154
- onEventClick,
155
- onEventAdd,
156
- onEventEdit,
157
- onEventDelete,
158
- onDateChange,
159
- onViewChange,
160
- onEventDrop,
161
- className,
162
- showWeekends = true,
163
- showEventDetails = true,
164
- disabled = false,
165
- minDate,
166
- maxDate,
167
- highlightToday = true,
168
- height,
169
- defaultView = 'month',
170
- enableDragDrop = true,
171
- enableSearch = true,
172
- enableFilters = true,
173
- enableExport = true,
174
- enableImport = false,
175
- enableRecurringEvents = true,
176
- enableReminders = true,
177
- eventCategories,
178
- workingHours = { start: '09:00', end: '18:00' },
179
- holidays = [],
180
- locale = 'en-US',
181
- timeFormat = '12h',
182
- firstDayOfWeek = 0,
183
- theme = 'auto',
184
- customTheme,
185
- compactMode = false,
186
- showProUpgrade = false
187
- }: CalendarProps) {
188
- // For NPM package usage, show upgrade prompt if specified
189
- if (showProUpgrade) {
190
- return (
191
- <Card className={cn("w-full", className)}>
192
- <CardContent className="py-12 text-center">
193
- <div className="max-w-md mx-auto space-y-4">
194
- <div className="rounded-full bg-purple-100 dark:bg-purple-900/30 p-3 w-fit mx-auto">
195
- <Lock className="h-6 w-6 text-purple-600 dark:text-purple-400" />
196
- </div>
197
- <div>
198
- <h3 className="font-semibold text-lg mb-2">Pro Feature</h3>
199
- <p className="text-muted-foreground text-sm mb-4">
200
- Calendar is available exclusively to MoonUI Pro subscribers.
201
- </p>
202
- <div className="flex gap-3 justify-center">
203
- <a href="/pricing">
204
- <Button size="sm">
205
- <Sparkles className="mr-2 h-4 w-4" />
206
- Upgrade to Pro
207
- </Button>
208
- </a>
209
- </div>
210
- </div>
211
- </div>
212
- </CardContent>
213
- </Card>
214
- )
215
- }
216
-
217
- const [currentDate, setCurrentDate] = React.useState(new Date())
218
- const [selectedDate, setSelectedDate] = React.useState<Date | null>(null)
219
- const [view, setView] = React.useState<'month' | 'week' | 'day' | 'year' | 'agenda'>(defaultView)
220
- const [eventDialogOpen, setEventDialogOpen] = React.useState(false)
221
- const [eventDialogMode, setEventDialogMode] = React.useState<'create' | 'edit'>('create')
222
- const [selectedEvent, setSelectedEvent] = React.useState<CalendarEvent | null>(null)
223
- const [draggedEvent, setDraggedEvent] = React.useState<CalendarEvent | null>(null)
224
- const [dragTargetDate, setDragTargetDate] = React.useState<Date | null>(null)
225
- const [searchQuery, setSearchQuery] = React.useState('')
226
- const [filterType, setFilterType] = React.useState<string>('all')
227
- const [filterPriority, setFilterPriority] = React.useState<string>('all')
228
- const [showFiltersPanel, setShowFiltersPanel] = React.useState(false)
229
- const [selectedTags, setSelectedTags] = React.useState<string[]>([])
230
- const [miniCalendarDate, setMiniCalendarDate] = React.useState(new Date())
231
-
232
- const today = new Date()
233
- const currentMonth = currentDate.getMonth()
234
- const currentYear = currentDate.getFullYear()
235
-
236
- const firstDayOfMonth = new Date(currentYear, currentMonth, 1)
237
- const lastDayOfMonth = new Date(currentYear, currentMonth + 1, 0)
238
- const startDate = new Date(firstDayOfMonth)
239
-
240
- // Adjust start date based on firstDayOfWeek setting
241
- const daysToSubtract = (startDate.getDay() - firstDayOfWeek + 7) % 7
242
- startDate.setDate(startDate.getDate() - daysToSubtract)
243
-
244
- const endDate = new Date(lastDayOfMonth)
245
- const daysToAdd = (6 - endDate.getDay() + firstDayOfWeek) % 7
246
- endDate.setDate(endDate.getDate() + daysToAdd)
247
-
248
- const calendarDays = []
249
- const currentDateIterator = new Date(startDate)
250
-
251
- while (currentDateIterator <= endDate) {
252
- calendarDays.push(new Date(currentDateIterator))
253
- currentDateIterator.setDate(currentDateIterator.getDate() + 1)
254
- }
255
-
256
- // Get all unique tags from events
257
- const allTags = React.useMemo(() => {
258
- const tags = new Set<string>()
259
- events.forEach(event => {
260
- event.tags?.forEach(tag => tags.add(tag))
261
- })
262
- return Array.from(tags)
263
- }, [events])
264
-
265
- // Filter events based on search and filters
266
- const filteredEvents = React.useMemo(() => {
267
- return events.filter(event => {
268
- // Search filter
269
- if (searchQuery && !event.title.toLowerCase().includes(searchQuery.toLowerCase()) &&
270
- !event.description?.toLowerCase().includes(searchQuery.toLowerCase())) {
271
- return false
272
- }
273
-
274
- // Type filter
275
- if (filterType !== 'all' && event.type !== filterType) {
276
- return false
277
- }
278
-
279
- // Priority filter
280
- if (filterPriority !== 'all' && event.priority !== filterPriority) {
281
- return false
282
- }
283
-
284
- // Tags filter
285
- if (selectedTags.length > 0 && (!event.tags || !event.tags.some(tag => selectedTags.includes(tag)))) {
286
- return false
287
- }
288
-
289
- return true
290
- })
291
- }, [events, searchQuery, filterType, filterPriority, selectedTags])
292
-
293
- const getEventsForDate = (date: Date) => {
294
- return filteredEvents.filter(event => {
295
- const eventDate = new Date(event.date)
296
-
297
- // Check for recurring events
298
- if (event.recurring) {
299
- return isDateInRecurringPattern(date, event)
300
- }
301
-
302
- return eventDate.toDateString() === date.toDateString()
303
- })
304
- }
305
-
306
- const isDateInRecurringPattern = (date: Date, event: CalendarEvent): boolean => {
307
- if (!event.recurring) return false
308
-
309
- const eventDate = new Date(event.date)
310
- const daysDiff = Math.floor((date.getTime() - eventDate.getTime()) / (1000 * 60 * 60 * 24))
311
-
312
- if (daysDiff < 0) return false
313
- if (event.recurring.endDate && date > event.recurring.endDate) return false
314
-
315
- switch (event.recurring.pattern) {
316
- case 'daily':
317
- return daysDiff % event.recurring.interval === 0
318
- case 'weekly':
319
- if (daysDiff % (event.recurring.interval * 7) !== 0) return false
320
- if (event.recurring.daysOfWeek) {
321
- return event.recurring.daysOfWeek.includes(date.getDay())
322
- }
323
- return date.getDay() === eventDate.getDay()
324
- case 'monthly':
325
- const monthsDiff = (date.getFullYear() - eventDate.getFullYear()) * 12 +
326
- (date.getMonth() - eventDate.getMonth())
327
- if (monthsDiff % event.recurring.interval !== 0) return false
328
- if (event.recurring.dayOfMonth) {
329
- return date.getDate() === event.recurring.dayOfMonth
330
- }
331
- return date.getDate() === eventDate.getDate()
332
- case 'yearly':
333
- const yearsDiff = date.getFullYear() - eventDate.getFullYear()
334
- if (yearsDiff % event.recurring.interval !== 0) return false
335
- return date.getMonth() === eventDate.getMonth() &&
336
- date.getDate() === eventDate.getDate()
337
- default:
338
- return false
339
- }
340
- }
341
-
342
- const isToday = (date: Date) => {
343
- return date.toDateString() === today.toDateString()
344
- }
345
-
346
- const isCurrentMonth = (date: Date) => {
347
- return date.getMonth() === currentMonth
348
- }
349
-
350
- const isSelected = (date: Date) => {
351
- return selectedDate && date.toDateString() === selectedDate.toDateString()
352
- }
353
-
354
- const isDisabled = (date: Date) => {
355
- if (disabled) return true
356
- if (minDate && date < minDate) return true
357
- if (maxDate && date > maxDate) return true
358
- return false
359
- }
360
-
361
- const navigateMonth = (direction: 'prev' | 'next') => {
362
- const newDate = new Date(currentDate)
363
- if (direction === 'prev') {
364
- newDate.setMonth(currentMonth - 1)
365
- } else {
366
- newDate.setMonth(currentMonth + 1)
367
- }
368
- setCurrentDate(newDate)
369
- onDateChange?.(newDate)
370
- }
371
-
372
- const changeView = (newView: 'month' | 'week' | 'day' | 'year' | 'agenda') => {
373
- setView(newView)
374
- onViewChange?.(newView)
375
- }
376
-
377
- const exportCalendar = () => {
378
- // Create iCal format
379
- const icalContent = generateICalContent(filteredEvents)
380
- const blob = new Blob([icalContent], { type: 'text/calendar' })
381
- const url = URL.createObjectURL(blob)
382
- const a = document.createElement('a')
383
- a.href = url
384
- a.download = 'calendar.ics'
385
- a.click()
386
- URL.revokeObjectURL(url)
387
- }
388
-
389
- const generateICalContent = (events: CalendarEvent[]): string => {
390
- let content = 'BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//MoonUI//Calendar//EN\r\n'
391
-
392
- events.forEach(event => {
393
- content += 'BEGIN:VEVENT\r\n'
394
- content += `UID:${event.id}@moonui.com\r\n`
395
- content += `DTSTART:${formatDateToICS(event.date, event.startTime)}\r\n`
396
- content += `DTEND:${formatDateToICS(event.date, event.endTime || event.startTime)}\r\n`
397
- content += `SUMMARY:${event.title}\r\n`
398
- if (event.description) content += `DESCRIPTION:${event.description}\r\n`
399
- if (event.location) content += `LOCATION:${event.location}\r\n`
400
- content += 'END:VEVENT\r\n'
401
- })
402
-
403
- content += 'END:VCALENDAR\r\n'
404
- return content
405
- }
406
-
407
- const formatDateToICS = (date: Date, time?: string): string => {
408
- const d = new Date(date)
409
- if (time) {
410
- const [hours, minutes] = time.split(':')
411
- d.setHours(parseInt(hours), parseInt(minutes))
412
- }
413
- return d.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'
414
- }
415
-
416
- const handleDateClick = (date: Date) => {
417
- if (isDisabled(date)) return
418
-
419
- setSelectedDate(date)
420
- const dateEvents = getEventsForDate(date)
421
-
422
- if (dateEvents.length === 0) {
423
- // Open dialog for creating new event
424
- setEventDialogMode('create')
425
- setSelectedEvent(null)
426
- setEventDialogOpen(true)
427
- } else if (dateEvents.length === 1) {
428
- onEventClick?.(dateEvents[0])
429
- }
430
- }
431
-
432
- const handleEventClick = (event: CalendarEvent, e: React.MouseEvent) => {
433
- e.stopPropagation()
434
- onEventClick?.(event)
435
- }
436
-
437
- const handleEventDragStart = (event: CalendarEvent, e: React.DragEvent) => {
438
- setDraggedEvent(event)
439
- e.dataTransfer.effectAllowed = 'move'
440
- e.dataTransfer.setData('text/plain', event.id)
441
-
442
- // Add visual feedback
443
- const target = e.target as HTMLElement
444
- target.style.opacity = '0.5'
445
- }
446
-
447
- const handleEventDragEnd = (e: React.DragEvent) => {
448
- setDraggedEvent(null)
449
- setDragTargetDate(null)
450
-
451
- // Remove visual feedback
452
- const target = e.target as HTMLElement
453
- target.style.opacity = '1'
454
- }
455
-
456
- const handleDateDragOver = (date: Date, e: React.DragEvent) => {
457
- if (isDisabled(date) || !draggedEvent) return
458
- e.preventDefault()
459
- e.dataTransfer.dropEffect = 'move'
460
- setDragTargetDate(date)
461
- }
462
-
463
- const handleDateDrop = (date: Date, e: React.DragEvent) => {
464
- e.preventDefault()
465
-
466
- if (!draggedEvent || isDisabled(date)) return
467
-
468
- // Only move if it's a different date
469
- if (draggedEvent.date.toDateString() !== date.toDateString()) {
470
- const updatedEvent = {
471
- ...draggedEvent,
472
- date: date
473
- }
474
-
475
- onEventEdit?.(updatedEvent)
476
- }
477
-
478
- setDraggedEvent(null)
479
- setDragTargetDate(null)
480
- }
481
-
482
- const handleEventEdit = (event: CalendarEvent, e: React.MouseEvent) => {
483
- e.stopPropagation()
484
- setEventDialogMode('edit')
485
- setSelectedEvent(event)
486
- setEventDialogOpen(true)
487
- }
488
-
489
- const handleEventDelete = (event: CalendarEvent, e: React.MouseEvent) => {
490
- e.stopPropagation()
491
- onEventDelete?.(event.id)
492
- }
493
-
494
- const handleEventSave = (eventData: Omit<CalendarEvent, 'id'> & { id?: string }) => {
495
- if (eventDialogMode === 'create') {
496
- onEventAdd?.(eventData)
497
- } else if (eventDialogMode === 'edit' && eventData.id) {
498
- onEventEdit?.(eventData as Omit<CalendarEvent, 'id'> & { id: string })
499
- }
500
- }
501
-
502
- const handleEventDialogDelete = (eventId: string) => {
503
- onEventDelete?.(eventId)
504
- }
505
-
506
- const goToToday = () => {
507
- const today = new Date()
508
- setCurrentDate(today)
509
- setSelectedDate(today)
510
- onDateChange?.(today)
511
- }
512
-
513
- const isHoliday = (date: Date) => {
514
- return holidays.some(holiday => {
515
- const holidayDate = new Date(holiday.date)
516
- return holidayDate.toDateString() === date.toDateString()
517
- })
518
- }
519
-
520
- const getHolidayName = (date: Date) => {
521
- const holiday = holidays.find(h => {
522
- const holidayDate = new Date(h.date)
523
- return holidayDate.toDateString() === date.toDateString()
524
- })
525
- return holiday?.name
526
- }
527
-
528
- const formatTime = (time?: string): string => {
529
- if (!time) return ''
530
- if (timeFormat === '24h') return time
531
-
532
- const [hours, minutes] = time.split(':')
533
- const h = parseInt(hours)
534
- const period = h >= 12 ? 'PM' : 'AM'
535
- const displayHours = h === 0 ? 12 : h > 12 ? h - 12 : h
536
- return `${displayHours}:${minutes} ${period}`
537
- }
538
-
539
- const filteredDays = showWeekends ? calendarDays : calendarDays.filter(day => {
540
- const dayOfWeek = day.getDay()
541
- return dayOfWeek !== 0 && dayOfWeek !== 6
542
- })
543
-
544
- // Reorder days of week based on firstDayOfWeek setting
545
- const orderedDaysOfWeek = [...DAYS_OF_WEEK.slice(firstDayOfWeek), ...DAYS_OF_WEEK.slice(0, firstDayOfWeek)]
546
- const visibleDaysOfWeek = showWeekends ? orderedDaysOfWeek : orderedDaysOfWeek.filter((_, index) => {
547
- const actualDay = (index + firstDayOfWeek) % 7
548
- return actualDay !== 0 && actualDay !== 6
549
- })
550
-
551
- return (
552
- <>
553
- <Card className={cn("w-full max-w-full overflow-hidden", className)} style={{ height: height ? `${height}px` : undefined }}>
554
- <CardHeader>
555
- <div className="flex items-center justify-between">
556
- <div>
557
- <CardTitle className="flex items-center gap-2">
558
- <CalendarIcon className="h-5 w-5" />
559
- Calendar
560
- </CardTitle>
561
- <CardDescription>
562
- {MONTHS[currentMonth]} {currentYear}
563
- </CardDescription>
564
- </div>
565
- <div className="flex items-center gap-2">
566
- <Button variant="outline" size="sm" onClick={goToToday}>
567
- Today
568
- </Button>
569
- <Button
570
- variant="outline"
571
- size="sm"
572
- onClick={() => navigateMonth('prev')}
573
- disabled={disabled}
574
- >
575
- <ChevronLeft className="h-4 w-4" />
576
- </Button>
577
- <Button
578
- variant="outline"
579
- size="sm"
580
- onClick={() => navigateMonth('next')}
581
- disabled={disabled}
582
- >
583
- <ChevronRight className="h-4 w-4" />
584
- </Button>
585
- </div>
586
- </div>
587
- </CardHeader>
588
- <CardContent>
589
- <div className="space-y-4">
590
- {/* Calendar View */}
591
- {view === 'month' && (
592
- <div className="grid grid-cols-7 gap-1 w-full">
593
- {/* Day Headers */}
594
- {visibleDaysOfWeek.map((day) => (
595
- <div key={day} className="p-1 text-center text-xs font-medium text-muted-foreground">
596
- {day}
597
- </div>
598
- ))}
599
-
600
- {/* Calendar Days */}
601
- {filteredDays.map((date, index) => {
602
- const dayEvents = getEventsForDate(date)
603
- const isCurrentMonthDate = isCurrentMonth(date)
604
- const isTodayDate = isToday(date)
605
- const isSelectedDate = isSelected(date)
606
- const isDisabledDate = isDisabled(date)
607
-
608
- return (
609
- <div
610
- key={index}
611
- className={cn(
612
- "min-h-[80px] p-1 border border-border/50 cursor-pointer hover:bg-muted/50 transition-colors text-xs flex flex-col",
613
- !isCurrentMonthDate && "text-muted-foreground bg-muted/20",
614
- isTodayDate && highlightToday && "bg-primary/10 border-primary/50",
615
- isSelectedDate && "bg-primary/20 border-primary",
616
- isDisabledDate && "cursor-not-allowed opacity-50",
617
- dragTargetDate && dragTargetDate.toDateString() === date.toDateString() && "bg-primary/30 border-primary"
618
- )}
619
- onClick={() => handleDateClick(date)}
620
- onDragOver={(e) => handleDateDragOver(date, e)}
621
- onDrop={(e) => handleDateDrop(date, e)}
622
- >
623
- <div className="flex items-center justify-between mb-1">
624
- <div className="flex flex-col">
625
- <span className={cn(
626
- "text-sm font-medium",
627
- isTodayDate && "text-primary font-bold",
628
- isHoliday(date) && "text-orange-600 dark:text-orange-400"
629
- )}>
630
- {date.getDate()}
631
- </span>
632
- {isHoliday(date) && (
633
- <span className="text-[10px] text-orange-600 dark:text-orange-400 truncate">
634
- {getHolidayName(date)}
635
- </span>
636
- )}
637
- </div>
638
- {dayEvents.length > 0 && (
639
- <Badge variant="secondary" className="text-xs px-1">
640
- {dayEvents.length}
641
- </Badge>
642
- )}
643
- </div>
644
-
645
- {/* Events */}
646
- <div className="flex-1 space-y-1 overflow-hidden">
647
- {dayEvents.slice(0, 3).map((event) => (
648
- <div
649
- key={event.id}
650
- className={cn(
651
- "text-xs p-1 mb-1 rounded text-white cursor-move group relative select-none block w-full truncate",
652
- event.color || EVENT_COLORS[event.type || 'event'],
653
- event.priority && PRIORITY_COLORS[event.priority],
654
- event.status && STATUS_STYLES[event.status]
655
- )}
656
- draggable={!disabled && enableDragDrop}
657
- onClick={(e) => handleEventClick(event, e)}
658
- onDragStart={(e) => handleEventDragStart(event, e)}
659
- onDragEnd={handleEventDragEnd}
660
- style={{
661
- backgroundColor: event.color || undefined
662
- }}
663
- >
664
- <div className="flex items-center justify-between">
665
- <div className="flex items-center gap-1 flex-1 min-w-0">
666
- {event.isPrivate && <Lock className="h-3 w-3 flex-shrink-0" />}
667
- {event.recurring && <Repeat className="h-3 w-3 flex-shrink-0" />}
668
- <span className="truncate">{event.title}</span>
669
- </div>
670
- {showEventDetails && (
671
- <div className="hidden group-hover:flex items-center gap-1 ml-1">
672
- <Button
673
- variant="ghost"
674
- size="sm"
675
- className="h-4 w-4 p-0 text-white/80 hover:text-white"
676
- onClick={(e) => handleEventEdit(event, e)}
677
- >
678
- <Edit className="h-3 w-3" />
679
- </Button>
680
- <Button
681
- variant="ghost"
682
- size="sm"
683
- className="h-4 w-4 p-0 text-white/80 hover:text-white"
684
- onClick={(e) => handleEventDelete(event, e)}
685
- >
686
- <Trash2 className="h-3 w-3" />
687
- </Button>
688
- </div>
689
- )}
690
- </div>
691
- {event.startTime && !compactMode && (
692
- <div className="flex items-center gap-1 mt-1">
693
- <Clock className="h-3 w-3" />
694
- <span className="text-[10px]">{formatTime(event.startTime)}</span>
695
- </div>
696
- )}
697
- </div>
698
- ))}
699
- {dayEvents.length > 3 && (
700
- <div className="text-xs text-muted-foreground text-center">
701
- +{dayEvents.length - 3} more
702
- </div>
703
- )}
704
- </div>
705
- </div>
706
- )
707
- })}
708
- </div>
709
- )}
710
-
711
- {/* Week View */}
712
- {view === 'week' && (
713
- <div className="space-y-4">
714
- <div className="grid grid-cols-8 gap-2">
715
- <div className="text-xs font-medium text-muted-foreground">Time</div>
716
- {Array.from({ length: 7 }, (_, i) => {
717
- const date = new Date(currentDate)
718
- date.setDate(date.getDate() - date.getDay() + i)
719
- return (
720
- <div key={i} className="text-center">
721
- <div className="text-xs font-medium text-muted-foreground">
722
- {DAYS_OF_WEEK[i]}
723
- </div>
724
- <div className={cn(
725
- "text-sm font-medium",
726
- isToday(date) && "text-primary"
727
- )}>
728
- {date.getDate()}
729
- </div>
730
- </div>
731
- )
732
- })}
733
- </div>
734
-
735
- {/* Time slots */}
736
- <div className="border rounded-lg overflow-hidden">
737
- <div className="max-h-[500px] overflow-y-auto">
738
- {Array.from({ length: 24 }, (_, hour) => (
739
- <div key={hour} className="grid grid-cols-8 border-b last:border-b-0">
740
- <div className="p-2 text-xs text-muted-foreground border-r">
741
- {formatTime(`${hour.toString().padStart(2, '0')}:00`)}
742
- </div>
743
- {Array.from({ length: 7 }, (_, dayIndex) => {
744
- const date = new Date(currentDate)
745
- date.setDate(date.getDate() - date.getDay() + dayIndex)
746
- const hourEvents = getEventsForDate(date).filter(event => {
747
- if (!event.startTime) return false
748
- const eventHour = parseInt(event.startTime.split(':')[0])
749
- return eventHour === hour
750
- })
751
-
752
- return (
753
- <div
754
- key={dayIndex}
755
- className={cn(
756
- "p-1 min-h-[60px] border-r last:border-r-0 hover:bg-muted/50 transition-colors",
757
- isToday(date) && "bg-primary/5"
758
- )}
759
- onClick={() => handleDateClick(date)}
760
- >
761
- {hourEvents.map(event => (
762
- <div
763
- key={event.id}
764
- className={cn(
765
- "text-xs p-1 rounded text-white mb-1 cursor-pointer",
766
- event.color || EVENT_COLORS[event.type || 'event']
767
- )}
768
- onClick={(e) => handleEventClick(event, e)}
769
- >
770
- <div className="font-medium truncate">{event.title}</div>
771
- {event.location && (
772
- <div className="text-[10px] opacity-80 truncate">{event.location}</div>
773
- )}
774
- </div>
775
- ))}
776
- </div>
777
- )
778
- })}
779
- </div>
780
- ))}
781
- </div>
782
- </div>
783
- </div>
784
- )}
785
-
786
- {/* Day View */}
787
- {view === 'day' && (
788
- <div className="space-y-4">
789
- <div className="text-center">
790
- <h3 className="text-lg font-semibold">
791
- {currentDate.toLocaleDateString(locale, {
792
- weekday: 'long',
793
- year: 'numeric',
794
- month: 'long',
795
- day: 'numeric'
796
- })}
797
- </h3>
798
- </div>
799
-
800
- <div className="border rounded-lg overflow-hidden">
801
- <div className="max-h-[500px] overflow-y-auto">
802
- {Array.from({ length: 24 }, (_, hour) => {
803
- const hourEvents = getEventsForDate(currentDate).filter(event => {
804
- if (!event.startTime) return false
805
- const eventHour = parseInt(event.startTime.split(':')[0])
806
- return eventHour === hour
807
- })
808
-
809
- return (
810
- <div key={hour} className="flex border-b last:border-b-0">
811
- <div className="w-20 p-3 text-sm text-muted-foreground border-r">
812
- {formatTime(`${hour.toString().padStart(2, '0')}:00`)}
813
- </div>
814
- <div className="flex-1 p-2 min-h-[80px]">
815
- {hourEvents.map(event => (
816
- <div
817
- key={event.id}
818
- className={cn(
819
- "p-2 rounded text-white mb-2 cursor-pointer",
820
- event.color || EVENT_COLORS[event.type || 'event']
821
- )}
822
- onClick={(e) => handleEventClick(event, e)}
823
- >
824
- <div className="flex items-center justify-between">
825
- <div>
826
- <div className="font-medium">{event.title}</div>
827
- {event.description && (
828
- <div className="text-sm opacity-80">{event.description}</div>
829
- )}
830
- <div className="flex items-center gap-3 text-xs mt-1">
831
- {event.startTime && (
832
- <div className="flex items-center gap-1">
833
- <Clock className="h-3 w-3" />
834
- {formatTime(event.startTime)} - {formatTime(event.endTime || event.startTime)}
835
- </div>
836
- )}
837
- {event.location && (
838
- <div className="flex items-center gap-1">
839
- <MapPin className="h-3 w-3" />
840
- {event.location}
841
- </div>
842
- )}
843
- </div>
844
- </div>
845
- <div className="flex gap-1">
846
- <Button
847
- variant="ghost"
848
- size="sm"
849
- className="h-6 w-6 p-0 text-white/80 hover:text-white"
850
- onClick={(e) => handleEventEdit(event, e)}
851
- >
852
- <Edit className="h-3 w-3" />
853
- </Button>
854
- <Button
855
- variant="ghost"
856
- size="sm"
857
- className="h-6 w-6 p-0 text-white/80 hover:text-white"
858
- onClick={(e) => handleEventDelete(event, e)}
859
- >
860
- <Trash2 className="h-3 w-3" />
861
- </Button>
862
- </div>
863
- </div>
864
- </div>
865
- ))}
866
- </div>
867
- </div>
868
- )
869
- })}
870
- </div>
871
- </div>
872
- </div>
873
- )}
874
-
875
- {/* Agenda View */}
876
- {view === 'agenda' && (
877
- <div className="space-y-4">
878
- <div className="max-h-[500px] overflow-y-auto space-y-4">
879
- {Array.from({ length: 30 }, (_, i) => {
880
- const date = new Date(currentDate)
881
- date.setDate(date.getDate() + i)
882
- const dayEvents = getEventsForDate(date)
883
-
884
- if (dayEvents.length === 0) return null
885
-
886
- return (
887
- <div key={i} className="border rounded-lg p-4">
888
- <h4 className="font-medium mb-3">
889
- {date.toLocaleDateString(locale, {
890
- weekday: 'long',
891
- year: 'numeric',
892
- month: 'long',
893
- day: 'numeric'
894
- })}
895
- </h4>
896
- <div className="space-y-2">
897
- {dayEvents.map(event => (
898
- <div
899
- key={event.id}
900
- className="flex items-start gap-3 p-3 rounded-lg border hover:bg-muted/50 transition-colors cursor-pointer"
901
- onClick={() => onEventClick?.(event)}
902
- >
903
- <div className={cn(
904
- "w-3 h-3 rounded-full mt-1 flex-shrink-0",
905
- event.color || EVENT_COLORS[event.type || 'event']
906
- )} />
907
- <div className="flex-1">
908
- <div className="flex items-center justify-between">
909
- <h5 className="font-medium">{event.title}</h5>
910
- <div className="flex gap-1">
911
- <Button
912
- variant="ghost"
913
- size="sm"
914
- className="h-7 w-7 p-0"
915
- onClick={(e) => handleEventEdit(event, e)}
916
- >
917
- <Edit className="h-3 w-3" />
918
- </Button>
919
- <Button
920
- variant="ghost"
921
- size="sm"
922
- className="h-7 w-7 p-0"
923
- onClick={(e) => handleEventDelete(event, e)}
924
- >
925
- <Trash2 className="h-3 w-3" />
926
- </Button>
927
- </div>
928
- </div>
929
- {event.description && (
930
- <p className="text-sm text-muted-foreground mt-1">
931
- {event.description}
932
- </p>
933
- )}
934
- <div className="flex items-center gap-4 text-xs text-muted-foreground mt-2">
935
- {event.startTime && (
936
- <div className="flex items-center gap-1">
937
- <Clock className="h-3 w-3" />
938
- {formatTime(event.startTime)} - {formatTime(event.endTime || event.startTime)}
939
- </div>
940
- )}
941
- {event.location && (
942
- <div className="flex items-center gap-1">
943
- <MapPin className="h-3 w-3" />
944
- {event.location}
945
- </div>
946
- )}
947
- {event.attendees && event.attendees.length > 0 && (
948
- <div className="flex items-center gap-1">
949
- <Users className="h-3 w-3" />
950
- {event.attendees.length} attendees
951
- </div>
952
- )}
953
- </div>
954
- </div>
955
- </div>
956
- ))}
957
- </div>
958
- </div>
959
- )
960
- }).filter(Boolean)}
961
- </div>
962
- </div>
963
- )}
964
-
965
- {/* Selected Date Details */}
966
- {selectedDate && view === 'month' && (
967
- <div className="border rounded-lg p-4 bg-muted/50">
968
- <h4 className="font-medium mb-2">
969
- {selectedDate.toLocaleDateString('en-US', {
970
- weekday: 'long',
971
- year: 'numeric',
972
- month: 'long',
973
- day: 'numeric'
974
- })}
975
- </h4>
976
-
977
- <div className="space-y-2">
978
- {getEventsForDate(selectedDate).map((event) => (
979
- <div key={event.id} className="flex items-start gap-3 p-2 border rounded">
980
- <div className={cn(
981
- "w-3 h-3 rounded-full mt-1 flex-shrink-0",
982
- event.color || EVENT_COLORS[event.type || 'event']
983
- )} />
984
- <div className="flex-1">
985
- <div className="flex items-center justify-between">
986
- <h5 className="font-medium">{event.title}</h5>
987
- <div className="flex items-center gap-1">
988
- <Button
989
- variant="ghost"
990
- size="sm"
991
- onClick={(e) => handleEventEdit(event, e)}
992
- >
993
- <Edit className="h-4 w-4" />
994
- </Button>
995
- <Button
996
- variant="ghost"
997
- size="sm"
998
- onClick={(e) => handleEventDelete(event, e)}
999
- >
1000
- <Trash2 className="h-4 w-4" />
1001
- </Button>
1002
- </div>
1003
- </div>
1004
- {event.description && (
1005
- <p className="text-sm text-muted-foreground mt-1">
1006
- {event.description}
1007
- </p>
1008
- )}
1009
- <div className="flex items-center gap-4 text-xs text-muted-foreground mt-2">
1010
- {event.startTime && (
1011
- <div className="flex items-center gap-1">
1012
- <Clock className="h-3 w-3" />
1013
- <span>{event.startTime} - {event.endTime}</span>
1014
- </div>
1015
- )}
1016
- {event.location && (
1017
- <div className="flex items-center gap-1">
1018
- <MapPin className="h-3 w-3" />
1019
- <span>{event.location}</span>
1020
- </div>
1021
- )}
1022
- {event.attendees && event.attendees.length > 0 && (
1023
- <div className="flex items-center gap-1">
1024
- <User className="h-3 w-3" />
1025
- <span>{event.attendees.length} attendees</span>
1026
- </div>
1027
- )}
1028
- </div>
1029
- </div>
1030
- </div>
1031
- ))}
1032
-
1033
- {getEventsForDate(selectedDate).length === 0 && (
1034
- <div className="text-center py-4">
1035
- <p className="text-sm text-muted-foreground mb-2">
1036
- No events scheduled for this date
1037
- </p>
1038
- <Button
1039
- variant="outline"
1040
- size="sm"
1041
- onClick={() => {
1042
- setEventDialogMode('create')
1043
- setSelectedEvent(null)
1044
- setEventDialogOpen(true)
1045
- }}
1046
- >
1047
- <Plus className="h-4 w-4 mr-1" />
1048
- Add Event
1049
- </Button>
1050
- </div>
1051
- )}
1052
- </div>
1053
- </div>
1054
- )}
1055
- </div>
1056
- </CardContent>
1057
- </Card>
1058
-
1059
- {/* Event Dialog */}
1060
- <EventDialog
1061
- open={eventDialogOpen}
1062
- onOpenChange={setEventDialogOpen}
1063
- event={selectedEvent}
1064
- selectedDate={selectedDate}
1065
- onSave={handleEventSave}
1066
- onDelete={handleEventDialogDelete}
1067
- mode={eventDialogMode}
1068
- />
1069
- </>
1070
- )
1071
- }
1072
-
1073
- export default Calendar