"use client"; import { FileText, type LucideIcon, MoreHorizontal, Plus, RefreshCw, Trash2 } from "lucide-react"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { useCallback, useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { notesApiService } from "@/lib/apis/notes-api.service"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { SidebarGroup, SidebarGroupContent, SidebarMenu, SidebarMenuAction, SidebarMenuButton, SidebarMenuItem, } from "@/components/ui/sidebar"; import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; import { cn } from "@/lib/utils"; import { useEffect, useRef } from "react"; import { createPortal } from "react-dom"; // Map of icon names to their components const actionIconMap: Record = { FileText, Trash2, MoreHorizontal, RefreshCw, }; interface NoteAction { name: string; icon: string; onClick: () => void; } interface NoteItem { name: string; url: string; icon: LucideIcon; id?: number; search_space_id?: number; actions?: NoteAction[]; } interface AllNotesSidebarProps { open: boolean; onOpenChange: (open: boolean) => void; searchSpaceId: string; onAddNote?: () => void; hoverTimeoutRef?: React.MutableRefObject; } export function AllNotesSidebar({ open, onOpenChange, searchSpaceId, onAddNote, hoverTimeoutRef, }: AllNotesSidebarProps) { const t = useTranslations("sidebar"); const router = useRouter(); const [isDeleting, setIsDeleting] = useState(null); const sidebarRef = useRef(null); const [sidebarLeft, setSidebarLeft] = useState(0); // Position from left edge of viewport // Calculate the sidebar's right edge position useEffect(() => { if (typeof window === "undefined") return; const updatePosition = () => { // Find the actual sidebar element (the fixed positioned one) const sidebarElement = document.querySelector( '[data-slot="sidebar"][data-sidebar="sidebar"]' ) as HTMLElement; if (sidebarElement) { const rect = sidebarElement.getBoundingClientRect(); // Set the left position to be the right edge of the sidebar setSidebarLeft(rect.right); } else { // Fallback: try to find any sidebar element const fallbackSidebar = document.querySelector('[data-slot="sidebar"]') as HTMLElement; if (fallbackSidebar) { const rect = fallbackSidebar.getBoundingClientRect(); setSidebarLeft(rect.right); } else { // Final fallback: use CSS variable const sidebarWidth = getComputedStyle(document.documentElement) .getPropertyValue("--sidebar-width") .trim(); if (sidebarWidth) { const remValue = parseFloat(sidebarWidth); setSidebarLeft(remValue * 16); // Convert rem to px } else { setSidebarLeft(256); // Default 16rem } } } }; updatePosition(); // Update on window resize and scroll window.addEventListener("resize", updatePosition); window.addEventListener("scroll", updatePosition, true); // Use MutationObserver to watch for sidebar state changes const observer = new MutationObserver(updatePosition); const sidebarWrapper = document.querySelector('[data-slot="sidebar-wrapper"]'); if (sidebarWrapper) { observer.observe(sidebarWrapper, { attributes: true, attributeFilter: ["data-state", "class"], childList: true, subtree: true, }); } // Also observe the sidebar element directly if it exists const sidebarElement = document.querySelector('[data-slot="sidebar"]'); if (sidebarElement) { observer.observe(sidebarElement, { attributes: true, attributeFilter: ["data-state", "class"], childList: false, subtree: false, }); } return () => { window.removeEventListener("resize", updatePosition); window.removeEventListener("scroll", updatePosition, true); observer.disconnect(); }; }, []); // Handle Escape key to close sidebar useEffect(() => { if (!open) return; const handleEscape = (e: KeyboardEvent) => { if (e.key === "Escape") { onOpenChange(false); } }; window.addEventListener("keydown", handleEscape); return () => window.removeEventListener("keydown", handleEscape); }, [open, onOpenChange]); // Fetch all notes const { data: notesData, error: notesError, isLoading: isLoadingNotes, refetch: refetchNotes, } = useQuery({ queryKey: ["all-notes", searchSpaceId], queryFn: () => notesApiService.getNotes({ search_space_id: Number(searchSpaceId), page_size: 1000, // Get all notes }), enabled: !!searchSpaceId && open, // Only fetch when sidebar is open }); // Handle note deletion with loading state const handleDeleteNote = useCallback( async (noteId: number, deleteAction: () => void) => { setIsDeleting(noteId); try { await deleteAction(); refetchNotes(); } finally { setIsDeleting(null); } }, [refetchNotes] ); // Transform notes to the format expected by the component const allNotes = useMemo(() => { return notesData?.items ? notesData.items.map((note) => ({ name: note.title, url: `/dashboard/${note.search_space_id}/editor/${note.id}`, icon: FileText as LucideIcon, id: note.id, search_space_id: note.search_space_id, actions: [ { name: "Delete", icon: "Trash2", onClick: async () => { try { await notesApiService.deleteNote({ search_space_id: note.search_space_id, note_id: note.id, }); } catch (error) { console.error("Error deleting note:", error); } }, }, ], })) : []; }, [notesData]); // Enhanced note item component const NoteItemComponent = useCallback( ({ note }: { note: NoteItem }) => { const isDeletingNote = isDeleting === note.id; return ( { router.push(note.url); onOpenChange(false); // Close sidebar when navigating }} disabled={isDeletingNote} className={cn("group/item relative", isDeletingNote && "opacity-50")} > {note.name} {note.actions && note.actions.length > 0 && ( More {note.actions.map((action, actionIndex) => { const ActionIcon = actionIconMap[action.icon] || FileText; const isDeleteAction = action.name.toLowerCase().includes("delete"); return ( { if (isDeleteAction) { handleDeleteNote(note.id || 0, action.onClick); } else { action.onClick(); } }} disabled={isDeletingNote} className={isDeleteAction ? "text-destructive" : ""} > {isDeletingNote && isDeleteAction ? "Deleting..." : action.name} ); })} )} ); }, [isDeleting, router, onOpenChange, handleDeleteNote] ); const sidebarContent = (
{ // Clear any pending close timeout when hovering over sidebar if (hoverTimeoutRef?.current) { clearTimeout(hoverTimeoutRef.current); hoverTimeoutRef.current = null; } }} onMouseLeave={() => { // Close sidebar when mouse leaves if (hoverTimeoutRef) { hoverTimeoutRef.current = setTimeout(() => { onOpenChange(false); }, 200); } else { onOpenChange(false); } }} >
{/* Header */}

{t("all_notes") || "All Notes"}

{/* Content */}
{isLoadingNotes ? ( {t("loading") || "Loading..."} ) : notesError ? ( {t("error_loading_notes") || "Error loading notes"} ) : allNotes.length > 0 ? ( {allNotes.map((note) => ( ))} ) : ( {onAddNote ? ( { onAddNote(); onOpenChange(false); }} className="text-muted-foreground hover:text-sidebar-foreground text-xs" > {t("create_new_note") || "Create a new note"} ) : ( {t("no_notes") || "No notes yet"} )} )}
{/* Footer with Add Note button */} {onAddNote && (
)}
); // Render sidebar via portal to avoid stacking context issues if (typeof window === "undefined") { return null; } return createPortal(sidebarContent, document.body); }