Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 21 additions & 13 deletions packages/app/src/components/home/GroupCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,15 @@ export const GroupCard = memo(function GroupCard({
const [isRenaming, setIsRenaming] = useState(false);
const [renameValue, setRenameValue] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
const openMenu = useCallback((x: number, y: number) => {
setMenuPos({ x, y });
setShowMenu(true);
}, []);

const closeMenu = useCallback(() => {
setShowMenu(false);
setMenuPos(null);
}, []);

const previewBooks = useMemo(
() =>
Expand All @@ -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();
Expand All @@ -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) => (
Expand All @@ -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);
}
}}
>
Expand All @@ -163,13 +174,11 @@ export const GroupCard = memo(function GroupCard({
<div
className="fixed inset-0 z-50"
onClick={() => {
setShowMenu(false);
setMenuPos(null);
closeMenu();
}}
onKeyDown={(event) => {
if (event.key === "Escape") {
setShowMenu(false);
setMenuPos(null);
closeMenu();
}
}}
/>
Expand All @@ -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);
}}
>
Expand Down
4 changes: 3 additions & 1 deletion packages/app/src/components/home/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
125 changes: 103 additions & 22 deletions packages/app/src/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export function HomeSidebar() {
removeTag,
renameTag,
removeGroup,
renameGroup,
} = useLibraryStore();
const setShowSettings = useAppStore((s) => s.setShowSettings);
const showSettings = useAppStore((s) => s.showSettings);
Expand All @@ -71,8 +72,23 @@ export function HomeSidebar() {
const [editingTag, setEditingTag] = useState<string | null>(null);
const [editingName, setEditingName] = useState("");
const [isTagsExpanded, setIsTagsExpanded] = useState(false);
const [editingGroup, setEditingGroup] = useState<string | null>(null);
const [editingGroupName, setEditingGroupName] = useState("");
const [groupMenu, setGroupMenu] = useState<{ groupId: string; x: number; y: number } | null>(null);
const newTagInputRef = useRef<HTMLInputElement>(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
Expand Down Expand Up @@ -218,28 +234,93 @@ export function HomeSidebar() {
{groups.length > 0 && (
<div className="flex flex-wrap gap-1 pt-1">
{groups.map((group) => (
<button
key={group.id}
type="button"
className={`rounded-full px-2.5 py-0.5 text-[11px] transition-colors ${
activeGroupId === group.id
? "bg-primary text-primary-foreground"
: "bg-muted text-muted-foreground hover:bg-muted/80 hover:text-foreground"
}`}
onClick={() => {
setActiveGroupId(group.id);
handleNavClick("home");
}}
onContextMenu={(e) => {
e.preventDefault();
const action = window.confirm(
`${group.name}\n\n${t("sidebar.deleteGroupConfirm")}`,
);
if (action) void removeGroup(group.id);
}}
>
{group.name}
</button>
<div key={group.id} className="group/group relative">
{editingGroup === group.id ? (
<input
type="text"
className="w-24 rounded-full border border-primary/40 bg-background px-2.5 py-0.5 text-[11px] outline-none"
value={editingGroupName}
onChange={(e) => 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
/>
) : (
<>
<button
type="button"
className={`rounded-full px-2.5 py-0.5 text-[11px] transition-colors ${
activeGroupId === group.id
? "bg-primary text-primary-foreground"
: "bg-muted text-muted-foreground hover:bg-muted/80 hover:text-foreground"
}`}
onClick={() => {
setActiveGroupId(group.id);
handleNavClick("home");
}}
onContextMenu={(event) => {
event.preventDefault();
event.stopPropagation();
setGroupMenu({
groupId: group.id,
x: event.clientX,
y: event.clientY,
});
}}
>
{group.name}
</button>
{groupMenu?.groupId === group.id && (
<>
<div
className="fixed inset-0 z-50"
onClick={() => setGroupMenu(null)}
onKeyDown={(event) => {
if (event.key === "Escape") setGroupMenu(null);
}}
/>
<div
className="fixed z-50 min-w-28 rounded-lg border bg-popover p-1 shadow-lg"
style={{ top: groupMenu.y + 4, left: groupMenu.x }}
onClick={(event) => event.stopPropagation()}
onKeyDown={(event) => event.stopPropagation()}
>
<button
type="button"
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-xs text-foreground hover:bg-muted"
onClick={() => {
setGroupMenu(null);
setEditingGroup(group.id);
setEditingGroupName(group.name);
}}
>
<Pencil size={12} />
{t("common.rename", "Rename")}
</button>
<button
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={() => {
setGroupMenu(null);
handleDeleteGroup(group);
}}
>
<Trash2 size={12} />
{t("common.delete", "Delete")}
</button>
</div>
</>
)}
</>
)}
</div>
))}
</div>
)}
Expand Down