/
/
1 import { useState, useEffect, useCallback } from "react"; 2
3 const API_BASE = "http://localhost:8080/api/v1"; 4
5 interface Todo { 6 id: string; 7 text: string; 8 completed: boolean; 9 createdAt: string; 10 priority: "low" | "medium" | "high"; 11 } 12
13 /* @gitian 14 * --group=clients 15 * useTodos hook — manages the full lifecycle of todos fetched 16 * from the Go API. Polls every 5 seconds for changes from 17 * other clients. 18 */ 19 function useTodos() { 20 const [todos, setTodos] = useState<Todo[]>([]); 21 const [loading, setLoading] = useState(true); 22
23 const refresh = useCallback(async () => { 24 // @gitian:todo Add error handling with retry + exponential backoff 25 const res = await fetch(`${API_BASE}/todos`); 26 setTodos(await res.json()); 27 setLoading(false); 28 }, []); 29
30 useEffect(() => { 31 refresh(); 32 // @gitian:perf Polling is simple but wasteful — switch to 33 // Server-Sent Events or WebSocket for real-time updates 34 const interval = setInterval(refresh, 5000); 35 return () => clearInterval(interval); 36 }, [refresh]); 37
38 const addTodo = useCallback(async (text: string) => { 39 const res = await fetch(`${API_BASE}/todos`, { 40 method: "POST", 41 headers: { "Content-Type": "application/json" }, 42 body: JSON.stringify({ text }), 43 }); 44 const todo = await res.json(); 45 setTodos((prev) => [...prev, todo]); 46 }, []); 47
48 const toggleTodo = useCallback(async (id: string, completed: boolean) => { 49 await fetch(`${API_BASE}/todos/${id}`, { 50 method: "PUT", 51 headers: { "Content-Type": "application/json" }, 52 body: JSON.stringify({ completed: !completed }), 53 }); 54 setTodos((prev) => 55 prev.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t)) 56 ); 57 }, []); 58
59 return { todos, loading, addTodo, toggleTodo }; 60 } 61
62 {/* @gitian 63 * --group=clients 64 * App component — renders the todo list with an add form. 65 * All data flows through the useTodos hook above. 66 */} 67 export function App() { 68 const { todos, loading, addTodo, toggleTodo } = useTodos(); 69 const completed = todos.filter((t) => t.completed).length; 70
71 // @gitian:warning No optimistic UI — toggles wait for the server 72 // round-trip before reflecting in the UI, causing visible lag 73
74 if (loading) return <p>Loading...</p>; 75
76 return ( 77 <div style={{ maxWidth: 480, margin: "2rem auto", fontFamily: "system-ui" }}> 78 <h1>Todo App</h1> 79 <p>{completed}/{todos.length} completed</p> 80 <form onSubmit={(e) => { 81 e.preventDefault(); 82 const input = e.currentTarget.elements.namedItem("text") as HTMLInputElement; 83 if (input.value.trim()) { 84 addTodo(input.value.trim()); 85 input.value = ""; 86 } 87 }}> 88 <input name="text" placeholder="What needs to be done?" /> 89 <button type="submit">Add</button> 90 </form> 91 <ul> 92 {todos.map((t) => ( 93 <li key={t.id} style={{ opacity: t.completed ? 0.5 : 1 }}> 94 <input 95 type="checkbox" 96 checked={t.completed} 97 onChange={() => toggleTodo(t.id, t.completed)} 98 /> 99 <span>{t.text}</span> 100 </li> 101 ))} 102 </ul> 103 </div> 104 ); 105 } 106