feat: enhance sidebar and editor functionality

- Updated AllNotesSidebar to render via portal for improved stacking context.
- Refactored sidebar styles and behavior for better user experience.
- Modified AppSidebarProvider to limit recent notes to 5 and sort by updated date.
- Improved editor page to handle document state updates and query invalidation on note creation.
- Added loading messages to translations for better user feedback during operations.
This commit is contained in:
Anish Sarkar 2025-12-16 20:14:54 +05:30
parent 5da41d91c8
commit 82d8320928
8 changed files with 139 additions and 118 deletions

View file

@ -84,7 +84,7 @@ export function AppSidebarProvider({
queryFn: () =>
notesApiService.getNotes({
search_space_id: Number(searchSpaceId),
page_size: 10, // Get recent 10 notes
page_size: 5, // Get 5 notes (changed from 10)
}),
enabled: !!searchSpaceId,
});
@ -183,32 +183,40 @@ export function AppSidebarProvider({
// Transform notes to the format expected by NavNotes
const recentNotes = useMemo(() => {
return notesData?.items
? notesData.items.map((note) => ({
name: note.title,
url: `/dashboard/${note.search_space_id}/editor/${note.id}`,
icon: "FileText",
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,
});
refetchNotes();
} catch (error) {
console.error("Error deleting note:", error);
}
},
},
],
}))
: [];
if (!notesData?.items) return [];
// Sort notes by updated_at (most recent first), fallback to created_at if updated_at is null
const sortedNotes = [...notesData.items].sort((a, b) => {
const dateA = a.updated_at ? new Date(a.updated_at).getTime() : new Date(a.created_at).getTime();
const dateB = b.updated_at ? new Date(b.updated_at).getTime() : new Date(b.created_at).getTime();
return dateB - dateA; // Descending order (most recent first)
});
// Limit to 5 notes
return sortedNotes.slice(0, 5).map((note) => ({
name: note.title,
url: `/dashboard/${note.search_space_id}/editor/${note.id}`,
icon: "FileText",
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,
});
refetchNotes();
} catch (error) {
console.error("Error deleting note:", error);
}
},
},
],
}));
}, [notesData, refetchNotes]);
// Handle add note

View file

@ -31,6 +31,7 @@ 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<string, LucideIcon> = {
@ -284,42 +285,40 @@ export function AllNotesSidebar({
[isDeleting, router, onOpenChange, handleDeleteNote]
);
return (
<>
{/* Floating Sidebar */}
<section
ref={sidebarRef}
aria-label="All notes sidebar"
className={cn(
"fixed top-16 bottom-0 z-[60] w-80 bg-sidebar text-sidebar-foreground shadow-2xl",
"transition-all duration-300 ease-out",
!open && "pointer-events-none"
)}
style={{
// Position it to slide from the right edge of the main sidebar
left: `${sidebarLeft}px`,
transform: open ? `scaleX(1)` : `scaleX(0)`,
transformOrigin: "left",
opacity: open ? 1 : 0,
}}
onMouseEnter={() => {
// 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 {
const sidebarContent = (
<section
ref={sidebarRef}
aria-label="All notes sidebar"
className={cn(
"fixed top-16 bottom-0 z-[100] w-80 bg-sidebar text-sidebar-foreground shadow-xl",
"transition-all duration-300 ease-out",
!open && "pointer-events-none"
)}
style={{
// Position it to slide from the right edge of the main sidebar
left: `${sidebarLeft}px`,
transform: open ? `scaleX(1)` : `scaleX(0)`,
transformOrigin: "left",
opacity: open ? 1 : 0,
}}
onMouseEnter={() => {
// 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);
}
}}
>
<div className="flex h-full flex-col">
{/* Header */}
<div className="flex h-16 shrink-0 items-center justify-between px-4">
@ -348,13 +347,13 @@ export function AllNotesSidebar({
</SidebarMenuButton>
</SidebarMenuItem>
) : allNotes.length > 0 ? (
<SidebarMenu>
<SidebarMenu className="list-none">
{allNotes.map((note) => (
<NoteItemComponent key={note.id || note.name} note={note} />
))}
</SidebarMenu>
) : (
<SidebarMenuItem>
<SidebarMenuItem className="list-none">
{onAddNote ? (
<SidebarMenuButton
onClick={() => {
@ -398,7 +397,13 @@ export function AllNotesSidebar({
)}
</div>
</section>
</>
);
// Render sidebar via portal to avoid stacking context issues
if (typeof window === "undefined") {
return null;
}
return createPortal(sidebarContent, document.body);
}

View file

@ -170,7 +170,7 @@ export function NavNotes({ notes, onAddNote, defaultOpen = true, searchSpaceId }
</SidebarGroupLabel>
</CollapsibleTrigger>
<div className="absolute top-1.5 right-1 flex items-center gap-0.5 opacity-0 group-hover/header:opacity-100 transition-opacity">
{searchSpaceId && (
{searchSpaceId && notes.length > 0 && (
<button
type="button"
onMouseEnter={(e) => {

View file

@ -1,6 +1,6 @@
"use client";
import { ChevronRight, Mail } from "lucide-react";
import { Mail } from "lucide-react";
import { Progress } from "@/components/ui/progress";
import {
SidebarGroup,
@ -8,7 +8,6 @@ import {
SidebarGroupLabel,
useSidebar,
} from "@/components/ui/sidebar";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
interface PageUsageDisplayProps {
pagesUsed: number;
@ -22,16 +21,19 @@ export function PageUsageDisplay({ pagesUsed, pagesLimit }: PageUsageDisplayProp
return (
<SidebarGroup>
<Collapsible defaultOpen={false} className="group-data-[collapsible=icon]:hidden">
<CollapsibleTrigger asChild>
<SidebarGroupLabel className="cursor-pointer hover:bg-sidebar-accent rounded-md px-2 py-1.5 -mx-2 transition-colors flex items-center justify-between group">
<span>Page Usage</span>
<ChevronRight className="h-4 w-4 text-muted-foreground transition-transform duration-200 group-data-[state=open]:rotate-90" />
</SidebarGroupLabel>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarGroupContent>
<div className="space-y-2 px-2 py-2">
<SidebarGroupLabel className="group-data-[collapsible=icon]:hidden">
Page Usage
</SidebarGroupLabel>
<SidebarGroupContent>
<div className="space-y-2 px-2 py-2">
{isCollapsed ? (
// Show only a compact progress indicator when collapsed
<div className="flex justify-center">
<Progress value={usagePercentage} className="h-2 w-8" />
</div>
) : (
// Show full details when expanded
<>
<div className="flex justify-between items-center text-xs">
<span className="text-muted-foreground">
{pagesUsed.toLocaleString()} / {pagesLimit.toLocaleString()} pages
@ -52,18 +54,10 @@ export function PageUsageDisplay({ pagesUsed, pagesLimit }: PageUsageDisplayProp
to increase limits
</p>
</div>
</div>
</SidebarGroupContent>
</CollapsibleContent>
</Collapsible>
{isCollapsed && (
// Show only a compact progress indicator when sidebar is collapsed
<SidebarGroupContent>
<div className="flex justify-center px-2 py-2">
<Progress value={usagePercentage} className="h-2 w-8" />
</div>
</SidebarGroupContent>
)}
</>
)}
</div>
</SidebarGroupContent>
</SidebarGroup>
);
}