All checks were successful
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
124 lines
3.7 KiB
TypeScript
124 lines
3.7 KiB
TypeScript
/**
|
|
* Calendar Widget - displays upcoming events
|
|
*/
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { Calendar as CalendarIcon, Clock, MapPin } from "lucide-react";
|
|
import type { WidgetProps, Event } from "@mosaic/shared";
|
|
import { fetchEvents } from "@/lib/api/events";
|
|
|
|
export function CalendarWidget({ id: _id, config: _config }: WidgetProps): React.JSX.Element {
|
|
const [events, setEvents] = useState<Event[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
let isMounted = true;
|
|
|
|
const loadEvents = async (): Promise<void> => {
|
|
setIsLoading(true);
|
|
try {
|
|
const data = await fetchEvents();
|
|
if (isMounted) {
|
|
setEvents(data);
|
|
}
|
|
} catch {
|
|
if (isMounted) {
|
|
setEvents([]);
|
|
}
|
|
} finally {
|
|
if (isMounted) {
|
|
setIsLoading(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
void loadEvents();
|
|
|
|
return (): void => {
|
|
isMounted = false;
|
|
};
|
|
}, []);
|
|
|
|
const formatTime = (dateValue: Date | string): string => {
|
|
const date = new Date(dateValue);
|
|
return date.toLocaleTimeString("en-US", {
|
|
hour: "numeric",
|
|
minute: "2-digit",
|
|
hour12: true,
|
|
});
|
|
};
|
|
|
|
const formatDay = (dateValue: Date | string): string => {
|
|
const date = new Date(dateValue);
|
|
const today = new Date();
|
|
const tomorrow = new Date(today);
|
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
|
|
if (date.toDateString() === today.toDateString()) {
|
|
return "Today";
|
|
} else if (date.toDateString() === tomorrow.toDateString()) {
|
|
return "Tomorrow";
|
|
}
|
|
return date.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" });
|
|
};
|
|
|
|
const getUpcomingEvents = (): Event[] => {
|
|
const now = new Date();
|
|
return events
|
|
.filter((e) => new Date(e.startTime) > now)
|
|
.sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime())
|
|
.slice(0, 5);
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-full">
|
|
<div className="text-gray-500 text-sm">Loading events...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const upcomingEvents = getUpcomingEvents();
|
|
|
|
return (
|
|
<div className="flex flex-col h-full space-y-3">
|
|
{/* Header */}
|
|
<div className="flex items-center gap-2 text-gray-700">
|
|
<CalendarIcon className="w-4 h-4" />
|
|
<span className="text-sm font-medium">Upcoming Events</span>
|
|
</div>
|
|
|
|
{/* Event list */}
|
|
<div className="flex-1 overflow-auto space-y-3">
|
|
{upcomingEvents.length === 0 ? (
|
|
<div className="text-center text-gray-500 text-sm py-4">No upcoming events</div>
|
|
) : (
|
|
upcomingEvents.map((event) => (
|
|
<div key={event.id} className="border-l-2 border-blue-500 pl-3 py-1">
|
|
<div className="text-sm font-medium text-gray-900">{event.title}</div>
|
|
<div className="flex items-center gap-3 mt-1 text-xs text-gray-500">
|
|
{!event.allDay && (
|
|
<div className="flex items-center gap-1">
|
|
<Clock className="w-3 h-3" />
|
|
<span>
|
|
{formatTime(event.startTime)}
|
|
{event.endTime && ` - ${formatTime(event.endTime)}`}
|
|
</span>
|
|
</div>
|
|
)}
|
|
{event.location && (
|
|
<div className="flex items-center gap-1">
|
|
<MapPin className="w-3 h-3" />
|
|
<span>{event.location}</span>
|
|
</div>
|
|
)}
|
|
<div className="text-gray-400">{formatDay(event.startTime)}</div>
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|