@moontra/moonui-pro 2.15.3 → 2.17.0

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.
@@ -4,6 +4,9 @@ import React from 'react'
4
4
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'
5
5
  import { Button } from '../ui/button'
6
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'
7
10
  import {
8
11
  ChevronLeft,
9
12
  ChevronRight,
@@ -15,10 +18,31 @@ import {
15
18
  MapPin,
16
19
  User,
17
20
  Lock,
18
- Sparkles
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
19
42
  } from 'lucide-react'
20
43
  import { cn } from '../../lib/utils'
21
44
  import { EventDialog } from './event-dialog'
45
+ import { motion, AnimatePresence } from 'framer-motion'
22
46
 
23
47
  export interface CalendarEvent {
24
48
  id: string
@@ -30,7 +54,27 @@ export interface CalendarEvent {
30
54
  location?: string
31
55
  attendees?: string[]
32
56
  color?: string
33
- type?: 'meeting' | 'task' | 'reminder' | 'event'
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
34
78
  }
35
79
 
36
80
  interface CalendarProps {
@@ -39,6 +83,9 @@ interface CalendarProps {
39
83
  onEventAdd?: (eventData: Omit<CalendarEvent, 'id'> & { id?: string }) => void
40
84
  onEventEdit?: (eventData: Omit<CalendarEvent, 'id'> & { id: string }) => void
41
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
42
89
  className?: string
43
90
  showWeekends?: boolean
44
91
  showEventDetails?: boolean
@@ -47,6 +94,29 @@ interface CalendarProps {
47
94
  maxDate?: Date
48
95
  highlightToday?: boolean
49
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
50
120
  // For NPM package usage - allows external control of pro access
51
121
  showProUpgrade?: boolean
52
122
  }
@@ -61,7 +131,22 @@ const EVENT_COLORS = {
61
131
  meeting: 'bg-blue-500',
62
132
  task: 'bg-green-500',
63
133
  reminder: 'bg-yellow-500',
64
- event: 'bg-purple-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-gray-400',
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'
65
150
  }
66
151
 
67
152
  export function Calendar({
@@ -70,6 +155,9 @@ export function Calendar({
70
155
  onEventAdd,
71
156
  onEventEdit,
72
157
  onEventDelete,
158
+ onDateChange,
159
+ onViewChange,
160
+ onEventDrop,
73
161
  className,
74
162
  showWeekends = true,
75
163
  showEventDetails = true,
@@ -78,6 +166,23 @@ export function Calendar({
78
166
  maxDate,
79
167
  highlightToday = true,
80
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,
81
186
  showProUpgrade = false
82
187
  }: CalendarProps) {
83
188
  // For NPM package usage, show upgrade prompt if specified
@@ -111,12 +216,18 @@ export function Calendar({
111
216
 
112
217
  const [currentDate, setCurrentDate] = React.useState(new Date())
113
218
  const [selectedDate, setSelectedDate] = React.useState<Date | null>(null)
114
- const [view, setView] = React.useState<'month' | 'week' | 'day'>('month')
219
+ const [view, setView] = React.useState<'month' | 'week' | 'day' | 'year' | 'agenda'>(defaultView)
115
220
  const [eventDialogOpen, setEventDialogOpen] = React.useState(false)
116
221
  const [eventDialogMode, setEventDialogMode] = React.useState<'create' | 'edit'>('create')
117
222
  const [selectedEvent, setSelectedEvent] = React.useState<CalendarEvent | null>(null)
118
223
  const [draggedEvent, setDraggedEvent] = React.useState<CalendarEvent | null>(null)
119
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())
120
231
 
121
232
  const today = new Date()
122
233
  const currentMonth = currentDate.getMonth()
@@ -125,10 +236,14 @@ export function Calendar({
125
236
  const firstDayOfMonth = new Date(currentYear, currentMonth, 1)
126
237
  const lastDayOfMonth = new Date(currentYear, currentMonth + 1, 0)
127
238
  const startDate = new Date(firstDayOfMonth)
128
- startDate.setDate(startDate.getDate() - startDate.getDay())
239
+
240
+ // Adjust start date based on firstDayOfWeek setting
241
+ const daysToSubtract = (startDate.getDay() - firstDayOfWeek + 7) % 7
242
+ startDate.setDate(startDate.getDate() - daysToSubtract)
129
243
 
130
244
  const endDate = new Date(lastDayOfMonth)
131
- endDate.setDate(endDate.getDate() + (6 - endDate.getDay()))
245
+ const daysToAdd = (6 - endDate.getDay() + firstDayOfWeek) % 7
246
+ endDate.setDate(endDate.getDate() + daysToAdd)
132
247
 
133
248
  const calendarDays = []
134
249
  const currentDateIterator = new Date(startDate)
@@ -138,13 +253,92 @@ export function Calendar({
138
253
  currentDateIterator.setDate(currentDateIterator.getDate() + 1)
139
254
  }
140
255
 
141
- const getEventsForDate = (date: Date) => {
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(() => {
142
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 => {
143
295
  const eventDate = new Date(event.date)
296
+
297
+ // Check for recurring events
298
+ if (event.recurring) {
299
+ return isDateInRecurringPattern(date, event)
300
+ }
301
+
144
302
  return eventDate.toDateString() === date.toDateString()
145
303
  })
146
304
  }
147
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
+
148
342
  const isToday = (date: Date) => {
149
343
  return date.toDateString() === today.toDateString()
150
344
  }
@@ -172,6 +366,51 @@ export function Calendar({
172
366
  newDate.setMonth(currentMonth + 1)
173
367
  }
174
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'
175
414
  }
176
415
 
177
416
  const handleDateClick = (date: Date) => {
@@ -265,8 +504,36 @@ export function Calendar({
265
504
  }
266
505
 
267
506
  const goToToday = () => {
268
- setCurrentDate(new Date())
269
- setSelectedDate(new Date())
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}`
270
537
  }
271
538
 
272
539
  const filteredDays = showWeekends ? calendarDays : calendarDays.filter(day => {
@@ -274,7 +541,12 @@ export function Calendar({
274
541
  return dayOfWeek !== 0 && dayOfWeek !== 6
275
542
  })
276
543
 
277
- const visibleDaysOfWeek = showWeekends ? DAYS_OF_WEEK : DAYS_OF_WEEK.slice(1, 6)
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
+ })
278
550
 
279
551
  return (
280
552
  <>
@@ -315,17 +587,18 @@ export function Calendar({
315
587
  </CardHeader>
316
588
  <CardContent>
317
589
  <div className="space-y-4">
318
- {/* Calendar Grid */}
319
- <div className="grid grid-cols-7 gap-1 w-full">
320
- {/* Day Headers */}
321
- {visibleDaysOfWeek.map((day) => (
322
- <div key={day} className="p-1 text-center text-xs font-medium text-muted-foreground">
323
- {day}
324
- </div>
325
- ))}
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
+ ))}
326
599
 
327
- {/* Calendar Days */}
328
- {filteredDays.map((date, index) => {
600
+ {/* Calendar Days */}
601
+ {filteredDays.map((date, index) => {
329
602
  const dayEvents = getEventsForDate(date)
330
603
  const isCurrentMonthDate = isCurrentMonth(date)
331
604
  const isTodayDate = isToday(date)
@@ -348,12 +621,20 @@ export function Calendar({
348
621
  onDrop={(e) => handleDateDrop(date, e)}
349
622
  >
350
623
  <div className="flex items-center justify-between mb-1">
351
- <span className={cn(
352
- "text-sm font-medium",
353
- isTodayDate && "text-primary font-bold"
354
- )}>
355
- {date.getDate()}
356
- </span>
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>
357
638
  {dayEvents.length > 0 && (
358
639
  <Badge variant="secondary" className="text-xs px-1">
359
640
  {dayEvents.length}
@@ -368,9 +649,11 @@ export function Calendar({
368
649
  key={event.id}
369
650
  className={cn(
370
651
  "text-xs p-1 mb-1 rounded text-white cursor-move group relative select-none block w-full truncate",
371
- event.color || EVENT_COLORS[event.type || 'event']
652
+ event.color || EVENT_COLORS[event.type || 'event'],
653
+ event.priority && PRIORITY_COLORS[event.priority],
654
+ event.status && STATUS_STYLES[event.status]
372
655
  )}
373
- draggable={!disabled}
656
+ draggable={!disabled && enableDragDrop}
374
657
  onClick={(e) => handleEventClick(event, e)}
375
658
  onDragStart={(e) => handleEventDragStart(event, e)}
376
659
  onDragEnd={handleEventDragEnd}
@@ -379,7 +662,11 @@ export function Calendar({
379
662
  }}
380
663
  >
381
664
  <div className="flex items-center justify-between">
382
- <span className="truncate flex-1">{event.title}</span>
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>
383
670
  {showEventDetails && (
384
671
  <div className="hidden group-hover:flex items-center gap-1 ml-1">
385
672
  <Button
@@ -401,10 +688,10 @@ export function Calendar({
401
688
  </div>
402
689
  )}
403
690
  </div>
404
- {event.startTime && (
691
+ {event.startTime && !compactMode && (
405
692
  <div className="flex items-center gap-1 mt-1">
406
693
  <Clock className="h-3 w-3" />
407
- <span>{event.startTime}</span>
694
+ <span className="text-[10px]">{formatTime(event.startTime)}</span>
408
695
  </div>
409
696
  )}
410
697
  </div>
@@ -418,10 +705,265 @@ export function Calendar({
418
705
  </div>
419
706
  )
420
707
  })}
421
- </div>
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
+ )}
422
964
 
423
965
  {/* Selected Date Details */}
424
- {selectedDate && (
966
+ {selectedDate && view === 'month' && (
425
967
  <div className="border rounded-lg p-4 bg-muted/50">
426
968
  <h4 className="font-medium mb-2">
427
969
  {selectedDate.toLocaleDateString('en-US', {