@object-ui/plugin-calendar 0.3.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/LICENSE +21 -0
- package/README.md +198 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +946 -0
- package/dist/index.umd.cjs +6 -0
- package/dist/src/CalendarView.d.ts +29 -0
- package/dist/src/CalendarView.d.ts.map +1 -0
- package/dist/src/ObjectCalendar.d.ts +13 -0
- package/dist/src/ObjectCalendar.d.ts.map +1 -0
- package/dist/src/calendar-view-renderer.d.ts +9 -0
- package/dist/src/calendar-view-renderer.d.ts.map +1 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.d.ts.map +1 -0
- package/package.json +53 -0
- package/src/CalendarView.tsx +504 -0
- package/src/ObjectCalendar.tsx +385 -0
- package/src/calendar-view-renderer.tsx +231 -0
- package/src/index.tsx +37 -0
- package/tsconfig.json +18 -0
- package/vite.config.ts +50 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
"use client"
|
|
10
|
+
|
|
11
|
+
import * as React from "react"
|
|
12
|
+
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"
|
|
13
|
+
import { cn, Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@object-ui/components"
|
|
14
|
+
|
|
15
|
+
const DEFAULT_EVENT_COLOR = "bg-blue-500 text-white"
|
|
16
|
+
|
|
17
|
+
export interface CalendarEvent {
|
|
18
|
+
id: string | number
|
|
19
|
+
title: string
|
|
20
|
+
start: Date
|
|
21
|
+
end?: Date
|
|
22
|
+
allDay?: boolean
|
|
23
|
+
color?: string
|
|
24
|
+
data?: any
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CalendarViewProps {
|
|
28
|
+
events?: CalendarEvent[]
|
|
29
|
+
view?: "month" | "week" | "day"
|
|
30
|
+
currentDate?: Date
|
|
31
|
+
onEventClick?: (event: CalendarEvent) => void
|
|
32
|
+
onDateClick?: (date: Date) => void
|
|
33
|
+
onViewChange?: (view: "month" | "week" | "day") => void
|
|
34
|
+
onNavigate?: (date: Date) => void
|
|
35
|
+
className?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function CalendarView({
|
|
39
|
+
events = [],
|
|
40
|
+
view = "month",
|
|
41
|
+
currentDate = new Date(),
|
|
42
|
+
onEventClick,
|
|
43
|
+
onDateClick,
|
|
44
|
+
onViewChange,
|
|
45
|
+
onNavigate,
|
|
46
|
+
className,
|
|
47
|
+
}: CalendarViewProps) {
|
|
48
|
+
const [selectedView, setSelectedView] = React.useState(view)
|
|
49
|
+
const [selectedDate, setSelectedDate] = React.useState(currentDate)
|
|
50
|
+
|
|
51
|
+
const handlePrevious = () => {
|
|
52
|
+
const newDate = new Date(selectedDate)
|
|
53
|
+
if (selectedView === "month") {
|
|
54
|
+
newDate.setMonth(newDate.getMonth() - 1)
|
|
55
|
+
} else if (selectedView === "week") {
|
|
56
|
+
newDate.setDate(newDate.getDate() - 7)
|
|
57
|
+
} else {
|
|
58
|
+
newDate.setDate(newDate.getDate() - 1)
|
|
59
|
+
}
|
|
60
|
+
setSelectedDate(newDate)
|
|
61
|
+
onNavigate?.(newDate)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const handleNext = () => {
|
|
65
|
+
const newDate = new Date(selectedDate)
|
|
66
|
+
if (selectedView === "month") {
|
|
67
|
+
newDate.setMonth(newDate.getMonth() + 1)
|
|
68
|
+
} else if (selectedView === "week") {
|
|
69
|
+
newDate.setDate(newDate.getDate() + 7)
|
|
70
|
+
} else {
|
|
71
|
+
newDate.setDate(newDate.getDate() + 1)
|
|
72
|
+
}
|
|
73
|
+
setSelectedDate(newDate)
|
|
74
|
+
onNavigate?.(newDate)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const handleToday = () => {
|
|
78
|
+
const today = new Date()
|
|
79
|
+
setSelectedDate(today)
|
|
80
|
+
onNavigate?.(today)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const handleViewChange = (newView: "month" | "week" | "day") => {
|
|
84
|
+
setSelectedView(newView)
|
|
85
|
+
onViewChange?.(newView)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const getDateLabel = () => {
|
|
89
|
+
if (selectedView === "month") {
|
|
90
|
+
return selectedDate.toLocaleDateString("default", {
|
|
91
|
+
month: "long",
|
|
92
|
+
year: "numeric",
|
|
93
|
+
})
|
|
94
|
+
} else if (selectedView === "week") {
|
|
95
|
+
const weekStart = getWeekStart(selectedDate)
|
|
96
|
+
const weekEnd = new Date(weekStart)
|
|
97
|
+
weekEnd.setDate(weekEnd.getDate() + 6)
|
|
98
|
+
return `${weekStart.toLocaleDateString("default", {
|
|
99
|
+
month: "short",
|
|
100
|
+
day: "numeric",
|
|
101
|
+
})} - ${weekEnd.toLocaleDateString("default", {
|
|
102
|
+
month: "short",
|
|
103
|
+
day: "numeric",
|
|
104
|
+
year: "numeric",
|
|
105
|
+
})}`
|
|
106
|
+
} else {
|
|
107
|
+
return selectedDate.toLocaleDateString("default", {
|
|
108
|
+
weekday: "long",
|
|
109
|
+
month: "long",
|
|
110
|
+
day: "numeric",
|
|
111
|
+
year: "numeric",
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<div className={cn("flex flex-col h-full bg-background", className)}>
|
|
118
|
+
{/* Header */}
|
|
119
|
+
<div className="flex items-center justify-between p-4 border-b">
|
|
120
|
+
<div className="flex items-center gap-2">
|
|
121
|
+
<Button variant="outline" size="sm" onClick={handleToday}>
|
|
122
|
+
Today
|
|
123
|
+
</Button>
|
|
124
|
+
<div className="flex items-center">
|
|
125
|
+
<Button
|
|
126
|
+
variant="ghost"
|
|
127
|
+
size="icon"
|
|
128
|
+
onClick={handlePrevious}
|
|
129
|
+
className="h-8 w-8"
|
|
130
|
+
>
|
|
131
|
+
<ChevronLeftIcon className="h-4 w-4" />
|
|
132
|
+
</Button>
|
|
133
|
+
<Button
|
|
134
|
+
variant="ghost"
|
|
135
|
+
size="icon"
|
|
136
|
+
onClick={handleNext}
|
|
137
|
+
className="h-8 w-8"
|
|
138
|
+
>
|
|
139
|
+
<ChevronRightIcon className="h-4 w-4" />
|
|
140
|
+
</Button>
|
|
141
|
+
</div>
|
|
142
|
+
<h2 className="text-lg font-semibold ml-2">{getDateLabel()}</h2>
|
|
143
|
+
</div>
|
|
144
|
+
<div className="flex items-center gap-2">
|
|
145
|
+
<Select value={selectedView} onValueChange={handleViewChange}>
|
|
146
|
+
<SelectTrigger className="w-32">
|
|
147
|
+
<SelectValue />
|
|
148
|
+
</SelectTrigger>
|
|
149
|
+
<SelectContent>
|
|
150
|
+
<SelectItem value="day">Day</SelectItem>
|
|
151
|
+
<SelectItem value="week">Week</SelectItem>
|
|
152
|
+
<SelectItem value="month">Month</SelectItem>
|
|
153
|
+
</SelectContent>
|
|
154
|
+
</Select>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
{/* Calendar Grid */}
|
|
159
|
+
<div className="flex-1 overflow-auto">
|
|
160
|
+
{selectedView === "month" && (
|
|
161
|
+
<MonthView
|
|
162
|
+
date={selectedDate}
|
|
163
|
+
events={events}
|
|
164
|
+
onEventClick={onEventClick}
|
|
165
|
+
onDateClick={onDateClick}
|
|
166
|
+
/>
|
|
167
|
+
)}
|
|
168
|
+
{selectedView === "week" && (
|
|
169
|
+
<WeekView
|
|
170
|
+
date={selectedDate}
|
|
171
|
+
events={events}
|
|
172
|
+
onEventClick={onEventClick}
|
|
173
|
+
onDateClick={onDateClick}
|
|
174
|
+
/>
|
|
175
|
+
)}
|
|
176
|
+
{selectedView === "day" && (
|
|
177
|
+
<DayView
|
|
178
|
+
date={selectedDate}
|
|
179
|
+
events={events}
|
|
180
|
+
onEventClick={onEventClick}
|
|
181
|
+
/>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function getWeekStart(date: Date): Date {
|
|
189
|
+
const d = new Date(date)
|
|
190
|
+
const day = d.getDay()
|
|
191
|
+
const diff = d.getDate() - day
|
|
192
|
+
d.setDate(diff)
|
|
193
|
+
return d
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function getMonthDays(date: Date): Date[] {
|
|
197
|
+
const year = date.getFullYear()
|
|
198
|
+
const month = date.getMonth()
|
|
199
|
+
const firstDay = new Date(year, month, 1)
|
|
200
|
+
const lastDay = new Date(year, month + 1, 0)
|
|
201
|
+
const startDay = firstDay.getDay()
|
|
202
|
+
const days: Date[] = []
|
|
203
|
+
|
|
204
|
+
// Add previous month days
|
|
205
|
+
for (let i = startDay - 1; i >= 0; i--) {
|
|
206
|
+
const prevDate = new Date(firstDay.getTime())
|
|
207
|
+
prevDate.setDate(prevDate.getDate() - (i + 1))
|
|
208
|
+
days.push(prevDate)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Add current month days
|
|
212
|
+
for (let i = 1; i <= lastDay.getDate(); i++) {
|
|
213
|
+
days.push(new Date(year, month, i))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Add next month days
|
|
217
|
+
const remainingDays = 42 - days.length
|
|
218
|
+
for (let i = 1; i <= remainingDays; i++) {
|
|
219
|
+
const nextDate = new Date(lastDay.getTime())
|
|
220
|
+
nextDate.setDate(nextDate.getDate() + i)
|
|
221
|
+
days.push(nextDate)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return days
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function isSameDay(date1: Date, date2: Date): boolean {
|
|
228
|
+
return (
|
|
229
|
+
date1.getFullYear() === date2.getFullYear() &&
|
|
230
|
+
date1.getMonth() === date2.getMonth() &&
|
|
231
|
+
date1.getDate() === date2.getDate()
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function getEventsForDate(date: Date, events: CalendarEvent[]): CalendarEvent[] {
|
|
236
|
+
return events.filter((event) => {
|
|
237
|
+
const eventStart = new Date(event.start)
|
|
238
|
+
const eventEnd = event.end ? new Date(event.end) : new Date(eventStart)
|
|
239
|
+
|
|
240
|
+
// Create new date objects for comparison to avoid mutation
|
|
241
|
+
const dateStart = new Date(date)
|
|
242
|
+
dateStart.setHours(0, 0, 0, 0)
|
|
243
|
+
const dateEnd = new Date(date)
|
|
244
|
+
dateEnd.setHours(23, 59, 59, 999)
|
|
245
|
+
|
|
246
|
+
const eventStartTime = new Date(eventStart)
|
|
247
|
+
eventStartTime.setHours(0, 0, 0, 0)
|
|
248
|
+
const eventEndTime = new Date(eventEnd)
|
|
249
|
+
eventEndTime.setHours(23, 59, 59, 999)
|
|
250
|
+
|
|
251
|
+
return dateStart <= eventEndTime && dateEnd >= eventStartTime
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
interface MonthViewProps {
|
|
256
|
+
date: Date
|
|
257
|
+
events: CalendarEvent[]
|
|
258
|
+
onEventClick?: (event: CalendarEvent) => void
|
|
259
|
+
onDateClick?: (date: Date) => void
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function MonthView({ date, events, onEventClick, onDateClick }: MonthViewProps) {
|
|
263
|
+
const days = getMonthDays(date)
|
|
264
|
+
const today = new Date()
|
|
265
|
+
const weekDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<div className="flex flex-col h-full">
|
|
269
|
+
{/* Week day headers */}
|
|
270
|
+
<div className="grid grid-cols-7 border-b">
|
|
271
|
+
{weekDays.map((day) => (
|
|
272
|
+
<div
|
|
273
|
+
key={day}
|
|
274
|
+
className="p-2 text-center text-sm font-medium text-muted-foreground border-r last:border-r-0"
|
|
275
|
+
>
|
|
276
|
+
{day}
|
|
277
|
+
</div>
|
|
278
|
+
))}
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
{/* Calendar days */}
|
|
282
|
+
<div className="grid grid-cols-7 flex-1 auto-rows-fr">
|
|
283
|
+
{days.map((day, index) => {
|
|
284
|
+
const dayEvents = getEventsForDate(day, events)
|
|
285
|
+
const isCurrentMonth = day.getMonth() === date.getMonth()
|
|
286
|
+
const isToday = isSameDay(day, today)
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
<div
|
|
290
|
+
key={index}
|
|
291
|
+
className={cn(
|
|
292
|
+
"border-b border-r last:border-r-0 p-2 min-h-[100px] cursor-pointer hover:bg-accent/50",
|
|
293
|
+
!isCurrentMonth && "bg-muted/30 text-muted-foreground"
|
|
294
|
+
)}
|
|
295
|
+
onClick={() => onDateClick?.(day)}
|
|
296
|
+
>
|
|
297
|
+
<div
|
|
298
|
+
className={cn(
|
|
299
|
+
"text-sm font-medium mb-1",
|
|
300
|
+
isToday &&
|
|
301
|
+
"inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground h-6 w-6"
|
|
302
|
+
)}
|
|
303
|
+
>
|
|
304
|
+
{day.getDate()}
|
|
305
|
+
</div>
|
|
306
|
+
<div className="space-y-1">
|
|
307
|
+
{dayEvents.slice(0, 3).map((event) => (
|
|
308
|
+
<div
|
|
309
|
+
key={event.id}
|
|
310
|
+
className={cn(
|
|
311
|
+
"text-xs px-2 py-1 rounded truncate cursor-pointer hover:opacity-80",
|
|
312
|
+
event.color || DEFAULT_EVENT_COLOR
|
|
313
|
+
)}
|
|
314
|
+
style={
|
|
315
|
+
event.color && event.color.startsWith("#")
|
|
316
|
+
? { backgroundColor: event.color }
|
|
317
|
+
: undefined
|
|
318
|
+
}
|
|
319
|
+
onClick={(e) => {
|
|
320
|
+
e.stopPropagation()
|
|
321
|
+
onEventClick?.(event)
|
|
322
|
+
}}
|
|
323
|
+
>
|
|
324
|
+
{event.title}
|
|
325
|
+
</div>
|
|
326
|
+
))}
|
|
327
|
+
{dayEvents.length > 3 && (
|
|
328
|
+
<div className="text-xs text-muted-foreground px-2">
|
|
329
|
+
+{dayEvents.length - 3} more
|
|
330
|
+
</div>
|
|
331
|
+
)}
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
)
|
|
335
|
+
})}
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
interface WeekViewProps {
|
|
342
|
+
date: Date
|
|
343
|
+
events: CalendarEvent[]
|
|
344
|
+
onEventClick?: (event: CalendarEvent) => void
|
|
345
|
+
onDateClick?: (date: Date) => void
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function WeekView({ date, events, onEventClick, onDateClick }: WeekViewProps) {
|
|
349
|
+
const weekStart = getWeekStart(date)
|
|
350
|
+
const weekDays = Array.from({ length: 7 }, (_, i) => {
|
|
351
|
+
const day = new Date(weekStart)
|
|
352
|
+
day.setDate(day.getDate() + i)
|
|
353
|
+
return day
|
|
354
|
+
})
|
|
355
|
+
const today = new Date()
|
|
356
|
+
|
|
357
|
+
return (
|
|
358
|
+
<div className="flex flex-col h-full">
|
|
359
|
+
{/* Week day headers */}
|
|
360
|
+
<div className="grid grid-cols-7 border-b">
|
|
361
|
+
{weekDays.map((day) => {
|
|
362
|
+
const isToday = isSameDay(day, today)
|
|
363
|
+
return (
|
|
364
|
+
<div
|
|
365
|
+
key={day.toISOString()}
|
|
366
|
+
className="p-3 text-center border-r last:border-r-0"
|
|
367
|
+
>
|
|
368
|
+
<div className="text-sm font-medium text-muted-foreground">
|
|
369
|
+
{day.toLocaleDateString("default", { weekday: "short" })}
|
|
370
|
+
</div>
|
|
371
|
+
<div
|
|
372
|
+
className={cn(
|
|
373
|
+
"text-lg font-semibold mt-1",
|
|
374
|
+
isToday &&
|
|
375
|
+
"inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground h-8 w-8"
|
|
376
|
+
)}
|
|
377
|
+
>
|
|
378
|
+
{day.getDate()}
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
)
|
|
382
|
+
})}
|
|
383
|
+
</div>
|
|
384
|
+
|
|
385
|
+
{/* Week events */}
|
|
386
|
+
<div className="grid grid-cols-7 flex-1">
|
|
387
|
+
{weekDays.map((day) => {
|
|
388
|
+
const dayEvents = getEventsForDate(day, events)
|
|
389
|
+
return (
|
|
390
|
+
<div
|
|
391
|
+
key={day.toISOString()}
|
|
392
|
+
className="border-r last:border-r-0 p-2 min-h-[400px] cursor-pointer hover:bg-accent/50"
|
|
393
|
+
onClick={() => onDateClick?.(day)}
|
|
394
|
+
>
|
|
395
|
+
<div className="space-y-2">
|
|
396
|
+
{dayEvents.map((event) => (
|
|
397
|
+
<div
|
|
398
|
+
key={event.id}
|
|
399
|
+
className={cn(
|
|
400
|
+
"text-sm px-3 py-2 rounded cursor-pointer hover:opacity-80",
|
|
401
|
+
event.color || DEFAULT_EVENT_COLOR
|
|
402
|
+
)}
|
|
403
|
+
style={
|
|
404
|
+
event.color && event.color.startsWith("#")
|
|
405
|
+
? { backgroundColor: event.color }
|
|
406
|
+
: undefined
|
|
407
|
+
}
|
|
408
|
+
onClick={(e) => {
|
|
409
|
+
e.stopPropagation()
|
|
410
|
+
onEventClick?.(event)
|
|
411
|
+
}}
|
|
412
|
+
>
|
|
413
|
+
<div className="font-medium">{event.title}</div>
|
|
414
|
+
{!event.allDay && (
|
|
415
|
+
<div className="text-xs opacity-90 mt-1">
|
|
416
|
+
{event.start.toLocaleTimeString("default", {
|
|
417
|
+
hour: "numeric",
|
|
418
|
+
minute: "2-digit",
|
|
419
|
+
})}
|
|
420
|
+
</div>
|
|
421
|
+
)}
|
|
422
|
+
</div>
|
|
423
|
+
))}
|
|
424
|
+
</div>
|
|
425
|
+
</div>
|
|
426
|
+
)
|
|
427
|
+
})}
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
interface DayViewProps {
|
|
434
|
+
date: Date
|
|
435
|
+
events: CalendarEvent[]
|
|
436
|
+
onEventClick?: (event: CalendarEvent) => void
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function DayView({ date, events, onEventClick }: DayViewProps) {
|
|
440
|
+
const dayEvents = getEventsForDate(date, events)
|
|
441
|
+
const hours = Array.from({ length: 24 }, (_, i) => i)
|
|
442
|
+
|
|
443
|
+
return (
|
|
444
|
+
<div className="flex flex-col h-full">
|
|
445
|
+
<div className="flex-1 overflow-auto">
|
|
446
|
+
{hours.map((hour) => {
|
|
447
|
+
const hourEvents = dayEvents.filter((event) => {
|
|
448
|
+
if (event.allDay) return hour === 0
|
|
449
|
+
const eventHour = event.start.getHours()
|
|
450
|
+
return eventHour === hour
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
return (
|
|
454
|
+
<div key={hour} className="flex border-b min-h-[60px]">
|
|
455
|
+
<div className="w-20 p-2 text-sm text-muted-foreground border-r">
|
|
456
|
+
{hour === 0
|
|
457
|
+
? "12 AM"
|
|
458
|
+
: hour < 12
|
|
459
|
+
? `${hour} AM`
|
|
460
|
+
: hour === 12
|
|
461
|
+
? "12 PM"
|
|
462
|
+
: `${hour - 12} PM`}
|
|
463
|
+
</div>
|
|
464
|
+
<div className="flex-1 p-2 space-y-2">
|
|
465
|
+
{hourEvents.map((event) => (
|
|
466
|
+
<div
|
|
467
|
+
key={event.id}
|
|
468
|
+
className={cn(
|
|
469
|
+
"px-3 py-2 rounded cursor-pointer hover:opacity-80",
|
|
470
|
+
event.color || DEFAULT_EVENT_COLOR
|
|
471
|
+
)}
|
|
472
|
+
style={
|
|
473
|
+
event.color && event.color.startsWith("#")
|
|
474
|
+
? { backgroundColor: event.color }
|
|
475
|
+
: undefined
|
|
476
|
+
}
|
|
477
|
+
onClick={() => onEventClick?.(event)}
|
|
478
|
+
>
|
|
479
|
+
<div className="font-medium">{event.title}</div>
|
|
480
|
+
{!event.allDay && (
|
|
481
|
+
<div className="text-xs opacity-90 mt-1">
|
|
482
|
+
{event.start.toLocaleTimeString("default", {
|
|
483
|
+
hour: "numeric",
|
|
484
|
+
minute: "2-digit",
|
|
485
|
+
})}
|
|
486
|
+
{event.end &&
|
|
487
|
+
` - ${event.end.toLocaleTimeString("default", {
|
|
488
|
+
hour: "numeric",
|
|
489
|
+
minute: "2-digit",
|
|
490
|
+
})}`}
|
|
491
|
+
</div>
|
|
492
|
+
)}
|
|
493
|
+
</div>
|
|
494
|
+
))}
|
|
495
|
+
</div>
|
|
496
|
+
</div>
|
|
497
|
+
)
|
|
498
|
+
})}
|
|
499
|
+
</div>
|
|
500
|
+
</div>
|
|
501
|
+
)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export { CalendarView }
|