Kraft102's picture
fix: sql.js Docker/Alpine compatibility layer for PatternMemory and FailureMemory
5a81b95
/**
* 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>
);
}