TopicItem.tsx (3546B)
1 import { useState, useRef, useEffect } from "react"; 2 import type { Topic } from "../../store/topic"; 3 import { useTopicStore } from "../../store/topic"; 4 5 interface TopicItemProps { 6 topic: Topic; 7 isSelected: boolean; 8 onSelect: () => void; 9 } 10 11 export function TopicItem({ topic, isSelected, onSelect }: TopicItemProps) { 12 const [menuOpen, setMenuOpen] = useState(false); 13 const [editing, setEditing] = useState(false); 14 const [editName, setEditName] = useState(topic.name); 15 const menuRef = useRef<HTMLDivElement>(null); 16 const { updateTopic, deleteTopic } = useTopicStore(); 17 18 useEffect(() => { 19 if (!menuOpen) return; 20 const handleClick = (e: MouseEvent) => { 21 if (menuRef.current && !menuRef.current.contains(e.target as Node)) { 22 setMenuOpen(false); 23 } 24 }; 25 document.addEventListener("mousedown", handleClick); 26 return () => document.removeEventListener("mousedown", handleClick); 27 }, [menuOpen]); 28 29 const handleRename = async () => { 30 if (editName.trim() && editName !== topic.name) { 31 await updateTopic(topic.id, { name: editName.trim() }); 32 } 33 setEditing(false); 34 }; 35 36 const handleRecolor = async (color: string) => { 37 await updateTopic(topic.id, { color }); 38 setMenuOpen(false); 39 }; 40 41 const handleDelete = async () => { 42 await deleteTopic(topic.id); 43 setMenuOpen(false); 44 }; 45 46 return ( 47 <div 48 className={`group flex items-center gap-2 rounded px-2 py-1.5 text-sm cursor-pointer transition-colors ${ 49 isSelected 50 ? "bg-gray-700 text-white" 51 : "text-gray-400 hover:bg-gray-800 hover:text-gray-200" 52 }`} 53 onClick={onSelect} 54 onContextMenu={(e) => { 55 e.preventDefault(); 56 setMenuOpen(true); 57 }} 58 > 59 <span 60 className="h-2.5 w-2.5 flex-shrink-0 rounded-full" 61 style={{ backgroundColor: topic.color }} 62 /> 63 {editing ? ( 64 <input 65 autoFocus 66 value={editName} 67 onChange={(e) => setEditName(e.target.value)} 68 onBlur={handleRename} 69 onKeyDown={(e) => e.key === "Enter" && handleRename()} 70 className="flex-1 bg-transparent border-b border-gray-600 outline-none text-sm" 71 onClick={(e) => e.stopPropagation()} 72 /> 73 ) : ( 74 <span className="flex-1 truncate">{topic.name}</span> 75 )} 76 <span className="text-xs text-gray-600">{topic.session_count ?? 0}</span> 77 78 {menuOpen && ( 79 <div 80 ref={menuRef} 81 className="absolute right-0 z-50 mt-1 w-40 rounded border border-gray-700 bg-gray-900 shadow-lg" 82 > 83 <button 84 className="w-full px-3 py-1.5 text-left text-sm text-gray-300 hover:bg-gray-800" 85 onClick={() => { 86 setEditing(true); 87 setMenuOpen(false); 88 }} 89 > 90 Rename 91 </button> 92 <div className="flex gap-1 px-3 py-1.5"> 93 {["#6366f1", "#ef4444", "#10b981", "#f59e0b", "#8b5cf6"].map( 94 (c) => ( 95 <button 96 key={c} 97 className="h-4 w-4 rounded-full border border-gray-600" 98 style={{ backgroundColor: c }} 99 onClick={() => handleRecolor(c)} 100 /> 101 ), 102 )} 103 </div> 104 <button 105 className="w-full px-3 py-1.5 text-left text-sm text-red-400 hover:bg-gray-800" 106 onClick={handleDelete} 107 > 108 Delete 109 </button> 110 </div> 111 )} 112 </div> 113 ); 114 }