|
1 | 1 | "use client"; |
2 | 2 |
|
3 | 3 | import { useState, useEffect, useCallback } from "react"; |
4 | | -import { HugeiconsIcon } from "@hugeicons/react"; |
5 | | -import { PlusSignIcon } from "@hugeicons/core-free-icons"; |
6 | | -import { Input } from "@/components/ui/input"; |
7 | | -import { Button } from "@/components/ui/button"; |
8 | | -import { ScrollArea } from "@/components/ui/scroll-area"; |
9 | 4 | import { cn } from "@/lib/utils"; |
10 | | -import { TaskCard } from "./TaskCard"; |
11 | 5 | import { useTranslation } from "@/hooks/useTranslation"; |
12 | 6 | import type { TaskItem, TaskStatus } from "@/types"; |
13 | 7 |
|
14 | 8 | interface TaskListProps { |
15 | 9 | sessionId: string; |
16 | 10 | } |
17 | 11 |
|
18 | | -type FilterTab = "all" | "in_progress" | "completed"; |
19 | | - |
20 | 12 | export function TaskList({ sessionId }: TaskListProps) { |
21 | 13 | const { t } = useTranslation(); |
22 | 14 | const [tasks, setTasks] = useState<TaskItem[]>([]); |
23 | 15 | const [loading, setLoading] = useState(false); |
24 | | - const [newTitle, setNewTitle] = useState(""); |
25 | | - const [filter, setFilter] = useState<FilterTab>("all"); |
26 | 16 |
|
27 | 17 | const fetchTasks = useCallback(async () => { |
28 | 18 | if (!sessionId) return; |
@@ -51,135 +41,74 @@ export function TaskList({ sessionId }: TaskListProps) { |
51 | 41 | return () => window.removeEventListener('tasks-updated', handler); |
52 | 42 | }, [fetchTasks]); |
53 | 43 |
|
54 | | - const handleCreate = async () => { |
55 | | - const title = newTitle.trim(); |
56 | | - if (!title || !sessionId) return; |
57 | | - |
58 | | - try { |
59 | | - const res = await fetch("/api/tasks", { |
60 | | - method: "POST", |
61 | | - headers: { "Content-Type": "application/json" }, |
62 | | - body: JSON.stringify({ session_id: sessionId, title }), |
63 | | - }); |
64 | | - if (res.ok) { |
65 | | - const data = await res.json(); |
66 | | - setTasks((prev) => [...prev, data.task]); |
67 | | - setNewTitle(""); |
68 | | - } |
69 | | - } catch { |
70 | | - // silently fail |
71 | | - } |
72 | | - }; |
73 | | - |
74 | | - const handleUpdate = async ( |
75 | | - id: string, |
76 | | - updates: { title?: string; status?: TaskStatus } |
77 | | - ) => { |
| 44 | + const handleToggle = async (task: TaskItem) => { |
| 45 | + const nextStatus: TaskStatus = task.status === "completed" ? "pending" : "completed"; |
78 | 46 | try { |
79 | | - const res = await fetch(`/api/tasks/${id}`, { |
| 47 | + const res = await fetch(`/api/tasks/${task.id}`, { |
80 | 48 | method: "PATCH", |
81 | 49 | headers: { "Content-Type": "application/json" }, |
82 | | - body: JSON.stringify(updates), |
| 50 | + body: JSON.stringify({ status: nextStatus }), |
83 | 51 | }); |
84 | 52 | if (res.ok) { |
85 | 53 | const data = await res.json(); |
86 | | - setTasks((prev) => prev.map((t) => (t.id === id ? data.task : t))); |
87 | | - } |
88 | | - } catch { |
89 | | - // silently fail |
90 | | - } |
91 | | - }; |
92 | | - |
93 | | - const handleDelete = async (id: string) => { |
94 | | - try { |
95 | | - const res = await fetch(`/api/tasks/${id}`, { method: "DELETE" }); |
96 | | - if (res.ok) { |
97 | | - setTasks((prev) => prev.filter((t) => t.id !== id)); |
| 54 | + setTasks((prev) => prev.map((t) => (t.id === task.id ? data.task : t))); |
98 | 55 | } |
99 | 56 | } catch { |
100 | 57 | // silently fail |
101 | 58 | } |
102 | 59 | }; |
103 | 60 |
|
104 | | - const filtered = tasks.filter((task) => { |
105 | | - if (filter === "all") return true; |
106 | | - if (filter === "in_progress") |
107 | | - return task.status === "pending" || task.status === "in_progress"; |
108 | | - if (filter === "completed") return task.status === "completed"; |
109 | | - return true; |
110 | | - }); |
111 | | - |
112 | | - const filterTabs: { key: FilterTab; label: string }[] = [ |
113 | | - { key: "all", label: t('tasks.all') }, |
114 | | - { key: "in_progress", label: t('tasks.active') }, |
115 | | - { key: "completed", label: t('tasks.done') }, |
116 | | - ]; |
| 61 | + if (loading && tasks.length === 0) { |
| 62 | + return ( |
| 63 | + <p className="py-2 text-center text-xs text-muted-foreground"> |
| 64 | + {t('tasks.loading')} |
| 65 | + </p> |
| 66 | + ); |
| 67 | + } |
| 68 | + |
| 69 | + if (tasks.length === 0) { |
| 70 | + return ( |
| 71 | + <p className="py-2 text-center text-xs text-muted-foreground"> |
| 72 | + {t('tasks.noTasks')} |
| 73 | + </p> |
| 74 | + ); |
| 75 | + } |
117 | 76 |
|
118 | 77 | return ( |
119 | | - <div className="flex h-full flex-col"> |
120 | | - {/* Filter tabs */} |
121 | | - <div className="flex items-center gap-1 pb-2"> |
122 | | - {filterTabs.map((tab) => ( |
123 | | - <Button |
124 | | - key={tab.key} |
125 | | - variant="ghost" |
126 | | - size="sm" |
127 | | - className={cn( |
128 | | - "h-6 px-2 text-[10px]", |
129 | | - filter === tab.key && "bg-accent" |
130 | | - )} |
131 | | - onClick={() => setFilter(tab.key)} |
| 78 | + <div className="flex flex-col gap-0.5"> |
| 79 | + {tasks.map((task) => { |
| 80 | + const isDone = task.status === "completed"; |
| 81 | + return ( |
| 82 | + <button |
| 83 | + key={task.id} |
| 84 | + className="flex items-center gap-2 rounded-md px-1 py-1 text-left hover:bg-accent/50 transition-colors" |
| 85 | + onClick={() => handleToggle(task)} |
132 | 86 | > |
133 | | - {tab.label} |
134 | | - </Button> |
135 | | - ))} |
136 | | - </div> |
137 | | - |
138 | | - {/* Add task input */} |
139 | | - <div className="flex items-center gap-1 pb-2"> |
140 | | - <Input |
141 | | - placeholder={t('tasks.addPlaceholder')} |
142 | | - value={newTitle} |
143 | | - onChange={(e) => setNewTitle(e.target.value)} |
144 | | - onKeyDown={(e) => { |
145 | | - if (e.key === "Enter") handleCreate(); |
146 | | - }} |
147 | | - className="h-7 text-xs" |
148 | | - /> |
149 | | - <Button |
150 | | - variant="ghost" |
151 | | - size="icon-sm" |
152 | | - onClick={handleCreate} |
153 | | - disabled={!newTitle.trim()} |
154 | | - > |
155 | | - <HugeiconsIcon icon={PlusSignIcon} className="h-3.5 w-3.5" /> |
156 | | - <span className="sr-only">{t('tasks.addTask')}</span> |
157 | | - </Button> |
158 | | - </div> |
159 | | - |
160 | | - {/* Task list */} |
161 | | - <ScrollArea className="flex-1"> |
162 | | - {loading && tasks.length === 0 ? ( |
163 | | - <p className="py-4 text-center text-xs text-muted-foreground"> |
164 | | - {t('tasks.loading')} |
165 | | - </p> |
166 | | - ) : filtered.length === 0 ? ( |
167 | | - <p className="py-4 text-center text-xs text-muted-foreground"> |
168 | | - {tasks.length === 0 ? t('tasks.noTasks') : t('tasks.noMatching')} |
169 | | - </p> |
170 | | - ) : ( |
171 | | - <div className="flex flex-col gap-1.5 pb-4"> |
172 | | - {filtered.map((task) => ( |
173 | | - <TaskCard |
174 | | - key={task.id} |
175 | | - task={task} |
176 | | - onUpdate={handleUpdate} |
177 | | - onDelete={handleDelete} |
178 | | - /> |
179 | | - ))} |
180 | | - </div> |
181 | | - )} |
182 | | - </ScrollArea> |
| 87 | + <span |
| 88 | + className={cn( |
| 89 | + "flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded border transition-colors", |
| 90 | + isDone |
| 91 | + ? "border-foreground bg-foreground text-background" |
| 92 | + : "border-muted-foreground/40" |
| 93 | + )} |
| 94 | + > |
| 95 | + {isDone && ( |
| 96 | + <svg className="h-2.5 w-2.5" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2"> |
| 97 | + <path d="M2.5 6l2.5 2.5 4.5-5" /> |
| 98 | + </svg> |
| 99 | + )} |
| 100 | + </span> |
| 101 | + <span |
| 102 | + className={cn( |
| 103 | + "flex-1 truncate text-xs", |
| 104 | + isDone && "text-muted-foreground line-through" |
| 105 | + )} |
| 106 | + > |
| 107 | + {task.title} |
| 108 | + </span> |
| 109 | + </button> |
| 110 | + ); |
| 111 | + })} |
183 | 112 | </div> |
184 | 113 | ); |
185 | 114 | } |
0 commit comments