diff --git a/packages/app/src/components/home/GroupCard.tsx b/packages/app/src/components/home/GroupCard.tsx index 8ea4399b..17cd2b8a 100644 --- a/packages/app/src/components/home/GroupCard.tsx +++ b/packages/app/src/components/home/GroupCard.tsx @@ -94,6 +94,15 @@ export const GroupCard = memo(function GroupCard({ const [isRenaming, setIsRenaming] = useState(false); const [renameValue, setRenameValue] = useState(""); const inputRef = useRef(null); + const openMenu = useCallback((x: number, y: number) => { + setMenuPos({ x, y }); + setShowMenu(true); + }, []); + + const closeMenu = useCallback(() => { + setShowMenu(false); + setMenuPos(null); + }, []); const previewBooks = useMemo( () => @@ -107,10 +116,9 @@ export const GroupCard = memo(function GroupCard({ const handleStartRename = useCallback(() => { setRenameValue(group.name); setIsRenaming(true); - setShowMenu(false); - setMenuPos(null); + closeMenu(); requestAnimationFrame(() => inputRef.current?.focus()); - }, [group.name]); + }, [group.name, closeMenu]); const commitRename = useCallback(() => { const name = renameValue.trim(); @@ -127,6 +135,11 @@ export const GroupCard = memo(function GroupCard({ onClick={() => { if (!isRenaming) onOpen(group.id); }} + onContextMenu={(event) => { + event.preventDefault(); + event.stopPropagation(); + if (!isRenaming) openMenu(event.clientX, event.clientY); + }} > {previewBooks.length > 0 ? ( previewBooks.map((book, index) => ( @@ -145,12 +158,10 @@ export const GroupCard = memo(function GroupCard({ onClick={(event) => { event.stopPropagation(); if (showMenu) { - setShowMenu(false); - setMenuPos(null); + closeMenu(); } else { const rect = event.currentTarget.getBoundingClientRect(); - setMenuPos({ x: rect.right, y: rect.top }); - setShowMenu(true); + openMenu(rect.right, rect.top); } }} > @@ -163,13 +174,11 @@ export const GroupCard = memo(function GroupCard({
{ - setShowMenu(false); - setMenuPos(null); + closeMenu(); }} onKeyDown={(event) => { if (event.key === "Escape") { - setShowMenu(false); - setMenuPos(null); + closeMenu(); } }} /> @@ -191,8 +200,7 @@ export const GroupCard = memo(function GroupCard({ type="button" className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-xs text-destructive hover:bg-destructive/10" onClick={() => { - setShowMenu(false); - setMenuPos(null); + closeMenu(); onDelete(group); }} > diff --git a/packages/app/src/components/home/HomePage.tsx b/packages/app/src/components/home/HomePage.tsx index fc42eb7e..c7514cef 100644 --- a/packages/app/src/components/home/HomePage.tsx +++ b/packages/app/src/components/home/HomePage.tsx @@ -283,9 +283,11 @@ export function HomePage() { const handleDeleteGroup = useCallback( async (group: BookGroup) => { + const confirmed = window.confirm(`${group.name}\n\n${t("sidebar.deleteGroupConfirm")}`); + if (!confirmed) return; await removeGroup(group.id); }, - [removeGroup], + [removeGroup, t], ); const toggleBookSelection = useCallback((bookId: string) => { diff --git a/packages/app/src/components/layout/Sidebar.tsx b/packages/app/src/components/layout/Sidebar.tsx index 57365cae..8c30010e 100644 --- a/packages/app/src/components/layout/Sidebar.tsx +++ b/packages/app/src/components/layout/Sidebar.tsx @@ -60,6 +60,7 @@ export function HomeSidebar() { removeTag, renameTag, removeGroup, + renameGroup, } = useLibraryStore(); const setShowSettings = useAppStore((s) => s.setShowSettings); const showSettings = useAppStore((s) => s.showSettings); @@ -71,8 +72,23 @@ export function HomeSidebar() { const [editingTag, setEditingTag] = useState(null); const [editingName, setEditingName] = useState(""); const [isTagsExpanded, setIsTagsExpanded] = useState(false); + const [editingGroup, setEditingGroup] = useState(null); + const [editingGroupName, setEditingGroupName] = useState(""); + const [groupMenu, setGroupMenu] = useState<{ groupId: string; x: number; y: number } | null>(null); const newTagInputRef = useRef(null); + const handleDeleteGroup = (group: (typeof groups)[number]) => { + const confirmed = window.confirm(`${group.name}\n\n${t("sidebar.deleteGroupConfirm")}`); + if (confirmed) void removeGroup(group.id); + }; + + const handleRenameGroup = (groupId: string) => { + const name = editingGroupName.trim(); + if (name) renameGroup(groupId, name); + setEditingGroup(null); + setEditingGroupName(""); + }; + // Refresh unread feedback count on mount and whenever the settings dialog // closes (the user may have marked replies as seen inside it). Depends on // `libraryLoaded` so the first run waits for DB init — otherwise the call @@ -218,28 +234,93 @@ export function HomeSidebar() { {groups.length > 0 && (
{groups.map((group) => ( - +
+ {editingGroup === group.id ? ( + setEditingGroupName(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleRenameGroup(group.id); + } else if (e.key === "Escape") { + setEditingGroup(null); + setEditingGroupName(""); + } + }} + onBlur={() => handleRenameGroup(group.id)} + autoFocus + /> + ) : ( + <> + + {groupMenu?.groupId === group.id && ( + <> +
setGroupMenu(null)} + onKeyDown={(event) => { + if (event.key === "Escape") setGroupMenu(null); + }} + /> +
event.stopPropagation()} + onKeyDown={(event) => event.stopPropagation()} + > + + +
+ + )} + + )} +
))}
)}