"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import SidebarItem from "@/components/shared/sidebar/SidebarItem"; import { Button } from "@/components/ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTriggerTriangle, } from "@/components/ui/collapsible"; import { toast } from "@/components/ui/sonner"; import { BOOKMARK_DRAG_MIME } from "@/lib/bookmark-drag"; import { useTranslation } from "@/lib/i18n/client"; import { cn } from "@/lib/utils"; import { MoreHorizontal, Plus } from "lucide-react"; import type { ZBookmarkList } from "@karakeep/shared/types/lists"; import { augmentBookmarkListsWithInitialData, useAddBookmarkToList, useBookmarkLists, } from "@karakeep/shared-react/hooks/lists"; import { ZBookmarkListTreeNode } from "@karakeep/shared/utils/listUtils"; import { CollapsibleBookmarkLists } from "../lists/CollapsibleBookmarkLists"; import { EditListModal } from "../lists/EditListModal"; import { ListOptions } from "../lists/ListOptions"; import { InvitationNotificationBadge } from "./InvitationNotificationBadge"; function useDropTarget(listId: string, listName: string) { const { mutateAsync: addToList } = useAddBookmarkToList(); const [dropHighlight, setDropHighlight] = useState(false); const dragCounterRef = useRef(0); const { t } = useTranslation(); const onDragOver = useCallback((e: React.DragEvent) => { if (e.dataTransfer.types.includes(BOOKMARK_DRAG_MIME)) { e.preventDefault(); e.dataTransfer.dropEffect = "copy"; } }, []); const onDragEnter = useCallback((e: React.DragEvent) => { if (e.dataTransfer.types.includes(BOOKMARK_DRAG_MIME)) { e.preventDefault(); dragCounterRef.current++; setDropHighlight(true); } }, []); const onDragLeave = useCallback(() => { dragCounterRef.current--; if (dragCounterRef.current <= 0) { dragCounterRef.current = 0; setDropHighlight(false); } }, []); const onDrop = useCallback( async (e: React.DragEvent) => { dragCounterRef.current = 0; setDropHighlight(false); const bookmarkId = e.dataTransfer.getData(BOOKMARK_DRAG_MIME); if (!bookmarkId) return; e.preventDefault(); try { await addToList({ bookmarkId, listId }); toast({ description: t("lists.add_to_list_success", { list: listName, defaultValue: `Added to "${listName}"`, }), }); } catch { toast({ description: t("common.something_went_wrong", { defaultValue: "Something went wrong", }), variant: "destructive", }); } }, [addToList, listId, listName, t], ); return { dropHighlight, onDragOver, onDragEnter, onDragLeave, onDrop }; } function DroppableListSidebarItem({ node, level, open, numBookmarks, selectedListId, setSelectedListId, }: { node: ZBookmarkListTreeNode; level: number; open: boolean; numBookmarks?: number; selectedListId: string | null; setSelectedListId: (id: string | null) => void; }) { const canDrop = node.item.type === "manual" && (node.item.userRole === "owner" || node.item.userRole === "editor"); const { dropHighlight, onDragOver, onDragEnter, onDragLeave, onDrop } = useDropTarget(node.item.id, node.item.name); return ( 0 && ( ) } logo={ {node.item.icon} } name={node.item.name} path={`/dashboard/lists/${node.item.id}`} className="group px-0.5" right={ { if (isOpen) { setSelectedListId(node.item.id); } else { setSelectedListId(null); } }} list={node.item} > } linkClassName="py-0.5" style={{ marginLeft: `${level * 1}rem` }} dropHighlight={canDrop && dropHighlight} onDragOver={canDrop ? onDragOver : undefined} onDragEnter={canDrop ? onDragEnter : undefined} onDragLeave={canDrop ? onDragLeave : undefined} onDrop={canDrop ? onDrop : undefined} /> ); } export default function AllLists({ initialData, }: { initialData: { lists: ZBookmarkList[] }; }) { const { t } = useTranslation(); const pathName = usePathname(); const isNodeOpen = useCallback( (node: ZBookmarkListTreeNode) => pathName.includes(node.item.id), [pathName], ); const [selectedListId, setSelectedListId] = useState(null); // Fetch live lists data const { data: listsData } = useBookmarkLists(undefined, { initialData: { lists: initialData.lists }, }); const lists = augmentBookmarkListsWithInitialData( listsData, initialData.lists, ); // Check if any shared list is currently being viewed const isViewingSharedList = useMemo(() => { return lists.data.some( (list) => list.userRole !== "owner" && pathName.includes(list.id), ); }, [lists.data, pathName]); // Check if there are any shared lists const hasSharedLists = useMemo(() => { return lists.data.some((list) => list.userRole !== "owner"); }, [lists.data]); const [sharedListsOpen, setSharedListsOpen] = useState(isViewingSharedList); // Auto-open shared lists if viewing one useEffect(() => { if (isViewingSharedList && !sharedListsOpen) { setSharedListsOpen(true); } }, [isViewingSharedList, sharedListsOpen]); return (
  • Lists

  • 📋} name={t("lists.all_lists")} path={`/dashboard/lists`} linkClassName="py-0.5" className="px-0.5" right={} /> ⭐️} name={t("lists.favourites")} path={`/dashboard/favourites`} linkClassName="py-0.5" className="px-0.5" /> {/* Owned Lists */} node.item.userRole === "owner"} isOpenFunc={isNodeOpen} render={({ node, level, open, numBookmarks }) => ( )} /> {/* Shared Lists */} {hasSharedLists && ( } logo={👥} name={t("lists.shared_lists")} path="#" linkClassName="py-0.5" className="px-0.5" /> node.item.userRole !== "owner"} isOpenFunc={isNodeOpen} indentOffset={1} render={({ node, level, open, numBookmarks }) => ( )} /> )}
); }