feat: removed All chats,notes sidebar sheet

- Integrated AnimatePresence and motion for smoother transitions in AllChatsSidebar and AllNotesSidebar.
- Added escape key handling to close the sidebar and body scroll locking when open.
- Refactored search input styles and layout for better usability and consistency.
- Ensured proper mounting of components before rendering to avoid layout shifts.
This commit is contained in:
DESKTOP-RTLN3BA\$punk 2025-12-23 20:51:10 -08:00
parent aeb9379c27
commit da559e4abc
2 changed files with 485 additions and 369 deletions

View file

@ -3,9 +3,11 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { format } from "date-fns";
import { FileText, Loader2, MoreHorizontal, Plus, Search, Trash2, X } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useCallback, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { createPortal } from "react-dom";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
@ -14,14 +16,6 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useDebouncedValue } from "@/hooks/use-debounced-value";
import { documentsApiService } from "@/lib/apis/documents-api.service";
@ -46,8 +40,37 @@ export function AllNotesSidebar({
const queryClient = useQueryClient();
const [deletingNoteId, setDeletingNoteId] = useState<number | null>(null);
const [searchQuery, setSearchQuery] = useState("");
const [mounted, setMounted] = useState(false);
const debouncedSearchQuery = useDebouncedValue(searchQuery, 300);
// Handle mounting for portal
useEffect(() => {
setMounted(true);
}, []);
// Handle escape key
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape" && open) {
onOpenChange(false);
}
};
document.addEventListener("keydown", handleEscape);
return () => document.removeEventListener("keydown", handleEscape);
}, [open, onOpenChange]);
// Lock body scroll when open
useEffect(() => {
if (open) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
return () => {
document.body.style.overflow = "";
};
}, [open]);
// Fetch all notes (when no search query)
const {
data: notesData,
@ -100,7 +123,6 @@ export function AllNotesSidebar({
search_space_id: noteSearchSpaceId,
note_id: noteId,
});
// Invalidate queries to refresh the list
queryClient.invalidateQueries({ queryKey: ["all-notes", searchSpaceId] });
queryClient.invalidateQueries({ queryKey: ["notes", searchSpaceId] });
queryClient.invalidateQueries({ queryKey: ["search-notes", searchSpaceId] });
@ -157,179 +179,215 @@ export function AllNotesSidebar({
});
}, [isSearchMode, searchData, notesData]);
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent side="left" className="w-80 p-0 flex flex-col border-0">
<SheetHeader className="mx-3 px-4 pt-4 pb-2 border-b space-y-2">
<SheetTitle>{t("all_notes") || "All Notes"}</SheetTitle>
<SheetDescription className="sr-only">
{t("all_notes_description") || "Browse and manage all your notes"}
</SheetDescription>
if (!mounted) return null;
{/* Search Input */}
<div className="relative">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
type="text"
placeholder={t("search_notes") || "Search notes..."}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 pr-8 h-9 border-0 focus-visible:ring-0 focus-visible:border-0 shadow-none"
/>
{searchQuery && (
<Button
variant="ghost"
size="icon"
className="absolute right-1 top-1/2 -translate-y-1/2 h-6 w-6"
onClick={handleClearSearch}
>
<X className="h-3.5 w-3.5" />
<span className="sr-only">{t("clear_search") || "Clear search"}</span>
</Button>
)}
</div>
</SheetHeader>
return createPortal(
<AnimatePresence>
{open && (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 z-50 bg-black/50"
onClick={() => onOpenChange(false)}
aria-hidden="true"
/>
<ScrollArea className="flex-1 min-h-0 overflow-hidden">
<div className="p-2">
{isLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
{/* Panel */}
<motion.div
initial={{ x: "-100%" }}
animate={{ x: 0 }}
exit={{ x: "-100%" }}
transition={{ type: "spring", damping: 25, stiffness: 300 }}
className="fixed inset-y-0 left-0 z-50 w-80 bg-background shadow-xl flex flex-col"
role="dialog"
aria-modal="true"
aria-label={t("all_notes") || "All Notes"}
>
{/* Header */}
<div className="flex-shrink-0 p-4 pb-3 space-y-3 border-b">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">{t("all_notes") || "All Notes"}</h2>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-full"
onClick={() => onOpenChange(false)}
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</Button>
</div>
) : error ? (
<div className="text-center py-8 text-sm text-destructive">
{t("error_loading_notes") || "Error loading notes"}
</div>
) : notes.length > 0 ? (
<div className="space-y-1">
{notes.map((note) => {
const isDeleting = deletingNoteId === note.id;
return (
<div
key={note.id}
className={cn(
"group flex items-center gap-2 rounded-md px-2 py-1.5 text-sm",
"hover:bg-accent hover:text-accent-foreground",
"transition-colors cursor-pointer",
isDeleting && "opacity-50 pointer-events-none"
)}
>
{/* Main clickable area for navigation */}
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => handleNoteClick(note.id, note.search_space_id)}
disabled={isDeleting}
className="flex items-center gap-2 flex-1 min-w-0 text-left"
>
<FileText className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="truncate">{note.title}</span>
</button>
</TooltipTrigger>
<TooltipContent side="right">
<div className="space-y-1">
<p>
{t("created") || "Created"}:{" "}
{format(new Date(note.created_at), "MMM d, yyyy 'at' h:mm a")}
</p>
{note.updated_at && (
<p>
{t("updated") || "Updated"}:{" "}
{format(new Date(note.updated_at), "MMM d, yyyy 'at' h:mm a")}
</p>
)}
</div>
</TooltipContent>
</Tooltip>
{/* Actions dropdown - separate from main click area */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className={cn(
"h-6 w-6 shrink-0",
"opacity-0 group-hover:opacity-100 focus:opacity-100",
"transition-opacity"
)}
disabled={isDeleting}
>
{isDeleting ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<MoreHorizontal className="h-3.5 w-3.5" />
)}
<span className="sr-only">{t("more_options") || "More options"}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem
onClick={() => handleDeleteNote(note.id, note.search_space_id)}
className="text-destructive focus:text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
<span>{t("delete") || "Delete"}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
})}
</div>
) : isSearchMode ? (
<div className="text-center py-8">
<Search className="h-12 w-12 mx-auto text-muted-foreground/50 mb-3" />
<p className="text-sm text-muted-foreground">
{t("no_results_found") || "No notes found"}
</p>
<p className="text-xs text-muted-foreground/70 mt-1">
{t("try_different_search") || "Try a different search term"}
</p>
</div>
) : (
<div className="text-center py-8">
<FileText className="h-12 w-12 mx-auto text-muted-foreground/50 mb-3" />
<p className="text-sm text-muted-foreground mb-4">
{t("no_notes") || "No notes yet"}
</p>
{onAddNote && (
{/* Search Input */}
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
type="text"
placeholder={t("search_notes") || "Search notes..."}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9 pr-8 h-9"
/>
{searchQuery && (
<Button
variant="outline"
size="sm"
onClick={() => {
onAddNote();
onOpenChange(false);
}}
variant="ghost"
size="icon"
className="absolute right-1 top-1/2 -translate-y-1/2 h-6 w-6"
onClick={handleClearSearch}
>
<Plus className="mr-2 h-4 w-4" />
{t("create_new_note") || "Create a note"}
<X className="h-3.5 w-3.5" />
<span className="sr-only">{t("clear_search") || "Clear search"}</span>
</Button>
)}
</div>
)}
</div>
</ScrollArea>
</div>
{/* Footer with Add Note button */}
{onAddNote && notes.length > 0 && (
<div className="mx-3 p-3">
<Button
onClick={() => {
onAddNote();
onOpenChange(false);
}}
className="w-full"
size="sm"
>
<Plus className="mr-2 h-4 w-4" />
{t("create_new_note") || "Create a new note"}
</Button>
</div>
)}
</SheetContent>
</Sheet>
{/* Scrollable Content */}
<div className="flex-1 overflow-y-auto overflow-x-hidden p-2">
{isLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : error ? (
<div className="text-center py-8 text-sm text-destructive">
{t("error_loading_notes") || "Error loading notes"}
</div>
) : notes.length > 0 ? (
<div className="space-y-1">
{notes.map((note) => {
const isDeleting = deletingNoteId === note.id;
return (
<div
key={note.id}
className={cn(
"group flex items-center gap-2 rounded-md px-2 py-1.5 text-sm",
"hover:bg-accent hover:text-accent-foreground",
"transition-colors cursor-pointer",
isDeleting && "opacity-50 pointer-events-none"
)}
>
{/* Main clickable area for navigation */}
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => handleNoteClick(note.id, note.search_space_id)}
disabled={isDeleting}
className="flex items-center gap-2 flex-1 min-w-0 text-left overflow-hidden"
>
<FileText className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="truncate">{note.title}</span>
</button>
</TooltipTrigger>
<TooltipContent side="bottom" align="start">
<div className="space-y-1">
<p>
{t("created") || "Created"}:{" "}
{format(new Date(note.created_at), "MMM d, yyyy 'at' h:mm a")}
</p>
{note.updated_at && (
<p>
{t("updated") || "Updated"}:{" "}
{format(new Date(note.updated_at), "MMM d, yyyy 'at' h:mm a")}
</p>
)}
</div>
</TooltipContent>
</Tooltip>
{/* Actions dropdown - separate from main click area */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className={cn(
"h-6 w-6 shrink-0",
"opacity-0 group-hover:opacity-100 focus:opacity-100",
"transition-opacity"
)}
disabled={isDeleting}
>
{isDeleting ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<MoreHorizontal className="h-3.5 w-3.5" />
)}
<span className="sr-only">{t("more_options") || "More options"}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem
onClick={() => handleDeleteNote(note.id, note.search_space_id)}
className="text-destructive focus:text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
<span>{t("delete") || "Delete"}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
})}
</div>
) : isSearchMode ? (
<div className="text-center py-8">
<Search className="h-12 w-12 mx-auto text-muted-foreground/50 mb-3" />
<p className="text-sm text-muted-foreground">
{t("no_results_found") || "No notes found"}
</p>
<p className="text-xs text-muted-foreground/70 mt-1">
{t("try_different_search") || "Try a different search term"}
</p>
</div>
) : (
<div className="text-center py-8">
<FileText className="h-12 w-12 mx-auto text-muted-foreground/50 mb-3" />
<p className="text-sm text-muted-foreground mb-4">
{t("no_notes") || "No notes yet"}
</p>
{onAddNote && (
<Button
variant="outline"
size="sm"
onClick={() => {
onAddNote();
onOpenChange(false);
}}
>
<Plus className="mr-2 h-4 w-4" />
{t("create_new_note") || "Create a note"}
</Button>
)}
</div>
)}
</div>
{/* Footer with Add Note button */}
{onAddNote && notes.length > 0 && (
<div className="flex-shrink-0 p-3 border-t">
<Button
onClick={() => {
onAddNote();
onOpenChange(false);
}}
className="w-full"
size="sm"
>
<Plus className="mr-2 h-4 w-4" />
{t("create_new_note") || "Create a new note"}
</Button>
</div>
)}
</motion.div>
</>
)}
</AnimatePresence>,
document.body
);
}