From 379c49b2cd6d081cbe593c969b6f2128b60407c9 Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sun, 2 Mar 2025 10:46:23 +0000 Subject: feat(web): Show list stats in the sidebar --- .../dashboard/lists/CollapsibleBookmarkLists.tsx | 7 ++ .../web/components/dashboard/lists/ListOptions.tsx | 8 +- apps/web/components/dashboard/sidebar/AllLists.tsx | 107 +++++++++++++-------- packages/shared-react/hooks/bookmarks.ts | 4 + packages/shared-react/hooks/lists.ts | 2 + packages/trpc/routers/lists.ts | 11 +++ 6 files changed, 97 insertions(+), 42 deletions(-) diff --git a/apps/web/components/dashboard/lists/CollapsibleBookmarkLists.tsx b/apps/web/components/dashboard/lists/CollapsibleBookmarkLists.tsx index 77a67f5f..522bb1d6 100644 --- a/apps/web/components/dashboard/lists/CollapsibleBookmarkLists.tsx +++ b/apps/web/components/dashboard/lists/CollapsibleBookmarkLists.tsx @@ -1,6 +1,8 @@ import { useEffect, useState } from "react"; import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible"; import { FullPageSpinner } from "@/components/ui/full-page-spinner"; +import { api } from "@/lib/trpc"; +import { keepPreviousData } from "@tanstack/react-query"; import { augmentBookmarkListsWithInitialData, @@ -13,6 +15,7 @@ type RenderFunc = (params: { item: ZBookmarkListTreeNode; level: number; open: boolean; + numBookmarks?: number; }) => React.ReactNode; type IsOpenFunc = (list: ZBookmarkListTreeNode) => boolean; @@ -44,6 +47,9 @@ function ListItem({ useEffect(() => { setOpen((curr) => curr || isAnyChildOpen(node, isOpenFunc)); }, [node, isOpenFunc]); + const { data: listStats } = api.lists.stats.useQuery(undefined, { + placeholderData: keepPreviousData, + }); return ( @@ -51,6 +57,7 @@ function ListItem({ item: node, level, open, + numBookmarks: listStats?.stats.get(node.item.id), })} {node.children diff --git a/apps/web/components/dashboard/lists/ListOptions.tsx b/apps/web/components/dashboard/lists/ListOptions.tsx index a7217954..0e24d6a2 100644 --- a/apps/web/components/dashboard/lists/ListOptions.tsx +++ b/apps/web/components/dashboard/lists/ListOptions.tsx @@ -1,5 +1,3 @@ -"use client"; - import { useState } from "react"; import { DropdownMenu, @@ -17,8 +15,12 @@ import DeleteListConfirmationDialog from "./DeleteListConfirmationDialog"; export function ListOptions({ list, + isOpen, + onOpenChange, children, }: { + isOpen?: boolean; + onOpenChange?: (open: boolean) => void; list: ZBookmarkList; children?: React.ReactNode; }) { @@ -29,7 +31,7 @@ export function ListOptions({ const [editModalOpen, setEditModalOpen] = useState(false); return ( - + pathName.includes(node.item.id), [pathName], ); + + const [selectedListId, setSelectedListId] = useState(null); + return (
  • @@ -49,45 +54,69 @@ export default function AllLists({ path={`/dashboard/favourites`} linkClassName="py-0.5" /> + ( + 0 && ( + + ) + } + logo={ + + {node.item.icon} + + } + name={node.item.name} + path={`/dashboard/lists/${node.item.id}`} + right={ + { + if (open) { + setSelectedListId(node.item.id); + } else { + setSelectedListId(null); + } + }} + list={node.item} + > + - - } - linkClassName="group py-0.5" - style={{ marginLeft: `${level * 1}rem` }} - /> - )} - /> - } + + {numBookmarks} + + + + + } + linkClassName="group py-0.5" + style={{ marginLeft: `${level * 1}rem` }} + /> + )} + />
); } diff --git a/packages/shared-react/hooks/bookmarks.ts b/packages/shared-react/hooks/bookmarks.ts index 89715e4f..7339f6c2 100644 --- a/packages/shared-react/hooks/bookmarks.ts +++ b/packages/shared-react/hooks/bookmarks.ts @@ -30,6 +30,7 @@ export function useCreateBookmark( onSuccess: (res, req, meta) => { apiUtils.bookmarks.getBookmarks.invalidate(); apiUtils.bookmarks.searchBookmarks.invalidate(); + apiUtils.lists.stats.invalidate(); return opts[0]?.onSuccess?.(res, req, meta); }, }); @@ -61,6 +62,7 @@ export function useDeleteBookmark( apiUtils.bookmarks.getBookmarks.invalidate(); apiUtils.bookmarks.searchBookmarks.invalidate(); apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId }); + apiUtils.lists.stats.invalidate(); return opts[0]?.onSuccess?.(res, req, meta); }, }); @@ -76,6 +78,7 @@ export function useUpdateBookmark( apiUtils.bookmarks.getBookmarks.invalidate(); apiUtils.bookmarks.searchBookmarks.invalidate(); apiUtils.bookmarks.getBookmark.invalidate({ bookmarkId: req.bookmarkId }); + apiUtils.lists.stats.invalidate(); return opts[0]?.onSuccess?.(res, req, meta); }, }); @@ -138,6 +141,7 @@ export function useUpdateBookmarkTags( apiUtils.bookmarks.getBookmarks.invalidate({ tagId: id }); }); apiUtils.tags.list.invalidate(); + apiUtils.lists.stats.invalidate(); return opts[0]?.onSuccess?.(res, req, meta); }, }); diff --git a/packages/shared-react/hooks/lists.ts b/packages/shared-react/hooks/lists.ts index 46477228..ecb5d408 100644 --- a/packages/shared-react/hooks/lists.ts +++ b/packages/shared-react/hooks/lists.ts @@ -47,6 +47,7 @@ export function useAddBookmarkToList( apiUtils.lists.getListsOfBookmark.invalidate({ bookmarkId: req.bookmarkId, }); + apiUtils.lists.stats.invalidate(); return opts[0]?.onSuccess?.(res, req, meta); }, }); @@ -63,6 +64,7 @@ export function useRemoveBookmarkFromList( apiUtils.lists.getListsOfBookmark.invalidate({ bookmarkId: req.bookmarkId, }); + apiUtils.lists.stats.invalidate(); return opts[0]?.onSuccess?.(res, req, meta); }, }); diff --git a/packages/trpc/routers/lists.ts b/packages/trpc/routers/lists.ts index 59441879..f9e382f2 100644 --- a/packages/trpc/routers/lists.ts +++ b/packages/trpc/routers/lists.ts @@ -106,4 +106,15 @@ export const listsAppRouter = router({ const lists = await List.forBookmark(ctx, input.bookmarkId); return { lists: lists.map((l) => l.list) }; }), + stats: authedProcedure + .output( + z.object({ + stats: z.map(z.string(), z.number()), + }), + ) + .query(async ({ ctx }) => { + const lists = await List.getAll(ctx); + const sizes = await Promise.all(lists.map((l) => l.getSize())); + return { stats: new Map(lists.map((l, i) => [l.list.id, sizes[i]])) }; + }), }); -- cgit v1.2.3-70-g09d2