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