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>
122 lines
3.8 KiB
TypeScript
122 lines
3.8 KiB
TypeScript
/**
|
|
* Tasks Widget - displays task summary and list
|
|
*/
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { CheckCircle, Circle, Clock, AlertCircle } from "lucide-react";
|
|
import { TaskPriority, TaskStatus, type WidgetProps, type Task } from "@mosaic/shared";
|
|
import { fetchTasks } from "@/lib/api/tasks";
|
|
|
|
export function TasksWidget({ id: _id, config: _config }: WidgetProps): React.JSX.Element {
|
|
const [tasks, setTasks] = useState<Task[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
let isMounted = true;
|
|
|
|
const loadTasks = async (): Promise<void> => {
|
|
setIsLoading(true);
|
|
try {
|
|
const data = await fetchTasks();
|
|
if (isMounted) {
|
|
setTasks(data);
|
|
}
|
|
} catch {
|
|
if (isMounted) {
|
|
setTasks([]);
|
|
}
|
|
} finally {
|
|
if (isMounted) {
|
|
setIsLoading(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
void loadTasks();
|
|
|
|
return (): void => {
|
|
isMounted = false;
|
|
};
|
|
}, []);
|
|
|
|
const getPriorityIcon = (priority: TaskPriority): React.JSX.Element => {
|
|
switch (priority) {
|
|
case TaskPriority.HIGH:
|
|
return <AlertCircle className="w-4 h-4 text-red-500" />;
|
|
case TaskPriority.MEDIUM:
|
|
return <Clock className="w-4 h-4 text-yellow-500" />;
|
|
case TaskPriority.LOW:
|
|
return <Circle className="w-4 h-4 text-gray-400" />;
|
|
default:
|
|
return <Circle className="w-4 h-4 text-gray-400" />;
|
|
}
|
|
};
|
|
|
|
const getStatusIcon = (status: TaskStatus): React.JSX.Element => {
|
|
return status === TaskStatus.COMPLETED ? (
|
|
<CheckCircle className="w-4 h-4 text-green-500" />
|
|
) : (
|
|
<Circle className="w-4 h-4 text-gray-400" />
|
|
);
|
|
};
|
|
|
|
const stats = {
|
|
total: tasks.length,
|
|
inProgress: tasks.filter((t) => t.status === TaskStatus.IN_PROGRESS).length,
|
|
completed: tasks.filter((t) => t.status === TaskStatus.COMPLETED).length,
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-full">
|
|
<div className="text-gray-500 text-sm">Loading tasks...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col h-full space-y-3">
|
|
{/* Summary stats */}
|
|
<div className="grid grid-cols-3 gap-2 text-center">
|
|
<div className="bg-gray-50 rounded p-2">
|
|
<div className="text-2xl font-bold text-gray-900">{stats.total}</div>
|
|
<div className="text-xs text-gray-500">Total</div>
|
|
</div>
|
|
<div className="bg-blue-50 rounded p-2">
|
|
<div className="text-2xl font-bold text-blue-600">{stats.inProgress}</div>
|
|
<div className="text-xs text-blue-500">In Progress</div>
|
|
</div>
|
|
<div className="bg-green-50 rounded p-2">
|
|
<div className="text-2xl font-bold text-green-600">{stats.completed}</div>
|
|
<div className="text-xs text-green-500">Done</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Task list */}
|
|
<div className="flex-1 overflow-auto space-y-2">
|
|
{tasks.length === 0 ? (
|
|
<div className="text-center text-gray-500 text-sm py-4">No tasks yet</div>
|
|
) : (
|
|
tasks.slice(0, 5).map((task) => (
|
|
<div
|
|
key={task.id}
|
|
className="flex items-center gap-3 p-2 hover:bg-gray-50 rounded transition-colors"
|
|
>
|
|
{getStatusIcon(task.status)}
|
|
<div className="flex-1 min-w-0">
|
|
<div className="text-sm font-medium text-gray-900 truncate">{task.title}</div>
|
|
{task.dueDate && (
|
|
<div className="text-xs text-gray-500">
|
|
Due: {new Date(task.dueDate).toLocaleDateString()}
|
|
</div>
|
|
)}
|
|
</div>
|
|
{getPriorityIcon(task.priority)}
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|