Kraft102's picture
fix: sql.js Docker/Alpine compatibility layer for PatternMemory and FailureMemory
5a81b95
/**
* 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 (
<div className="h-full flex flex-col bg-gradient-to-br from-blue-500/10 to-purple-500/10 backdrop-blur-sm border border-blue-500/30 rounded-lg overflow-hidden">
{/* Header */}
<div className="p-4 border-b border-blue-500/20 bg-gradient-to-r from-blue-500/10 to-transparent">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Calendar className="w-5 h-5 text-blue-400" />
<span className="font-display text-sm uppercase tracking-wider text-blue-400">
Calendar
</span>
</div>
{/* View Mode Toggle */}
<div className="flex gap-1 bg-black/20 rounded p-1">
{(['day', 'week', 'month'] as const).map((mode) => (
<button
key={mode}
onClick={() => setViewMode(mode)}
className={cn(
"px-2 py-1 rounded text-xs transition-all",
viewMode === mode
? "bg-blue-500 text-white"
: "text-blue-300 hover:bg-blue-500/20"
)}
>
{mode}
</button>
))}
</div>
</div>
{/* Date Navigator */}
<div className="flex items-center justify-between">
<button
onClick={() => {
const newDate = new Date(selectedDate);
newDate.setDate(newDate.getDate() - 1);
setSelectedDate(newDate);
}}
className="p-1 hover:bg-blue-500/20 rounded transition-colors"
>
<ChevronLeft className="w-4 h-4 text-blue-300" />
</button>
<div className="text-center">
<div className="text-lg font-semibold text-white">
{selectedDate.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })}
</div>
<div className="text-xs text-blue-300">
{todayEvents.length} events
</div>
</div>
<button
onClick={() => {
const newDate = new Date(selectedDate);
newDate.setDate(newDate.getDate() + 1);
setSelectedDate(newDate);
}}
className="p-1 hover:bg-blue-500/20 rounded transition-colors"
>
<ChevronRight className="w-4 h-4 text-blue-300" />
</button>
</div>
</div>
{/* Connection Status */}
{!connected && (
<div className="p-3 bg-orange-500/10 border-b border-orange-500/30">
<div className="flex items-center gap-2 text-xs text-orange-400">
<Clock className="w-4 h-4" />
<span>Connecting to calendar sources...</span>
</div>
</div>
)}
{/* Next Meeting Highlight */}
{nextMeeting && (
<div className="p-3 bg-green-500/10 border-b border-green-500/30">
<div className="flex items-center justify-between">
<div>
<div className="text-xs text-green-400 mb-1">Up Next</div>
<div className="text-sm font-medium text-white">{nextMeeting.title}</div>
<div className="text-xs text-green-300">
{new Date(nextMeeting.start).toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit'
})}
</div>
</div>
{nextMeeting.meetingLink && (
<button
onClick={() => handleJoinMeeting(nextMeeting)}
className="flex items-center gap-2 px-3 py-2 bg-green-500 text-white rounded text-xs hover:bg-green-600 transition-colors"
>
<Video className="w-3 h-3" />
Join
</button>
)}
</div>
</div>
)}
{/* Events List */}
<div className="flex-1 overflow-y-auto p-3 space-y-2">
{connected && todayEvents.length === 0 && (
<div className="text-center py-8">
<Calendar className="w-8 h-8 text-blue-400/30 mx-auto mb-2" />
<p className="text-sm text-blue-300/50">No events for this day</p>
</div>
)}
{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 (
<div
key={event.id}
className={cn(
"p-3 rounded-lg border transition-all",
isNow && "bg-green-500/20 border-green-500/50",
!isNow && !isPast && "bg-blue-500/10 border-blue-500/30 hover:bg-blue-500/20",
isPast && "bg-gray-500/10 border-gray-500/20 opacity-50"
)}
>
<div className="flex items-start justify-between gap-2">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: event.color || '#3b82f6' }}
/>
<span className="text-sm font-medium text-white">
{event.title}
</span>
</div>
<div className="flex items-center gap-3 text-xs text-blue-300">
<div className="flex items-center gap-1">
<Clock className="w-3 h-3" />
{new Date(event.start).toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit'
})}
</div>
{event.attendees && event.attendees.length > 0 && (
<div className="flex items-center gap-1">
<Users className="w-3 h-3" />
{event.attendees.length}
</div>
)}
</div>
{event.location && (
<div className="text-xs text-blue-300/70 mt-1">
📍 {event.location}
</div>
)}
</div>
{event.meetingLink && !isPast && (
<button
onClick={() => handleJoinMeeting(event)}
className="p-1.5 bg-blue-500/20 hover:bg-blue-500/30 rounded transition-colors"
>
<Video className="w-4 h-4 text-blue-400" />
</button>
)}
</div>
</div>
);
})}
</div>
{/* Source Recommendations */}
{recommendedSources.length > 0 && (
<div className="p-3 border-t border-blue-500/20 bg-orange-500/10">
<div className="text-xs text-orange-400 mb-2">
Missing Calendar Sources:
</div>
{recommendedSources.map((source) => (
<div key={source.id} className="flex items-center justify-between mb-1">
<span className="text-xs text-orange-300">{source.name}</span>
<button
onClick={() => {
// Enable source
refetch();
}}
className="px-2 py-1 bg-orange-500 text-white rounded text-xs"
>
Enable
</button>
</div>
))}
</div>
)}
{/* Quick Actions */}
<div className="p-3 border-t border-blue-500/20 bg-black/20">
<button className="w-full flex items-center justify-center gap-2 px-3 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded transition-colors">
<Plus className="w-4 h-4" />
<span className="text-sm">New Event</span>
</button>
</div>
</div>
);
}