@moontra/moonui-pro 2.0.22 → 2.0.23
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 +2 -1
- package/src/__tests__/use-intersection-observer.test.tsx +216 -0
- package/src/__tests__/use-local-storage.test.tsx +174 -0
- package/src/__tests__/use-pro-access.test.tsx +183 -0
- package/src/components/advanced-chart/advanced-chart.test.tsx +281 -0
- package/src/components/advanced-chart/index.tsx +412 -0
- package/src/components/advanced-forms/index.tsx +431 -0
- package/src/components/animated-button/index.tsx +202 -0
- package/src/components/calendar/event-dialog.tsx +372 -0
- package/src/components/calendar/index.tsx +531 -0
- package/src/components/color-picker/index.tsx +434 -0
- package/src/components/dashboard/index.tsx +334 -0
- package/src/components/data-table/data-table.test.tsx +187 -0
- package/src/components/data-table/index.tsx +368 -0
- package/src/components/draggable-list/index.tsx +100 -0
- package/src/components/enhanced/button.tsx +360 -0
- package/src/components/enhanced/card.tsx +272 -0
- package/src/components/enhanced/dialog.tsx +248 -0
- package/src/components/enhanced/index.ts +3 -0
- package/src/components/error-boundary/index.tsx +111 -0
- package/src/components/file-upload/file-upload.test.tsx +242 -0
- package/src/components/file-upload/index.tsx +362 -0
- package/src/components/floating-action-button/index.tsx +209 -0
- package/src/components/github-stars/index.tsx +414 -0
- package/src/components/health-check/index.tsx +441 -0
- package/src/components/hover-card-3d/index.tsx +170 -0
- package/src/components/index.ts +76 -0
- package/src/components/kanban/index.tsx +436 -0
- package/src/components/lazy-component/index.tsx +342 -0
- package/src/components/magnetic-button/index.tsx +170 -0
- package/src/components/memory-efficient-data/index.tsx +352 -0
- package/src/components/optimized-image/index.tsx +427 -0
- package/src/components/performance-debugger/index.tsx +591 -0
- package/src/components/performance-monitor/index.tsx +775 -0
- package/src/components/pinch-zoom/index.tsx +172 -0
- package/src/components/rich-text-editor/index-old-backup.tsx +443 -0
- package/src/components/rich-text-editor/index.tsx +1537 -0
- package/src/components/rich-text-editor/slash-commands-extension.ts +220 -0
- package/src/components/rich-text-editor/slash-commands.css +35 -0
- package/src/components/rich-text-editor/table-styles.css +65 -0
- package/src/components/spotlight-card/index.tsx +194 -0
- package/src/components/swipeable-card/index.tsx +100 -0
- package/src/components/timeline/index.tsx +333 -0
- package/src/components/ui/animated-button.tsx +185 -0
- package/src/components/ui/avatar.tsx +135 -0
- package/src/components/ui/badge.tsx +225 -0
- package/src/components/ui/button.tsx +221 -0
- package/src/components/ui/card.tsx +141 -0
- package/src/components/ui/checkbox.tsx +256 -0
- package/src/components/ui/color-picker.tsx +95 -0
- package/src/components/ui/dialog.tsx +332 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/hover-card-3d.tsx +103 -0
- package/src/components/ui/index.ts +33 -0
- package/src/components/ui/input.tsx +219 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/magnetic-button.tsx +129 -0
- package/src/components/ui/popover.tsx +183 -0
- package/src/components/ui/select.tsx +273 -0
- package/src/components/ui/separator.tsx +140 -0
- package/src/components/ui/slider.tsx +351 -0
- package/src/components/ui/spotlight-card.tsx +119 -0
- package/src/components/ui/switch.tsx +83 -0
- package/src/components/ui/tabs.tsx +195 -0
- package/src/components/ui/textarea.tsx +25 -0
- package/src/components/ui/toast.tsx +313 -0
- package/src/components/ui/tooltip.tsx +152 -0
- package/src/components/virtual-list/index.tsx +369 -0
- package/src/hooks/use-chart.ts +205 -0
- package/src/hooks/use-data-table.ts +182 -0
- package/src/hooks/use-docs-pro-access.ts +13 -0
- package/src/hooks/use-license-check.ts +65 -0
- package/src/hooks/use-subscription.ts +19 -0
- package/src/index.ts +11 -0
- package/src/lib/micro-interactions.ts +255 -0
- package/src/lib/utils.ts +6 -0
- package/src/patterns/login-form/index.tsx +276 -0
- package/src/patterns/login-form/types.ts +67 -0
- package/src/setupTests.ts +41 -0
- package/src/styles/design-system.css +365 -0
- package/src/styles/index.css +4 -0
- package/src/styles/tailwind.css +6 -0
- package/src/styles/tokens.css +453 -0
- package/src/types/moonui.d.ts +22 -0
- package/src/use-intersection-observer.tsx +154 -0
- package/src/use-local-storage.tsx +71 -0
- package/src/use-paddle.ts +138 -0
- package/src/use-performance-optimizer.ts +379 -0
- package/src/use-pro-access.ts +141 -0
- package/src/use-scroll-animation.ts +221 -0
- package/src/use-subscription.ts +37 -0
- package/src/use-toast.ts +32 -0
- package/src/utils/chart-helpers.ts +257 -0
- package/src/utils/cn.ts +69 -0
- package/src/utils/data-processing.ts +151 -0
- package/src/utils/license-validator.tsx +183 -0
|
@@ -0,0 +1,531 @@
|
|
|
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 { Badge } from '../ui/badge'
|
|
7
|
+
import {
|
|
8
|
+
ChevronLeft,
|
|
9
|
+
ChevronRight,
|
|
10
|
+
Calendar as CalendarIcon,
|
|
11
|
+
Plus,
|
|
12
|
+
Edit,
|
|
13
|
+
Trash2,
|
|
14
|
+
Clock,
|
|
15
|
+
MapPin,
|
|
16
|
+
User,
|
|
17
|
+
Lock,
|
|
18
|
+
Sparkles
|
|
19
|
+
} from 'lucide-react'
|
|
20
|
+
import { cn } from '../../lib/utils'
|
|
21
|
+
import { EventDialog } from './event-dialog'
|
|
22
|
+
|
|
23
|
+
export interface CalendarEvent {
|
|
24
|
+
id: string
|
|
25
|
+
title: string
|
|
26
|
+
description?: string
|
|
27
|
+
date: Date
|
|
28
|
+
startTime?: string
|
|
29
|
+
endTime?: string
|
|
30
|
+
location?: string
|
|
31
|
+
attendees?: string[]
|
|
32
|
+
color?: string
|
|
33
|
+
type?: 'meeting' | 'task' | 'reminder' | 'event'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CalendarProps {
|
|
37
|
+
events?: CalendarEvent[]
|
|
38
|
+
onEventClick?: (event: CalendarEvent) => void
|
|
39
|
+
onEventAdd?: (eventData: Omit<CalendarEvent, 'id'> & { id?: string }) => void
|
|
40
|
+
onEventEdit?: (eventData: Omit<CalendarEvent, 'id'> & { id: string }) => void
|
|
41
|
+
onEventDelete?: (eventId: string) => void
|
|
42
|
+
className?: string
|
|
43
|
+
showWeekends?: boolean
|
|
44
|
+
showEventDetails?: boolean
|
|
45
|
+
disabled?: boolean
|
|
46
|
+
minDate?: Date
|
|
47
|
+
maxDate?: Date
|
|
48
|
+
highlightToday?: boolean
|
|
49
|
+
height?: number
|
|
50
|
+
// For NPM package usage - allows external control of pro access
|
|
51
|
+
showProUpgrade?: boolean
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const DAYS_OF_WEEK = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
|
55
|
+
const MONTHS = [
|
|
56
|
+
'January', 'February', 'March', 'April', 'May', 'June',
|
|
57
|
+
'July', 'August', 'September', 'October', 'November', 'December'
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
const EVENT_COLORS = {
|
|
61
|
+
meeting: 'bg-blue-500',
|
|
62
|
+
task: 'bg-green-500',
|
|
63
|
+
reminder: 'bg-yellow-500',
|
|
64
|
+
event: 'bg-purple-500'
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function Calendar({
|
|
68
|
+
events = [],
|
|
69
|
+
onEventClick,
|
|
70
|
+
onEventAdd,
|
|
71
|
+
onEventEdit,
|
|
72
|
+
onEventDelete,
|
|
73
|
+
className,
|
|
74
|
+
showWeekends = true,
|
|
75
|
+
showEventDetails = true,
|
|
76
|
+
disabled = false,
|
|
77
|
+
minDate,
|
|
78
|
+
maxDate,
|
|
79
|
+
highlightToday = true,
|
|
80
|
+
height,
|
|
81
|
+
showProUpgrade = false
|
|
82
|
+
}: CalendarProps) {
|
|
83
|
+
// For NPM package usage, show upgrade prompt if specified
|
|
84
|
+
if (showProUpgrade) {
|
|
85
|
+
return (
|
|
86
|
+
<Card className={cn("w-full", className)}>
|
|
87
|
+
<CardContent className="py-12 text-center">
|
|
88
|
+
<div className="max-w-md mx-auto space-y-4">
|
|
89
|
+
<div className="rounded-full bg-purple-100 dark:bg-purple-900/30 p-3 w-fit mx-auto">
|
|
90
|
+
<Lock className="h-6 w-6 text-purple-600 dark:text-purple-400" />
|
|
91
|
+
</div>
|
|
92
|
+
<div>
|
|
93
|
+
<h3 className="font-semibold text-lg mb-2">Pro Feature</h3>
|
|
94
|
+
<p className="text-muted-foreground text-sm mb-4">
|
|
95
|
+
Calendar is available exclusively to MoonUI Pro subscribers.
|
|
96
|
+
</p>
|
|
97
|
+
<div className="flex gap-3 justify-center">
|
|
98
|
+
<a href="/pricing">
|
|
99
|
+
<Button size="sm">
|
|
100
|
+
<Sparkles className="mr-2 h-4 w-4" />
|
|
101
|
+
Upgrade to Pro
|
|
102
|
+
</Button>
|
|
103
|
+
</a>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</CardContent>
|
|
108
|
+
</Card>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const [currentDate, setCurrentDate] = React.useState(new Date())
|
|
113
|
+
const [selectedDate, setSelectedDate] = React.useState<Date | null>(null)
|
|
114
|
+
const [view, setView] = React.useState<'month' | 'week' | 'day'>('month')
|
|
115
|
+
const [eventDialogOpen, setEventDialogOpen] = React.useState(false)
|
|
116
|
+
const [eventDialogMode, setEventDialogMode] = React.useState<'create' | 'edit'>('create')
|
|
117
|
+
const [selectedEvent, setSelectedEvent] = React.useState<CalendarEvent | null>(null)
|
|
118
|
+
const [draggedEvent, setDraggedEvent] = React.useState<CalendarEvent | null>(null)
|
|
119
|
+
const [dragTargetDate, setDragTargetDate] = React.useState<Date | null>(null)
|
|
120
|
+
|
|
121
|
+
const today = new Date()
|
|
122
|
+
const currentMonth = currentDate.getMonth()
|
|
123
|
+
const currentYear = currentDate.getFullYear()
|
|
124
|
+
|
|
125
|
+
const firstDayOfMonth = new Date(currentYear, currentMonth, 1)
|
|
126
|
+
const lastDayOfMonth = new Date(currentYear, currentMonth + 1, 0)
|
|
127
|
+
const startDate = new Date(firstDayOfMonth)
|
|
128
|
+
startDate.setDate(startDate.getDate() - startDate.getDay())
|
|
129
|
+
|
|
130
|
+
const endDate = new Date(lastDayOfMonth)
|
|
131
|
+
endDate.setDate(endDate.getDate() + (6 - endDate.getDay()))
|
|
132
|
+
|
|
133
|
+
const calendarDays = []
|
|
134
|
+
const currentDateIterator = new Date(startDate)
|
|
135
|
+
|
|
136
|
+
while (currentDateIterator <= endDate) {
|
|
137
|
+
calendarDays.push(new Date(currentDateIterator))
|
|
138
|
+
currentDateIterator.setDate(currentDateIterator.getDate() + 1)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const getEventsForDate = (date: Date) => {
|
|
142
|
+
return events.filter(event => {
|
|
143
|
+
const eventDate = new Date(event.date)
|
|
144
|
+
return eventDate.toDateString() === date.toDateString()
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const isToday = (date: Date) => {
|
|
149
|
+
return date.toDateString() === today.toDateString()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const isCurrentMonth = (date: Date) => {
|
|
153
|
+
return date.getMonth() === currentMonth
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const isSelected = (date: Date) => {
|
|
157
|
+
return selectedDate && date.toDateString() === selectedDate.toDateString()
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const isDisabled = (date: Date) => {
|
|
161
|
+
if (disabled) return true
|
|
162
|
+
if (minDate && date < minDate) return true
|
|
163
|
+
if (maxDate && date > maxDate) return true
|
|
164
|
+
return false
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const navigateMonth = (direction: 'prev' | 'next') => {
|
|
168
|
+
const newDate = new Date(currentDate)
|
|
169
|
+
if (direction === 'prev') {
|
|
170
|
+
newDate.setMonth(currentMonth - 1)
|
|
171
|
+
} else {
|
|
172
|
+
newDate.setMonth(currentMonth + 1)
|
|
173
|
+
}
|
|
174
|
+
setCurrentDate(newDate)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const handleDateClick = (date: Date) => {
|
|
178
|
+
if (isDisabled(date)) return
|
|
179
|
+
|
|
180
|
+
setSelectedDate(date)
|
|
181
|
+
const dateEvents = getEventsForDate(date)
|
|
182
|
+
|
|
183
|
+
if (dateEvents.length === 0) {
|
|
184
|
+
// Open dialog for creating new event
|
|
185
|
+
setEventDialogMode('create')
|
|
186
|
+
setSelectedEvent(null)
|
|
187
|
+
setEventDialogOpen(true)
|
|
188
|
+
} else if (dateEvents.length === 1) {
|
|
189
|
+
onEventClick?.(dateEvents[0])
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const handleEventClick = (event: CalendarEvent, e: React.MouseEvent) => {
|
|
194
|
+
e.stopPropagation()
|
|
195
|
+
onEventClick?.(event)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const handleEventDragStart = (event: CalendarEvent, e: React.DragEvent) => {
|
|
199
|
+
setDraggedEvent(event)
|
|
200
|
+
e.dataTransfer.effectAllowed = 'move'
|
|
201
|
+
e.dataTransfer.setData('text/plain', event.id)
|
|
202
|
+
|
|
203
|
+
// Add visual feedback
|
|
204
|
+
const target = e.target as HTMLElement
|
|
205
|
+
target.style.opacity = '0.5'
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const handleEventDragEnd = (e: React.DragEvent) => {
|
|
209
|
+
setDraggedEvent(null)
|
|
210
|
+
setDragTargetDate(null)
|
|
211
|
+
|
|
212
|
+
// Remove visual feedback
|
|
213
|
+
const target = e.target as HTMLElement
|
|
214
|
+
target.style.opacity = '1'
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const handleDateDragOver = (date: Date, e: React.DragEvent) => {
|
|
218
|
+
if (isDisabled(date) || !draggedEvent) return
|
|
219
|
+
e.preventDefault()
|
|
220
|
+
e.dataTransfer.dropEffect = 'move'
|
|
221
|
+
setDragTargetDate(date)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const handleDateDrop = (date: Date, e: React.DragEvent) => {
|
|
225
|
+
e.preventDefault()
|
|
226
|
+
|
|
227
|
+
if (!draggedEvent || isDisabled(date)) return
|
|
228
|
+
|
|
229
|
+
// Only move if it's a different date
|
|
230
|
+
if (draggedEvent.date.toDateString() !== date.toDateString()) {
|
|
231
|
+
const updatedEvent = {
|
|
232
|
+
...draggedEvent,
|
|
233
|
+
date: date
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
onEventEdit?.(updatedEvent)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
setDraggedEvent(null)
|
|
240
|
+
setDragTargetDate(null)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const handleEventEdit = (event: CalendarEvent, e: React.MouseEvent) => {
|
|
244
|
+
e.stopPropagation()
|
|
245
|
+
setEventDialogMode('edit')
|
|
246
|
+
setSelectedEvent(event)
|
|
247
|
+
setEventDialogOpen(true)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const handleEventDelete = (event: CalendarEvent, e: React.MouseEvent) => {
|
|
251
|
+
e.stopPropagation()
|
|
252
|
+
onEventDelete?.(event.id)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const handleEventSave = (eventData: Omit<CalendarEvent, 'id'> & { id?: string }) => {
|
|
256
|
+
if (eventDialogMode === 'create') {
|
|
257
|
+
onEventAdd?.(eventData)
|
|
258
|
+
} else if (eventDialogMode === 'edit' && eventData.id) {
|
|
259
|
+
onEventEdit?.(eventData as Omit<CalendarEvent, 'id'> & { id: string })
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const handleEventDialogDelete = (eventId: string) => {
|
|
264
|
+
onEventDelete?.(eventId)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const goToToday = () => {
|
|
268
|
+
setCurrentDate(new Date())
|
|
269
|
+
setSelectedDate(new Date())
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const filteredDays = showWeekends ? calendarDays : calendarDays.filter(day => {
|
|
273
|
+
const dayOfWeek = day.getDay()
|
|
274
|
+
return dayOfWeek !== 0 && dayOfWeek !== 6
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
const visibleDaysOfWeek = showWeekends ? DAYS_OF_WEEK : DAYS_OF_WEEK.slice(1, 6)
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
<>
|
|
281
|
+
<Card className={cn("w-full max-w-full overflow-hidden", className)} style={{ height: height ? `${height}px` : undefined }}>
|
|
282
|
+
<CardHeader>
|
|
283
|
+
<div className="flex items-center justify-between">
|
|
284
|
+
<div>
|
|
285
|
+
<CardTitle className="flex items-center gap-2">
|
|
286
|
+
<CalendarIcon className="h-5 w-5" />
|
|
287
|
+
Calendar
|
|
288
|
+
</CardTitle>
|
|
289
|
+
<CardDescription>
|
|
290
|
+
{MONTHS[currentMonth]} {currentYear}
|
|
291
|
+
</CardDescription>
|
|
292
|
+
</div>
|
|
293
|
+
<div className="flex items-center gap-2">
|
|
294
|
+
<Button variant="outline" size="sm" onClick={goToToday}>
|
|
295
|
+
Today
|
|
296
|
+
</Button>
|
|
297
|
+
<Button
|
|
298
|
+
variant="outline"
|
|
299
|
+
size="sm"
|
|
300
|
+
onClick={() => navigateMonth('prev')}
|
|
301
|
+
disabled={disabled}
|
|
302
|
+
>
|
|
303
|
+
<ChevronLeft className="h-4 w-4" />
|
|
304
|
+
</Button>
|
|
305
|
+
<Button
|
|
306
|
+
variant="outline"
|
|
307
|
+
size="sm"
|
|
308
|
+
onClick={() => navigateMonth('next')}
|
|
309
|
+
disabled={disabled}
|
|
310
|
+
>
|
|
311
|
+
<ChevronRight className="h-4 w-4" />
|
|
312
|
+
</Button>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
</CardHeader>
|
|
316
|
+
<CardContent className="overflow-x-auto">
|
|
317
|
+
<div className="space-y-4">
|
|
318
|
+
{/* Calendar Grid */}
|
|
319
|
+
<div className="grid grid-cols-7 gap-1 w-full" style={{ minWidth: "500px" }}>
|
|
320
|
+
{/* Day Headers */}
|
|
321
|
+
{visibleDaysOfWeek.map((day) => (
|
|
322
|
+
<div key={day} className="p-1 text-center text-xs font-medium text-muted-foreground min-w-[70px]">
|
|
323
|
+
{day}
|
|
324
|
+
</div>
|
|
325
|
+
))}
|
|
326
|
+
|
|
327
|
+
{/* Calendar Days */}
|
|
328
|
+
{filteredDays.map((date, index) => {
|
|
329
|
+
const dayEvents = getEventsForDate(date)
|
|
330
|
+
const isCurrentMonthDate = isCurrentMonth(date)
|
|
331
|
+
const isTodayDate = isToday(date)
|
|
332
|
+
const isSelectedDate = isSelected(date)
|
|
333
|
+
const isDisabledDate = isDisabled(date)
|
|
334
|
+
|
|
335
|
+
return (
|
|
336
|
+
<div
|
|
337
|
+
key={index}
|
|
338
|
+
className={cn(
|
|
339
|
+
"min-h-[80px] min-w-[70px] p-1 border border-border/50 cursor-pointer hover:bg-muted/50 transition-colors text-xs flex flex-col",
|
|
340
|
+
!isCurrentMonthDate && "text-muted-foreground bg-muted/20",
|
|
341
|
+
isTodayDate && highlightToday && "bg-primary/10 border-primary/50",
|
|
342
|
+
isSelectedDate && "bg-primary/20 border-primary",
|
|
343
|
+
isDisabledDate && "cursor-not-allowed opacity-50",
|
|
344
|
+
dragTargetDate && dragTargetDate.toDateString() === date.toDateString() && "bg-primary/30 border-primary"
|
|
345
|
+
)}
|
|
346
|
+
onClick={() => handleDateClick(date)}
|
|
347
|
+
onDragOver={(e) => handleDateDragOver(date, e)}
|
|
348
|
+
onDrop={(e) => handleDateDrop(date, e)}
|
|
349
|
+
>
|
|
350
|
+
<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>
|
|
357
|
+
{dayEvents.length > 0 && (
|
|
358
|
+
<Badge variant="secondary" className="text-xs px-1">
|
|
359
|
+
{dayEvents.length}
|
|
360
|
+
</Badge>
|
|
361
|
+
)}
|
|
362
|
+
</div>
|
|
363
|
+
|
|
364
|
+
{/* Events */}
|
|
365
|
+
<div className="flex-1 space-y-1 overflow-hidden">
|
|
366
|
+
{dayEvents.slice(0, 3).map((event) => (
|
|
367
|
+
<div
|
|
368
|
+
key={event.id}
|
|
369
|
+
className={cn(
|
|
370
|
+
"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']
|
|
372
|
+
)}
|
|
373
|
+
draggable={!disabled}
|
|
374
|
+
onClick={(e) => handleEventClick(event, e)}
|
|
375
|
+
onDragStart={(e) => handleEventDragStart(event, e)}
|
|
376
|
+
onDragEnd={handleEventDragEnd}
|
|
377
|
+
style={{
|
|
378
|
+
backgroundColor: event.color || undefined
|
|
379
|
+
}}
|
|
380
|
+
>
|
|
381
|
+
<div className="flex items-center justify-between">
|
|
382
|
+
<span className="truncate flex-1">{event.title}</span>
|
|
383
|
+
{showEventDetails && (
|
|
384
|
+
<div className="hidden group-hover:flex items-center gap-1 ml-1">
|
|
385
|
+
<Button
|
|
386
|
+
variant="ghost"
|
|
387
|
+
size="sm"
|
|
388
|
+
className="h-4 w-4 p-0 text-white/80 hover:text-white"
|
|
389
|
+
onClick={(e) => handleEventEdit(event, e)}
|
|
390
|
+
>
|
|
391
|
+
<Edit className="h-3 w-3" />
|
|
392
|
+
</Button>
|
|
393
|
+
<Button
|
|
394
|
+
variant="ghost"
|
|
395
|
+
size="sm"
|
|
396
|
+
className="h-4 w-4 p-0 text-white/80 hover:text-white"
|
|
397
|
+
onClick={(e) => handleEventDelete(event, e)}
|
|
398
|
+
>
|
|
399
|
+
<Trash2 className="h-3 w-3" />
|
|
400
|
+
</Button>
|
|
401
|
+
</div>
|
|
402
|
+
)}
|
|
403
|
+
</div>
|
|
404
|
+
{event.startTime && (
|
|
405
|
+
<div className="flex items-center gap-1 mt-1">
|
|
406
|
+
<Clock className="h-3 w-3" />
|
|
407
|
+
<span>{event.startTime}</span>
|
|
408
|
+
</div>
|
|
409
|
+
)}
|
|
410
|
+
</div>
|
|
411
|
+
))}
|
|
412
|
+
{dayEvents.length > 3 && (
|
|
413
|
+
<div className="text-xs text-muted-foreground text-center">
|
|
414
|
+
+{dayEvents.length - 3} more
|
|
415
|
+
</div>
|
|
416
|
+
)}
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
)
|
|
420
|
+
})}
|
|
421
|
+
</div>
|
|
422
|
+
|
|
423
|
+
{/* Selected Date Details */}
|
|
424
|
+
{selectedDate && (
|
|
425
|
+
<div className="border rounded-lg p-4 bg-muted/50">
|
|
426
|
+
<h4 className="font-medium mb-2">
|
|
427
|
+
{selectedDate.toLocaleDateString('en-US', {
|
|
428
|
+
weekday: 'long',
|
|
429
|
+
year: 'numeric',
|
|
430
|
+
month: 'long',
|
|
431
|
+
day: 'numeric'
|
|
432
|
+
})}
|
|
433
|
+
</h4>
|
|
434
|
+
|
|
435
|
+
<div className="space-y-2">
|
|
436
|
+
{getEventsForDate(selectedDate).map((event) => (
|
|
437
|
+
<div key={event.id} className="flex items-start gap-3 p-2 border rounded">
|
|
438
|
+
<div className={cn(
|
|
439
|
+
"w-3 h-3 rounded-full mt-1 flex-shrink-0",
|
|
440
|
+
event.color || EVENT_COLORS[event.type || 'event']
|
|
441
|
+
)} />
|
|
442
|
+
<div className="flex-1">
|
|
443
|
+
<div className="flex items-center justify-between">
|
|
444
|
+
<h5 className="font-medium">{event.title}</h5>
|
|
445
|
+
<div className="flex items-center gap-1">
|
|
446
|
+
<Button
|
|
447
|
+
variant="ghost"
|
|
448
|
+
size="sm"
|
|
449
|
+
onClick={(e) => handleEventEdit(event, e)}
|
|
450
|
+
>
|
|
451
|
+
<Edit className="h-4 w-4" />
|
|
452
|
+
</Button>
|
|
453
|
+
<Button
|
|
454
|
+
variant="ghost"
|
|
455
|
+
size="sm"
|
|
456
|
+
onClick={(e) => handleEventDelete(event, e)}
|
|
457
|
+
>
|
|
458
|
+
<Trash2 className="h-4 w-4" />
|
|
459
|
+
</Button>
|
|
460
|
+
</div>
|
|
461
|
+
</div>
|
|
462
|
+
{event.description && (
|
|
463
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
464
|
+
{event.description}
|
|
465
|
+
</p>
|
|
466
|
+
)}
|
|
467
|
+
<div className="flex items-center gap-4 text-xs text-muted-foreground mt-2">
|
|
468
|
+
{event.startTime && (
|
|
469
|
+
<div className="flex items-center gap-1">
|
|
470
|
+
<Clock className="h-3 w-3" />
|
|
471
|
+
<span>{event.startTime} - {event.endTime}</span>
|
|
472
|
+
</div>
|
|
473
|
+
)}
|
|
474
|
+
{event.location && (
|
|
475
|
+
<div className="flex items-center gap-1">
|
|
476
|
+
<MapPin className="h-3 w-3" />
|
|
477
|
+
<span>{event.location}</span>
|
|
478
|
+
</div>
|
|
479
|
+
)}
|
|
480
|
+
{event.attendees && event.attendees.length > 0 && (
|
|
481
|
+
<div className="flex items-center gap-1">
|
|
482
|
+
<User className="h-3 w-3" />
|
|
483
|
+
<span>{event.attendees.length} attendees</span>
|
|
484
|
+
</div>
|
|
485
|
+
)}
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
488
|
+
</div>
|
|
489
|
+
))}
|
|
490
|
+
|
|
491
|
+
{getEventsForDate(selectedDate).length === 0 && (
|
|
492
|
+
<div className="text-center py-4">
|
|
493
|
+
<p className="text-sm text-muted-foreground mb-2">
|
|
494
|
+
No events scheduled for this date
|
|
495
|
+
</p>
|
|
496
|
+
<Button
|
|
497
|
+
variant="outline"
|
|
498
|
+
size="sm"
|
|
499
|
+
onClick={() => {
|
|
500
|
+
setEventDialogMode('create')
|
|
501
|
+
setSelectedEvent(null)
|
|
502
|
+
setEventDialogOpen(true)
|
|
503
|
+
}}
|
|
504
|
+
>
|
|
505
|
+
<Plus className="h-4 w-4 mr-1" />
|
|
506
|
+
Add Event
|
|
507
|
+
</Button>
|
|
508
|
+
</div>
|
|
509
|
+
)}
|
|
510
|
+
</div>
|
|
511
|
+
</div>
|
|
512
|
+
)}
|
|
513
|
+
</div>
|
|
514
|
+
</CardContent>
|
|
515
|
+
</Card>
|
|
516
|
+
|
|
517
|
+
{/* Event Dialog */}
|
|
518
|
+
<EventDialog
|
|
519
|
+
open={eventDialogOpen}
|
|
520
|
+
onOpenChange={setEventDialogOpen}
|
|
521
|
+
event={selectedEvent}
|
|
522
|
+
selectedDate={selectedDate}
|
|
523
|
+
onSave={handleEventSave}
|
|
524
|
+
onDelete={handleEventDialogDelete}
|
|
525
|
+
mode={eventDialogMode}
|
|
526
|
+
/>
|
|
527
|
+
</>
|
|
528
|
+
)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export default Calendar
|