/** * CalendarWidget - Multi-Calendar Unified View * Integrates Google Calendar, Outlook, Apple Calendar * * Features: * - Auto-discovery of calendar sources * - Unified timeline view * - Quick meeting join * - Event creation * - Broadcasting calendar events to other widgets */ import React, { useEffect, useState } from 'react'; import { Calendar, Video, Users, Clock, Plus, ChevronLeft, ChevronRight } from 'lucide-react'; import { useLiveData } from '@/hooks/useLiveData'; import { useWidgetCommunication } from '@/contexts/WidgetContext'; import { cn } from '@/lib/utils'; interface CalendarEvent { id: string; title: string; start: string; end: string; attendees?: string[]; location?: string; meetingLink?: string; calendar: string; color?: string; } interface CalendarWidgetProps { widgetId: string; } export default function CalendarWidget({ widgetId }: CalendarWidgetProps) { const [selectedDate, setSelectedDate] = useState(new Date()); const [viewMode, setViewMode] = useState<'day' | 'week' | 'month'>('day'); // AUTO-DISCOVERY: Connect to calendar sources const { data: calendarData, connected, recommendedSources, connectionStatus, refetch, } = useLiveData({ widgetId, widgetType: 'calendar', requiredSources: ['google-calendar', 'outlook-calendar'], optionalSources: ['apple-calendar', 'notion-calendar'], autoConnect: true, pollInterval: 60000, // Refresh every minute }); // INTER-WIDGET COMMUNICATION const { broadcastEvent, subscribeToEvent } = useWidgetCommunication(widgetId); // Broadcast upcoming meeting useEffect(() => { if (calendarData?.events) { const now = new Date(); const upcomingMeeting = calendarData.events.find((event: CalendarEvent) => { const start = new Date(event.start); const diff = start.getTime() - now.getTime(); return diff > 0 && diff < 5 * 60 * 1000; // 5 minutes before }); if (upcomingMeeting) { broadcastEvent({ type: 'calendar.meeting.upcoming', data: { meetingId: upcomingMeeting.id, title: upcomingMeeting.title, start: upcomingMeeting.start, attendees: upcomingMeeting.attendees, meetingLink: upcomingMeeting.meetingLink, }, }); } } }, [calendarData]); // Listen to task completion events to update calendar useEffect(() => { const unsub = subscribeToEvent('task.completed', (event) => { // Remove related calendar block if task is done if (event.data.calendarBlockId) { // TODO: Remove calendar block } }); return () => unsub(); }, []); const handleJoinMeeting = (meeting: CalendarEvent) => { if (meeting.meetingLink) { window.open(meeting.meetingLink, '_blank'); broadcastEvent({ type: 'calendar.meeting.joined', data: { meetingId: meeting.id, title: meeting.title, }, }); } }; const todayEvents = calendarData?.events?.filter((event: CalendarEvent) => { const eventDate = new Date(event.start); return eventDate.toDateString() === selectedDate.toDateString(); }) || []; const nextMeeting = todayEvents.filter((event: CalendarEvent) => { return new Date(event.start) > new Date(); })[0]; return (
{/* Header */}
Calendar
{/* View Mode Toggle */}
{(['day', 'week', 'month'] as const).map((mode) => ( ))}
{/* Date Navigator */}
{selectedDate.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })}
{todayEvents.length} events
{/* Connection Status */} {!connected && (
Connecting to calendar sources...
)} {/* Next Meeting Highlight */} {nextMeeting && (
Up Next
{nextMeeting.title}
{new Date(nextMeeting.start).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}
{nextMeeting.meetingLink && ( )}
)} {/* Events List */}
{connected && todayEvents.length === 0 && (

No events for this day

)} {todayEvents.map((event: CalendarEvent) => { const isPast = new Date(event.end) < new Date(); const isNow = new Date(event.start) <= new Date() && new Date(event.end) >= new Date(); return (
{event.title}
{new Date(event.start).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}
{event.attendees && event.attendees.length > 0 && (
{event.attendees.length}
)}
{event.location && (
📍 {event.location}
)}
{event.meetingLink && !isPast && ( )}
); })}
{/* Source Recommendations */} {recommendedSources.length > 0 && (
Missing Calendar Sources:
{recommendedSources.map((source) => (
{source.name}
))}
)} {/* Quick Actions */}
); }