@moontra/moonui-pro 2.15.3 → 2.16.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.
- package/dist/index.d.ts +58 -2
- package/dist/index.mjs +366 -22
- package/package.json +1 -1
- package/src/components/calendar/index.tsx +575 -33
|
@@ -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'>(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
269
|
-
|
|
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
|
-
|
|
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
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
{day}
|
|
324
|
-
|
|
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
|
-
|
|
328
|
-
|
|
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
|
-
<
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
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', {
|