2025-12-16 19:14:35 +05:30
|
|
|
"use client";
|
|
|
|
|
|
2025-12-17 00:09:43 -08:00
|
|
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
2025-12-19 21:40:40 +05:30
|
|
|
import { format } from "date-fns";
|
2025-12-17 00:09:43 -08:00
|
|
|
import { FileText, Loader2, MoreHorizontal, Plus, Search, Trash2, X } from "lucide-react";
|
2025-12-23 20:51:10 -08:00
|
|
|
import { AnimatePresence, motion } from "motion/react";
|
2025-12-27 19:41:44 +05:30
|
|
|
import { useParams, useRouter } from "next/navigation";
|
2025-12-16 19:14:35 +05:30
|
|
|
import { useTranslations } from "next-intl";
|
2025-12-23 20:51:10 -08:00
|
|
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
|
|
|
import { createPortal } from "react-dom";
|
2025-12-17 00:09:43 -08:00
|
|
|
import { Button } from "@/components/ui/button";
|
2025-12-16 19:14:35 +05:30
|
|
|
import {
|
|
|
|
|
DropdownMenu,
|
|
|
|
|
DropdownMenuContent,
|
|
|
|
|
DropdownMenuItem,
|
|
|
|
|
DropdownMenuTrigger,
|
|
|
|
|
} from "@/components/ui/dropdown-menu";
|
2025-12-17 00:09:43 -08:00
|
|
|
import { Input } from "@/components/ui/input";
|
2025-12-19 21:40:40 +05:30
|
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
2025-12-17 00:09:43 -08:00
|
|
|
import { useDebouncedValue } from "@/hooks/use-debounced-value";
|
|
|
|
|
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
|
|
|
|
import { notesApiService } from "@/lib/apis/notes-api.service";
|
2025-12-16 19:14:35 +05:30
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
|
|
|
|
|
|
interface AllNotesSidebarProps {
|
|
|
|
|
open: boolean;
|
|
|
|
|
onOpenChange: (open: boolean) => void;
|
|
|
|
|
searchSpaceId: string;
|
|
|
|
|
onAddNote?: () => void;
|
2025-12-28 23:25:22 +05:30
|
|
|
onCloseMobileSidebar?: () => void;
|
2025-12-16 19:14:35 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function AllNotesSidebar({
|
|
|
|
|
open,
|
|
|
|
|
onOpenChange,
|
|
|
|
|
searchSpaceId,
|
|
|
|
|
onAddNote,
|
2025-12-28 23:25:22 +05:30
|
|
|
onCloseMobileSidebar,
|
2025-12-16 19:14:35 +05:30
|
|
|
}: AllNotesSidebarProps) {
|
|
|
|
|
const t = useTranslations("sidebar");
|
|
|
|
|
const router = useRouter();
|
2025-12-27 19:41:44 +05:30
|
|
|
const params = useParams();
|
2025-12-17 00:09:43 -08:00
|
|
|
const queryClient = useQueryClient();
|
2025-12-27 19:41:44 +05:30
|
|
|
|
|
|
|
|
// Get the current note ID from URL to highlight the open note
|
|
|
|
|
const currentNoteId = params.note_id ? Number(params.note_id) : null;
|
2025-12-17 00:09:43 -08:00
|
|
|
const [deletingNoteId, setDeletingNoteId] = useState<number | null>(null);
|
|
|
|
|
const [searchQuery, setSearchQuery] = useState("");
|
2025-12-23 20:51:10 -08:00
|
|
|
const [mounted, setMounted] = useState(false);
|
2025-12-28 23:25:22 +05:30
|
|
|
const [openDropdownId, setOpenDropdownId] = useState<number | null>(null);
|
2025-12-17 00:09:43 -08:00
|
|
|
const debouncedSearchQuery = useDebouncedValue(searchQuery, 300);
|
2025-12-16 19:14:35 +05:30
|
|
|
|
2025-12-23 20:51:10 -08:00
|
|
|
// 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]);
|
|
|
|
|
|
2025-12-17 00:09:43 -08:00
|
|
|
// Fetch all notes (when no search query)
|
2025-12-16 19:14:35 +05:30
|
|
|
const {
|
|
|
|
|
data: notesData,
|
|
|
|
|
error: notesError,
|
|
|
|
|
isLoading: isLoadingNotes,
|
|
|
|
|
} = useQuery({
|
|
|
|
|
queryKey: ["all-notes", searchSpaceId],
|
|
|
|
|
queryFn: () =>
|
|
|
|
|
notesApiService.getNotes({
|
|
|
|
|
search_space_id: Number(searchSpaceId),
|
2025-12-17 00:09:43 -08:00
|
|
|
page_size: 1000,
|
|
|
|
|
}),
|
|
|
|
|
enabled: !!searchSpaceId && open && !debouncedSearchQuery,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Search notes (when there's a search query)
|
|
|
|
|
const {
|
|
|
|
|
data: searchData,
|
|
|
|
|
error: searchError,
|
|
|
|
|
isLoading: isSearching,
|
|
|
|
|
} = useQuery({
|
|
|
|
|
queryKey: ["search-notes", searchSpaceId, debouncedSearchQuery],
|
|
|
|
|
queryFn: () =>
|
|
|
|
|
documentsApiService.searchDocuments({
|
|
|
|
|
queryParams: {
|
|
|
|
|
search_space_id: Number(searchSpaceId),
|
|
|
|
|
document_types: ["NOTE"],
|
|
|
|
|
title: debouncedSearchQuery,
|
|
|
|
|
page_size: 100,
|
|
|
|
|
},
|
2025-12-16 19:14:35 +05:30
|
|
|
}),
|
2025-12-17 00:09:43 -08:00
|
|
|
enabled: !!searchSpaceId && open && !!debouncedSearchQuery,
|
2025-12-16 19:14:35 +05:30
|
|
|
});
|
|
|
|
|
|
2025-12-17 00:09:43 -08:00
|
|
|
// Handle note navigation
|
|
|
|
|
const handleNoteClick = useCallback(
|
|
|
|
|
(noteId: number, noteSearchSpaceId: number) => {
|
|
|
|
|
router.push(`/dashboard/${noteSearchSpaceId}/editor/${noteId}`);
|
|
|
|
|
onOpenChange(false);
|
2025-12-28 23:25:22 +05:30
|
|
|
// Also close the main sidebar on mobile
|
|
|
|
|
onCloseMobileSidebar?.();
|
2025-12-17 00:09:43 -08:00
|
|
|
},
|
2025-12-28 23:25:22 +05:30
|
|
|
[router, onOpenChange, onCloseMobileSidebar]
|
2025-12-17 00:09:43 -08:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Handle note deletion
|
2025-12-16 19:14:35 +05:30
|
|
|
const handleDeleteNote = useCallback(
|
2025-12-17 00:09:43 -08:00
|
|
|
async (noteId: number, noteSearchSpaceId: number) => {
|
|
|
|
|
setDeletingNoteId(noteId);
|
2025-12-16 19:14:35 +05:30
|
|
|
try {
|
2025-12-17 00:09:43 -08:00
|
|
|
await notesApiService.deleteNote({
|
|
|
|
|
search_space_id: noteSearchSpaceId,
|
|
|
|
|
note_id: noteId,
|
|
|
|
|
});
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ["all-notes", searchSpaceId] });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ["notes", searchSpaceId] });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ["search-notes", searchSpaceId] });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Error deleting note:", error);
|
2025-12-16 19:14:35 +05:30
|
|
|
} finally {
|
2025-12-17 00:09:43 -08:00
|
|
|
setDeletingNoteId(null);
|
2025-12-16 19:14:35 +05:30
|
|
|
}
|
|
|
|
|
},
|
2025-12-17 00:09:43 -08:00
|
|
|
[queryClient, searchSpaceId]
|
2025-12-16 19:14:35 +05:30
|
|
|
);
|
|
|
|
|
|
2025-12-17 00:09:43 -08:00
|
|
|
// Clear search
|
|
|
|
|
const handleClearSearch = useCallback(() => {
|
|
|
|
|
setSearchQuery("");
|
|
|
|
|
}, []);
|
2025-12-16 19:14:35 +05:30
|
|
|
|
2025-12-17 00:09:43 -08:00
|
|
|
// Determine which data to show
|
|
|
|
|
const isSearchMode = !!debouncedSearchQuery;
|
|
|
|
|
const isLoading = isSearchMode ? isSearching : isLoadingNotes;
|
|
|
|
|
const error = isSearchMode ? searchError : notesError;
|
2025-12-16 19:14:35 +05:30
|
|
|
|
2025-12-19 21:40:40 +05:30
|
|
|
// Transform and sort notes data - handle both regular notes and search results
|
2025-12-17 00:09:43 -08:00
|
|
|
const notes = useMemo(() => {
|
2025-12-19 15:38:39 -08:00
|
|
|
let notesList: {
|
|
|
|
|
id: number;
|
|
|
|
|
title: string;
|
|
|
|
|
search_space_id: number;
|
|
|
|
|
created_at: string;
|
|
|
|
|
updated_at?: string | null;
|
|
|
|
|
}[];
|
|
|
|
|
|
2025-12-17 00:09:43 -08:00
|
|
|
if (isSearchMode && searchData?.items) {
|
2025-12-19 21:40:40 +05:30
|
|
|
notesList = searchData.items.map((doc) => ({
|
2025-12-17 00:09:43 -08:00
|
|
|
id: doc.id,
|
|
|
|
|
title: doc.title,
|
|
|
|
|
search_space_id: doc.search_space_id,
|
2025-12-19 21:40:40 +05:30
|
|
|
created_at: doc.created_at,
|
|
|
|
|
updated_at: doc.updated_at,
|
2025-12-17 00:09:43 -08:00
|
|
|
}));
|
2025-12-19 21:40:40 +05:30
|
|
|
} else {
|
|
|
|
|
notesList = notesData?.items ?? [];
|
2025-12-17 00:09:43 -08:00
|
|
|
}
|
2025-12-19 21:40:40 +05:30
|
|
|
|
|
|
|
|
// Sort notes by updated_at (most recent first), fallback to created_at
|
|
|
|
|
return [...notesList].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)
|
|
|
|
|
});
|
2025-12-17 00:09:43 -08:00
|
|
|
}, [isSearchMode, searchData, notesData]);
|
2025-12-16 19:14:35 +05:30
|
|
|
|
2025-12-23 20:51:10 -08:00
|
|
|
if (!mounted) return null;
|
2025-12-16 19:14:35 +05:30
|
|
|
|
2025-12-23 20:51:10 -08:00
|
|
|
return createPortal(
|
|
|
|
|
<AnimatePresence>
|
|
|
|
|
{open && (
|
|
|
|
|
<>
|
|
|
|
|
{/* Backdrop */}
|
|
|
|
|
<motion.div
|
|
|
|
|
initial={{ opacity: 0 }}
|
|
|
|
|
animate={{ opacity: 1 }}
|
|
|
|
|
exit={{ opacity: 0 }}
|
|
|
|
|
transition={{ duration: 0.2 }}
|
2025-12-28 23:25:22 +05:30
|
|
|
className="fixed inset-0 z-[70] bg-black/50"
|
2025-12-23 20:51:10 -08:00
|
|
|
onClick={() => onOpenChange(false)}
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
/>
|
2025-12-16 19:14:35 +05:30
|
|
|
|
2025-12-23 20:51:10 -08:00
|
|
|
{/* Panel */}
|
|
|
|
|
<motion.div
|
|
|
|
|
initial={{ x: "-100%" }}
|
|
|
|
|
animate={{ x: 0 }}
|
|
|
|
|
exit={{ x: "-100%" }}
|
|
|
|
|
transition={{ type: "spring", damping: 25, stiffness: 300 }}
|
2025-12-28 23:25:22 +05:30
|
|
|
className="fixed inset-y-0 left-0 z-[70] w-80 bg-background shadow-xl flex flex-col pointer-events-auto isolate"
|
2025-12-23 20:51:10 -08:00
|
|
|
role="dialog"
|
|
|
|
|
aria-modal="true"
|
|
|
|
|
aria-label={t("all_notes") || "All Notes"}
|
|
|
|
|
>
|
|
|
|
|
{/* Header */}
|
2025-12-27 15:27:29 +05:30
|
|
|
<div className="flex-shrink-0 p-4 pb-3 space-y-3">
|
2025-12-23 20:51:10 -08:00
|
|
|
<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>
|
2025-12-17 00:09:43 -08:00
|
|
|
</div>
|
|
|
|
|
|
2025-12-23 20:51:10 -08:00
|
|
|
{/* 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 && (
|
2025-12-17 00:09:43 -08:00
|
|
|
<Button
|
2025-12-23 20:51:10 -08:00
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
className="absolute right-1 top-1/2 -translate-y-1/2 h-6 w-6"
|
|
|
|
|
onClick={handleClearSearch}
|
2025-12-17 00:09:43 -08:00
|
|
|
>
|
2025-12-23 20:51:10 -08:00
|
|
|
<X className="h-3.5 w-3.5" />
|
|
|
|
|
<span className="sr-only">{t("clear_search") || "Clear search"}</span>
|
2025-12-17 00:09:43 -08:00
|
|
|
</Button>
|
2025-12-16 21:27:31 +05:30
|
|
|
)}
|
2025-12-17 00:09:43 -08:00
|
|
|
</div>
|
2025-12-23 20:51:10 -08:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 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;
|
2025-12-27 19:41:44 +05:30
|
|
|
const isActive = currentNoteId === note.id;
|
2025-12-23 20:51:10 -08:00
|
|
|
|
|
|
|
|
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",
|
2025-12-28 01:20:39 +05:30
|
|
|
isActive && "bg-accent text-accent-foreground",
|
2025-12-23 20:51:10 -08:00
|
|
|
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 */}
|
2025-12-28 23:25:22 +05:30
|
|
|
<DropdownMenu
|
|
|
|
|
open={openDropdownId === note.id}
|
|
|
|
|
onOpenChange={(isOpen) => setOpenDropdownId(isOpen ? note.id : null)}
|
|
|
|
|
>
|
2025-12-23 20:51:10 -08:00
|
|
|
<DropdownMenuTrigger asChild>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
className={cn(
|
|
|
|
|
"h-6 w-6 shrink-0",
|
2025-12-28 23:25:22 +05:30
|
|
|
"md:opacity-0 md:group-hover:opacity-100 md:focus:opacity-100",
|
2025-12-23 20:51:10 -08:00
|
|
|
"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>
|
2025-12-28 23:25:22 +05:30
|
|
|
<DropdownMenuContent align="end" className="w-40 z-[80]">
|
2025-12-23 20:51:10 -08:00
|
|
|
<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>
|
2025-12-16 19:14:35 +05:30
|
|
|
|
2025-12-23 20:51:10 -08:00
|
|
|
{/* Footer with Add Note button */}
|
|
|
|
|
{onAddNote && notes.length > 0 && (
|
2025-12-27 15:27:29 +05:30
|
|
|
<div className="flex-shrink-0 p-3">
|
2025-12-23 20:51:10 -08:00
|
|
|
<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
|
2025-12-16 19:14:35 +05:30
|
|
|
);
|
|
|
|
|
}
|