@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.
- package/package.json +8 -3
- package/plugin/index.d.ts +86 -0
- package/plugin/index.js +308 -0
- package/scripts/postinstall.js +176 -23
- package/src/components/advanced-chart/index.tsx +0 -1246
- package/src/components/advanced-forms/index.tsx +0 -585
- package/src/components/animated-button/index.tsx +0 -385
- package/src/components/calendar/event-dialog.tsx +0 -377
- package/src/components/calendar/index.tsx +0 -1220
- package/src/components/calendar-pro/index.tsx +0 -1697
- package/src/components/color-picker/index.tsx +0 -432
- package/src/components/credit-card-input/index.tsx +0 -406
- package/src/components/dashboard/dashboard-grid.tsx +0 -480
- package/src/components/dashboard/demo.tsx +0 -425
- package/src/components/dashboard/index.tsx +0 -1046
- package/src/components/dashboard/time-range-picker.tsx +0 -336
- package/src/components/dashboard/types.ts +0 -225
- package/src/components/dashboard/widgets/activity-feed.tsx +0 -349
- package/src/components/dashboard/widgets/chart-widget.tsx +0 -418
- package/src/components/dashboard/widgets/comparison-widget.tsx +0 -177
- package/src/components/dashboard/widgets/index.ts +0 -5
- package/src/components/dashboard/widgets/metric-card.tsx +0 -363
- package/src/components/dashboard/widgets/progress-widget.tsx +0 -113
- package/src/components/data-table/data-table-bulk-actions.tsx +0 -204
- package/src/components/data-table/data-table-column-toggle.tsx +0 -169
- package/src/components/data-table/data-table-export.ts +0 -156
- package/src/components/data-table/data-table-filter-drawer.tsx +0 -448
- package/src/components/data-table/index.tsx +0 -845
- package/src/components/draggable-list/index.tsx +0 -100
- package/src/components/error-boundary/index.tsx +0 -232
- package/src/components/file-upload/index.tsx +0 -1660
- package/src/components/floating-action-button/index.tsx +0 -206
- package/src/components/form-wizard/form-wizard-context.tsx +0 -335
- package/src/components/form-wizard/form-wizard-navigation.tsx +0 -118
- package/src/components/form-wizard/form-wizard-progress.tsx +0 -329
- package/src/components/form-wizard/form-wizard-step.tsx +0 -111
- package/src/components/form-wizard/index.tsx +0 -102
- package/src/components/form-wizard/types.ts +0 -77
- package/src/components/gesture-drawer/index.tsx +0 -551
- package/src/components/github-stars/github-api.ts +0 -426
- package/src/components/github-stars/hooks.ts +0 -517
- package/src/components/github-stars/index.tsx +0 -375
- package/src/components/github-stars/types.ts +0 -148
- package/src/components/github-stars/variants.tsx +0 -515
- package/src/components/health-check/index.tsx +0 -439
- package/src/components/hover-card-3d/index.tsx +0 -529
- package/src/components/index.ts +0 -130
- package/src/components/internal/index.ts +0 -78
- package/src/components/kanban/add-card-modal.tsx +0 -502
- package/src/components/kanban/card-detail-modal.tsx +0 -761
- package/src/components/kanban/index.ts +0 -13
- package/src/components/kanban/kanban.tsx +0 -1689
- package/src/components/kanban/types.ts +0 -168
- package/src/components/lazy-component/index.tsx +0 -823
- package/src/components/license-error/index.tsx +0 -31
- package/src/components/magnetic-button/index.tsx +0 -216
- package/src/components/memory-efficient-data/index.tsx +0 -1018
- package/src/components/moonui-quiz-form/index.tsx +0 -817
- package/src/components/navbar/index.tsx +0 -781
- package/src/components/optimized-image/index.tsx +0 -425
- package/src/components/performance-debugger/index.tsx +0 -613
- package/src/components/performance-monitor/index.tsx +0 -808
- package/src/components/phone-number-input/index.tsx +0 -343
- package/src/components/phone-number-input/phone-number-input-simple.tsx +0 -167
- package/src/components/pinch-zoom/index.tsx +0 -566
- package/src/components/quiz-form/index.tsx +0 -479
- package/src/components/rich-text-editor/index.tsx +0 -2322
- package/src/components/rich-text-editor/slash-commands-extension.ts +0 -230
- package/src/components/rich-text-editor/slash-commands.css +0 -35
- package/src/components/rich-text-editor/table-styles.css +0 -65
- package/src/components/sidebar/index.tsx +0 -884
- package/src/components/spotlight-card/index.tsx +0 -191
- package/src/components/swipeable-card/index.tsx +0 -100
- package/src/components/timeline/index.tsx +0 -1183
- package/src/components/ui/accordion.tsx +0 -581
- package/src/components/ui/alert-dialog.tsx +0 -141
- package/src/components/ui/alert.tsx +0 -141
- package/src/components/ui/aspect-ratio.tsx +0 -245
- package/src/components/ui/avatar.tsx +0 -155
- package/src/components/ui/badge.tsx +0 -230
- package/src/components/ui/breadcrumb.tsx +0 -216
- package/src/components/ui/button.tsx +0 -228
- package/src/components/ui/calendar.tsx +0 -387
- package/src/components/ui/card.tsx +0 -216
- package/src/components/ui/checkbox.tsx +0 -259
- package/src/components/ui/collapsible.tsx +0 -631
- package/src/components/ui/color-picker.tsx +0 -97
- package/src/components/ui/command.tsx +0 -948
- package/src/components/ui/dialog.tsx +0 -752
- package/src/components/ui/dropdown-menu.tsx +0 -706
- package/src/components/ui/gesture-drawer.tsx +0 -11
- package/src/components/ui/hover-card.tsx +0 -29
- package/src/components/ui/index.ts +0 -222
- package/src/components/ui/input.tsx +0 -224
- package/src/components/ui/label.tsx +0 -29
- package/src/components/ui/lightbox.tsx +0 -606
- package/src/components/ui/magnetic-button.tsx +0 -129
- package/src/components/ui/media-gallery.tsx +0 -611
- package/src/components/ui/navigation-menu.tsx +0 -130
- package/src/components/ui/pagination.tsx +0 -125
- package/src/components/ui/popover.tsx +0 -185
- package/src/components/ui/progress.tsx +0 -30
- package/src/components/ui/radio-group.tsx +0 -257
- package/src/components/ui/scroll-area.tsx +0 -47
- package/src/components/ui/select.tsx +0 -378
- package/src/components/ui/separator.tsx +0 -145
- package/src/components/ui/sheet.tsx +0 -139
- package/src/components/ui/skeleton.tsx +0 -20
- package/src/components/ui/slider.tsx +0 -354
- package/src/components/ui/spotlight-card.tsx +0 -119
- package/src/components/ui/switch.tsx +0 -86
- package/src/components/ui/table.tsx +0 -331
- package/src/components/ui/tabs-pro.tsx +0 -542
- package/src/components/ui/tabs.tsx +0 -54
- package/src/components/ui/textarea.tsx +0 -28
- package/src/components/ui/toast.tsx +0 -317
- package/src/components/ui/toggle.tsx +0 -119
- package/src/components/ui/tooltip.tsx +0 -151
- package/src/components/virtual-list/index.tsx +0 -668
- package/src/hooks/use-chart.ts +0 -205
- package/src/hooks/use-data-table.ts +0 -182
- package/src/hooks/use-docs-pro-access.ts +0 -13
- package/src/hooks/use-license-check.ts +0 -65
- package/src/hooks/use-subscription.ts +0 -19
- package/src/hooks/use-toast.ts +0 -15
- package/src/index.ts +0 -22
- package/src/lib/ai-providers.ts +0 -377
- package/src/lib/component-metadata.ts +0 -18
- package/src/lib/micro-interactions.ts +0 -255
- package/src/lib/paddle.ts +0 -17
- package/src/lib/utils.ts +0 -6
- package/src/patterns/login-form/index.tsx +0 -276
- package/src/patterns/login-form/types.ts +0 -67
- package/src/setupTests.ts +0 -41
- package/src/styles/advanced-chart.css +0 -239
- package/src/styles/calendar.css +0 -35
- package/src/styles/design-system.css +0 -363
- package/src/styles/index.css +0 -681
- package/src/styles/tailwind.css +0 -7
- package/src/styles/tokens.css +0 -455
- package/src/types/next-auth.d.ts +0 -21
- package/src/use-intersection-observer.tsx +0 -154
- package/src/use-local-storage.tsx +0 -71
- package/src/use-paddle.ts +0 -138
- package/src/use-performance-optimizer.ts +0 -389
- package/src/use-pro-access.ts +0 -141
- package/src/use-scroll-animation.ts +0 -219
- package/src/use-subscription.ts +0 -37
- package/src/use-toast.ts +0 -32
- package/src/utils/chart-helpers.ts +0 -357
- package/src/utils/cn.ts +0 -6
- package/src/utils/data-processing.ts +0 -151
- 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
|