Spaces:
Paused
Paused
| /** | |
| * TaskListWidget - Unified Task Management | |
| * Integrates Todoist, Asana, TickTick, Notion tasks | |
| * | |
| * Features: | |
| * - Auto-discovery of task sources | |
| * - Priority sorting | |
| * - Quick add | |
| * - Broadcasting task events | |
| */ | |
| import React, { useState, useEffect } from 'react'; | |
| import { CheckSquare, Square, Plus, Flag, Calendar as CalIcon, Trash2 } from 'lucide-react'; | |
| import { useLiveData } from '@/hooks/useLiveData'; | |
| import { useWidgetCommunication } from '@/contexts/WidgetContext'; | |
| import { cn } from '@/lib/utils'; | |
| interface Task { | |
| id: string; | |
| title: string; | |
| completed: boolean; | |
| priority: 1 | 2 | 3 | 4; | |
| dueDate?: string; | |
| project?: string; | |
| source: string; | |
| } | |
| interface TaskListWidgetProps { | |
| widgetId: string; | |
| } | |
| export default function TaskListWidget({ widgetId }: TaskListWidgetProps) { | |
| const [filter, setFilter] = useState<'all' | 'today' | 'high-priority'>('today'); | |
| const [newTaskTitle, setNewTaskTitle] = useState(''); | |
| // AUTO-DISCOVERY | |
| const { | |
| data: taskData, | |
| connected, | |
| recommendedSources, | |
| refetch, | |
| } = useLiveData({ | |
| widgetId, | |
| widgetType: 'tasks', | |
| requiredSources: ['todoist', 'asana'], | |
| optionalSources: ['ticktick', 'notion-tasks'], | |
| autoConnect: true, | |
| pollInterval: 30000, | |
| }); | |
| // INTER-WIDGET COMMUNICATION | |
| const { broadcastEvent, subscribeToEvent } = useWidgetCommunication(widgetId); | |
| // Broadcast task completion | |
| const handleToggleTask = async (task: Task) => { | |
| // TODO: API call to toggle task | |
| broadcastEvent({ | |
| type: task.completed ? 'task.uncompleted' : 'task.completed', | |
| data: { | |
| taskId: task.id, | |
| title: task.title, | |
| source: task.source, | |
| }, | |
| }); | |
| refetch(); | |
| }; | |
| // Listen for calendar events to create tasks | |
| useEffect(() => { | |
| const unsub = subscribeToEvent('calendar.meeting.upcoming', (event) => { | |
| // Auto-create prep task for upcoming meetings | |
| const meetingTitle = event.data.title; | |
| // TODO: Create task "Prepare for: {meetingTitle}" | |
| }); | |
| return () => unsub(); | |
| }, []); | |
| const handleAddTask = async () => { | |
| if (!newTaskTitle.trim()) return; | |
| // TODO: API call to create task | |
| broadcastEvent({ | |
| type: 'task.created', | |
| data: { | |
| title: newTaskTitle, | |
| }, | |
| }); | |
| setNewTaskTitle(''); | |
| refetch(); | |
| }; | |
| const tasks: Task[] = taskData?.tasks || []; | |
| const filteredTasks = tasks.filter(task => { | |
| if (filter === 'today') { | |
| if (!task.dueDate) return false; | |
| return new Date(task.dueDate).toDateString() === new Date().toDateString(); | |
| } | |
| if (filter === 'high-priority') { | |
| return task.priority === 1 || task.priority === 2; | |
| } | |
| return true; | |
| }); | |
| const getPriorityColor = (priority: number) => { | |
| switch (priority) { | |
| case 1: return 'text-red-400'; | |
| case 2: return 'text-orange-400'; | |
| case 3: return 'text-yellow-400'; | |
| default: return 'text-gray-400'; | |
| } | |
| }; | |
| return ( | |
| <div className="h-full flex flex-col bg-gradient-to-br from-emerald-500/10 to-teal-500/10 backdrop-blur-sm border border-emerald-500/30 rounded-lg overflow-hidden"> | |
| {/* Header */} | |
| <div className="p-4 border-b border-emerald-500/20"> | |
| <div className="flex items-center gap-2 mb-3"> | |
| <CheckSquare className="w-5 h-5 text-emerald-400" /> | |
| <span className="font-display text-sm uppercase tracking-wider text-emerald-400"> | |
| Tasks | |
| </span> | |
| <div className="ml-auto px-2 py-1 bg-emerald-500/20 text-emerald-400 rounded text-xs"> | |
| {filteredTasks.filter(t => !t.completed).length} active | |
| </div> | |
| </div> | |
| {/* Filters */} | |
| <div className="flex gap-2"> | |
| {(['all', 'today', 'high-priority'] as const).map((f) => ( | |
| <button | |
| key={f} | |
| onClick={() => setFilter(f)} | |
| className={cn( | |
| "px-3 py-1 rounded text-xs transition-all", | |
| filter === f | |
| ? "bg-emerald-500 text-white" | |
| : "text-emerald-300 hover:bg-emerald-500/20" | |
| )} | |
| > | |
| {f.replace('-', ' ')} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Quick Add */} | |
| <div className="p-3 border-b border-emerald-500/20 bg-black/20"> | |
| <div className="flex gap-2"> | |
| <input | |
| type="text" | |
| value={newTaskTitle} | |
| onChange={(e) => setNewTaskTitle(e.target.value)} | |
| onKeyDown={(e) => e.key === 'Enter' && handleAddTask()} | |
| placeholder="Add a task..." | |
| className="flex-1 px-3 py-2 bg-white/5 border border-emerald-500/30 rounded text-sm text-white placeholder-emerald-300/50 focus:outline-none focus:border-emerald-500" | |
| /> | |
| <button | |
| onClick={handleAddTask} | |
| className="p-2 bg-emerald-500 hover:bg-emerald-600 rounded transition-colors" | |
| > | |
| <Plus className="w-4 h-4 text-white" /> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Task List */} | |
| <div className="flex-1 overflow-y-auto p-3 space-y-2"> | |
| {filteredTasks.length === 0 && ( | |
| <div className="text-center py-8"> | |
| <CheckSquare className="w-8 h-8 text-emerald-400/30 mx-auto mb-2" /> | |
| <p className="text-sm text-emerald-300/50">No tasks for this filter</p> | |
| </div> | |
| )} | |
| {filteredTasks.map((task) => ( | |
| <div | |
| key={task.id} | |
| className={cn( | |
| "group flex items-start gap-3 p-3 rounded-lg border transition-all", | |
| task.completed | |
| ? "bg-gray-500/10 border-gray-500/20 opacity-50" | |
| : "bg-emerald-500/10 border-emerald-500/30 hover:bg-emerald-500/20" | |
| )} | |
| > | |
| {/* Checkbox */} | |
| <button | |
| onClick={() => handleToggleTask(task)} | |
| className="mt-0.5" | |
| > | |
| {task.completed ? ( | |
| <CheckSquare className="w-5 h-5 text-emerald-400" /> | |
| ) : ( | |
| <Square className="w-5 h-5 text-emerald-300 hover:text-emerald-400" /> | |
| )} | |
| </button> | |
| {/* Task Content */} | |
| <div className="flex-1"> | |
| <div className={cn( | |
| "text-sm font-medium mb-1", | |
| task.completed ? "line-through text-gray-400" : "text-white" | |
| )}> | |
| {task.title} | |
| </div> | |
| <div className="flex items-center gap-3 text-xs"> | |
| {/* Priority */} | |
| <div className="flex items-center gap-1"> | |
| <Flag className={cn("w-3 h-3", getPriorityColor(task.priority))} /> | |
| <span className={getPriorityColor(task.priority)}>P{task.priority}</span> | |
| </div> | |
| {/* Due Date */} | |
| {task.dueDate && ( | |
| <div className="flex items-center gap-1 text-emerald-300"> | |
| <CalIcon className="w-3 h-3" /> | |
| {new Date(task.dueDate).toLocaleDateString('en-US', { | |
| month: 'short', | |
| day: 'numeric' | |
| })} | |
| </div> | |
| )} | |
| {/* Project */} | |
| {task.project && ( | |
| <span className="text-emerald-300/70"> | |
| @ {task.project} | |
| </span> | |
| )} | |
| {/* Source */} | |
| <span className="text-emerald-300/50 text-xs ml-auto"> | |
| {task.source} | |
| </span> | |
| </div> | |
| </div> | |
| {/* Delete (on hover) */} | |
| <button | |
| className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-red-500/20 rounded" | |
| > | |
| <Trash2 className="w-4 h-4 text-red-400" /> | |
| </button> | |
| </div> | |
| ))} | |
| </div> | |
| {/* Source Recommendations */} | |
| {recommendedSources.length > 0 && ( | |
| <div className="p-3 border-t border-emerald-500/20 bg-orange-500/10"> | |
| <div className="text-xs text-orange-400 mb-2"> | |
| Missing Task 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={() => refetch()} | |
| className="px-2 py-1 bg-orange-500 text-white rounded text-xs" | |
| > | |
| Enable | |
| </button> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| {/* Stats Footer */} | |
| <div className="p-3 border-t border-emerald-500/20 bg-black/20"> | |
| <div className="flex justify-around text-xs"> | |
| <div className="text-center"> | |
| <div className="text-emerald-400 font-semibold"> | |
| {tasks.filter(t => !t.completed).length} | |
| </div> | |
| <div className="text-emerald-300/50">Active</div> | |
| </div> | |
| <div className="text-center"> | |
| <div className="text-emerald-400 font-semibold"> | |
| {tasks.filter(t => t.completed).length} | |
| </div> | |
| <div className="text-emerald-300/50">Done</div> | |
| </div> | |
| <div className="text-center"> | |
| <div className="text-red-400 font-semibold"> | |
| {tasks.filter(t => !t.completed && t.priority <= 2).length} | |
| </div> | |
| <div className="text-emerald-300/50">High Pri</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |