import React, { useState, useEffect, useCallback, useRef } from "react"; import { createRoot } from "react-dom/client"; interface Project { path: string; name: string; color: string; } interface AggregatedTask { id: string; title: string; status: string; priority?: string; description?: string; assignee: string[]; labels: string[]; projectName: string; projectColor: string; projectPath: string; createdDate?: string; updatedDate?: string; } interface WebSocketMessage { type: "init" | "update"; projects: Project[]; tasks: AggregatedTask[]; timestamp?: number; } function App() { const [projects, setProjects] = useState([]); const [tasks, setTasks] = useState([]); const [connected, setConnected] = useState(false); const [lastUpdate, setLastUpdate] = useState(null); const [filter, setFilter] = useState(""); const [selectedProject, setSelectedProject] = useState(null); const wsRef = useRef(null); const reconnectTimeoutRef = useRef | null>(null); const connectWebSocket = useCallback(() => { const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const ws = new WebSocket(`${protocol}//${window.location.host}`); ws.onopen = () => { setConnected(true); console.log("WebSocket connected"); }; ws.onmessage = (event) => { try { const data: WebSocketMessage = JSON.parse(event.data); if (data.type === "init" || data.type === "update") { setProjects(data.projects); setTasks(data.tasks); setLastUpdate(new Date()); } } catch (error) { console.error("Failed to parse WebSocket message:", error); } }; ws.onclose = () => { setConnected(false); console.log("WebSocket disconnected, reconnecting..."); reconnectTimeoutRef.current = setTimeout(connectWebSocket, 3000); }; ws.onerror = (error) => { console.error("WebSocket error:", error); }; wsRef.current = ws; }, []); useEffect(() => { connectWebSocket(); return () => { wsRef.current?.close(); if (reconnectTimeoutRef.current) { clearTimeout(reconnectTimeoutRef.current); } }; }, [connectWebSocket]); // Group tasks by status const statuses = ["To Do", "In Progress", "Done"]; const tasksByStatus = statuses.reduce( (acc, status) => { acc[status] = tasks.filter((t) => { const statusMatch = t.status.toLowerCase() === status.toLowerCase(); const projectMatch = !selectedProject || t.projectName === selectedProject; const filterMatch = !filter || t.title.toLowerCase().includes(filter.toLowerCase()) || t.projectName.toLowerCase().includes(filter.toLowerCase()); return statusMatch && projectMatch && filterMatch; }); return acc; }, {} as Record, ); return (
{/* Header */}

Backlog Aggregator

{connected ? "Live" : "Reconnecting..."}
{projects.length} projects | {tasks.length} tasks {lastUpdate && ` | Updated ${lastUpdate.toLocaleTimeString()}`}
{/* Project Filter Bar */}
setFilter(e.target.value)} style={{ padding: "0.5rem 1rem", borderRadius: "0.375rem", border: "1px solid #334155", backgroundColor: "#0f172a", color: "#e2e8f0", minWidth: "200px", }} /> {projects.map((project) => ( ))}
{/* Kanban Board */}
{statuses.map((status) => (
{/* Column Header */}

{status}

{tasksByStatus[status]?.length || 0}
{/* Tasks */}
{tasksByStatus[status]?.map((task) => ( ))} {(!tasksByStatus[status] || tasksByStatus[status].length === 0) && (
No tasks
)}
))}
); } function TaskCard({ task }: { task: AggregatedTask }) { const [expanded, setExpanded] = useState(false); const priorityColors: Record = { high: "#ef4444", medium: "#f59e0b", low: "#22c55e", }; return (
setExpanded(!expanded)} style={{ backgroundColor: "#0f172a", borderRadius: "0.375rem", padding: "0.75rem", cursor: "pointer", borderLeft: `4px solid ${task.projectColor}`, transition: "transform 0.1s, box-shadow 0.1s", }} onMouseEnter={(e) => { e.currentTarget.style.transform = "translateY(-1px)"; e.currentTarget.style.boxShadow = "0 4px 6px -1px rgba(0, 0, 0, 0.3)"; }} onMouseLeave={(e) => { e.currentTarget.style.transform = "translateY(0)"; e.currentTarget.style.boxShadow = "none"; }} > {/* Task Header */}
{task.title}
{task.id} | {task.projectName}
{task.priority && ( {task.priority} )}
{/* Labels */} {task.labels && task.labels.length > 0 && (
{task.labels.map((label) => ( {label} ))}
)} {/* Expanded Details */} {expanded && task.description && (
{task.description.slice(0, 500)} {task.description.length > 500 && "..."}
)} {/* Assignees */} {task.assignee && task.assignee.length > 0 && (
{task.assignee.join(", ")}
)}
); } // Mount the app const root = createRoot(document.getElementById("root")!); root.render();