TopicList.tsx (3279B)
1 import { useState } from "react"; 2 import { useTopicStore } from "../../store/topic"; 3 import { TopicItem } from "./TopicItem"; 4 5 export function TopicList() { 6 const { topics, selectedTopicId, selectTopic, createTopic, updateTopic } = 7 useTopicStore(); 8 const [filter, setFilter] = useState(""); 9 const [adding, setAdding] = useState(false); 10 const [newName, setNewName] = useState(""); 11 const [dragId, setDragId] = useState<string | null>(null); 12 13 const filtered = topics.filter((t) => 14 t.name.toLowerCase().includes(filter.toLowerCase()), 15 ); 16 17 const handleAdd = async () => { 18 if (!newName.trim()) return; 19 await createTopic(newName.trim()); 20 setNewName(""); 21 setAdding(false); 22 }; 23 24 const handleDragStart = (id: string) => setDragId(id); 25 26 const handleDragOver = (e: React.DragEvent, targetId: string) => { 27 e.preventDefault(); 28 if (!dragId || dragId === targetId) return; 29 const fromIdx = topics.findIndex((t) => t.id === dragId); 30 const toIdx = topics.findIndex((t) => t.id === targetId); 31 if (fromIdx === -1 || toIdx === -1) return; 32 updateTopic(dragId, { sort_order: toIdx }); 33 updateTopic(targetId, { sort_order: fromIdx }); 34 }; 35 36 const handleDragEnd = () => setDragId(null); 37 38 return ( 39 <div className="mb-4"> 40 <div className="mb-2 flex items-center justify-between"> 41 <h2 className="text-xs font-semibold uppercase text-gray-500"> 42 Topics 43 </h2> 44 <button 45 className="text-xs text-gray-500 hover:text-gray-300" 46 onClick={() => setAdding(true)} 47 > 48 + New 49 </button> 50 </div> 51 52 <input 53 type="text" 54 placeholder="Search topics..." 55 value={filter} 56 onChange={(e) => setFilter(e.target.value)} 57 className="mb-2 w-full rounded border border-gray-700 bg-gray-800 px-2 py-1 text-xs text-gray-300 outline-none focus:border-gray-500" 58 /> 59 60 {adding && ( 61 <div className="mb-1 flex gap-1"> 62 <input 63 autoFocus 64 value={newName} 65 onChange={(e) => setNewName(e.target.value)} 66 onKeyDown={(e) => e.key === "Enter" && handleAdd()} 67 placeholder="Topic name" 68 className="flex-1 rounded border border-gray-700 bg-gray-800 px-2 py-1 text-xs text-gray-300 outline-none" 69 /> 70 <button 71 className="rounded bg-indigo-600 px-2 py-1 text-xs text-white hover:bg-indigo-500" 72 onClick={handleAdd} 73 > 74 Add 75 </button> 76 </div> 77 )} 78 79 <div className="space-y-0.5"> 80 {filtered.map((topic) => ( 81 <div 82 key={topic.id} 83 draggable 84 onDragStart={() => handleDragStart(topic.id)} 85 onDragOver={(e) => handleDragOver(e, topic.id)} 86 onDragEnd={handleDragEnd} 87 > 88 <TopicItem 89 topic={topic} 90 isSelected={selectedTopicId === topic.id} 91 onSelect={() => 92 selectTopic(selectedTopicId === topic.id ? null : topic.id) 93 } 94 /> 95 </div> 96 ))} 97 </div> 98 99 {filtered.length === 0 && !adding && ( 100 <p className="text-xs text-gray-600">No topics yet</p> 101 )} 102 </div> 103 ); 104 }