feat: add note management functionality with BlockNote support

- Introduced a new ENUM value 'NOTE' for document types in the database.
- Implemented backend routes for creating, listing, and deleting notes.
- Added a new frontend page for creating notes with a BlockNote editor.
- Updated sidebar to include recent notes and an option to add new notes.
- Enhanced API service for notes with validation and request/response schemas.
- Updated translations to support new note-related terms.
This commit is contained in:
Anish Sarkar 2025-12-16 12:28:30 +05:30
parent 8aedead33e
commit 8eceb7a5cb
13 changed files with 948 additions and 36 deletions

View file

@ -0,0 +1,155 @@
"use client";
import { AlertCircle, FileText, Loader2, Plus, X } from "lucide-react";
import { motion } from "motion/react";
import { useParams, useRouter } from "next/navigation";
import { useState } from "react";
import { toast } from "sonner";
import { BlockNoteEditor } from "@/components/DynamicBlockNoteEditor";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { notesApiService } from "@/lib/apis/notes-api.service";
export default function NewNotePage() {
const params = useParams();
const router = useRouter();
const searchSpaceId = Number(params.search_space_id);
const [title, setTitle] = useState("");
const [editorContent, setEditorContent] = useState<any>(null);
const [creating, setCreating] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleCreate = async () => {
if (!title.trim()) {
toast.error("Please enter a title for your note");
return;
}
setCreating(true);
setError(null);
try {
const note = await notesApiService.createNote({
search_space_id: searchSpaceId,
title: title.trim(),
blocknote_document: editorContent || undefined,
});
toast.success("Note created successfully!");
// Redirect to editor
router.push(`/dashboard/${searchSpaceId}/editor/${note.id}`);
} catch (error) {
console.error("Error creating note:", error);
const errorMessage =
error instanceof Error ? error.message : "Failed to create note. Please try again.";
setError(errorMessage);
toast.error(errorMessage);
} finally {
setCreating(false);
}
};
const handleCancel = () => {
router.back();
};
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex flex-col h-full w-full"
>
{/* Toolbar */}
<div className="sticky top-0 z-40 flex h-16 shrink-0 items-center gap-4 border-b bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 px-6">
<div className="flex items-center gap-3 flex-1 min-w-0">
<FileText className="h-5 w-5 text-muted-foreground shrink-0" />
<div className="flex flex-col min-w-0">
<h1 className="text-lg font-semibold truncate">New Note</h1>
<p className="text-xs text-muted-foreground">Create a new note</p>
</div>
</div>
<Separator orientation="vertical" className="h-6" />
<div className="flex items-center gap-2">
<Button variant="outline" onClick={handleCancel} disabled={creating} className="gap-2">
<X className="h-4 w-4" />
Cancel
</Button>
<Button onClick={handleCreate} disabled={creating || !title.trim()} className="gap-2">
{creating ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Creating...
</>
) : (
<>
<Plus className="h-4 w-4" />
Create Note
</>
)}
</Button>
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-hidden relative">
<div className="h-full w-full overflow-auto p-6">
<div className="max-w-4xl mx-auto space-y-6">
{error && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
>
<Card className="border-destructive/50">
<CardHeader>
<div className="flex items-center gap-2">
<AlertCircle className="h-5 w-5 text-destructive" />
<CardTitle className="text-destructive">Error</CardTitle>
</div>
<CardDescription>{error}</CardDescription>
</CardHeader>
</Card>
</motion.div>
)}
<Card>
<CardHeader>
<CardTitle>Note Details</CardTitle>
<CardDescription>Enter a title for your note</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="title">Title</Label>
<Input
id="title"
placeholder="Enter note title..."
value={title}
onChange={(e) => setTitle(e.target.value)}
disabled={creating}
className="text-lg"
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Content</CardTitle>
<CardDescription>Start writing your note (optional)</CardDescription>
</CardHeader>
<CardContent>
<div className="min-h-[400px]">
<BlockNoteEditor initialContent={undefined} onChange={setEditorContent} />
</div>
</CardContent>
</Card>
</div>
</div>
</div>
</motion.div>
);
}

View file

@ -2,6 +2,7 @@
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { Trash2 } from "lucide-react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useCallback, useEffect, useMemo, useState } from "react";
import { deleteChatMutationAtom } from "@/atoms/chats/chat-mutation.atoms";
@ -19,6 +20,7 @@ import {
} from "@/components/ui/dialog";
import { useUser } from "@/hooks";
import { useQuery } from "@tanstack/react-query";
import { notesApiService } from "@/lib/apis/notes-api.service";
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
import { cacheKeys } from "@/lib/query-client/cache-keys";
@ -48,6 +50,7 @@ export function AppSidebarProvider({
}: AppSidebarProviderProps) {
const t = useTranslations("dashboard");
const tCommon = useTranslations("common");
const router = useRouter();
const setChatsQueryParams = useSetAtom(globalChatsQueryParamsAtom);
const { data: chats, error: chatError, isLoading: isLoadingChats } = useAtomValue(chatsAtom);
const [{ isPending: isDeletingChat, mutateAsync: deleteChat, error: deleteError }] =
@ -70,6 +73,22 @@ export function AppSidebarProvider({
const { user } = useUser();
// Fetch notes
const {
data: notesData,
error: notesError,
isLoading: isLoadingNotes,
refetch: refetchNotes,
} = useQuery({
queryKey: ["notes", searchSpaceId],
queryFn: () =>
notesApiService.getNotes({
search_space_id: Number(searchSpaceId),
page_size: 10, // Get recent 10 notes
}),
enabled: !!searchSpaceId,
});
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [chatToDelete, setChatToDelete] = useState<{ id: number; name: string } | null>(null);
const [isClient, setIsClient] = useState(false);
@ -162,6 +181,41 @@ export function AppSidebarProvider({
// Use fallback chats if there's an error or no chats
const displayChats = recentChats.length > 0 ? recentChats : fallbackChats;
// 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);
}
},
},
],
}))
: [];
}, [notesData, refetchNotes]);
// Handle add note
const handleAddNote = useCallback(() => {
router.push(`/dashboard/${searchSpaceId}/notes/new`);
}, [router, searchSpaceId]);
// Memoized updated navSecondary
const updatedNavSecondary = useMemo(() => {
const updated = [...navSecondary];
@ -204,6 +258,7 @@ export function AppSidebarProvider({
navSecondary={navSecondary}
navMain={navMain}
RecentChats={[]}
RecentNotes={[]}
pageUsage={pageUsage}
/>
);
@ -216,6 +271,8 @@ export function AppSidebarProvider({
navSecondary={updatedNavSecondary}
navMain={navMain}
RecentChats={displayChats}
RecentNotes={recentNotes}
onAddNote={handleAddNote}
pageUsage={pageUsage}
/>

View file

@ -24,7 +24,6 @@ import {
UserPlus,
Users,
} from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useTheme } from "next-themes";
import { memo, useEffect, useMemo, useState } from "react";
@ -115,6 +114,7 @@ function UserAvatar({ email, size = 32 }: { email: string; size?: number }) {
}
import { NavMain } from "@/components/sidebar/nav-main";
import { NavNotes } from "@/components/sidebar/nav-notes";
import { NavProjects } from "@/components/sidebar/nav-projects";
import { NavSecondary } from "@/components/sidebar/nav-secondary";
import { PageUsageDisplay } from "@/components/sidebar/page-usage-display";
@ -209,6 +209,20 @@ const defaultData = {
id: 1003,
},
],
RecentNotes: [
{
name: "Meeting Notes",
url: "#",
icon: "FileText",
id: 2001,
},
{
name: "Project Ideas",
url: "#",
icon: "FileText",
id: 2002,
},
],
};
interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
@ -240,6 +254,18 @@ interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
onClick: () => void;
}[];
}[];
RecentNotes?: {
name: string;
url: string;
icon: string;
id?: number;
search_space_id?: number;
actions?: {
name: string;
icon: string;
onClick: () => void;
}[];
}[];
user?: {
name: string;
email: string;
@ -249,6 +275,7 @@ interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
pagesUsed: number;
pagesLimit: number;
};
onAddNote?: () => void;
}
// Memoized AppSidebar component for better performance
@ -257,7 +284,9 @@ export const AppSidebar = memo(function AppSidebar({
navMain = defaultData.navMain,
navSecondary = defaultData.navSecondary,
RecentChats = defaultData.RecentChats,
RecentNotes = defaultData.RecentNotes,
pageUsage,
onAddNote,
...props
}: AppSidebarProps) {
const router = useRouter();
@ -295,6 +324,16 @@ export const AppSidebar = memo(function AppSidebar({
);
}, [RecentChats]);
// Process RecentNotes to resolve icon names to components
const processedRecentNotes = useMemo(() => {
return (
RecentNotes?.map((item) => ({
...item,
icon: iconMap[item.icon] || FileText,
})) || []
);
}, [RecentNotes]);
// Get user display name from email
const userDisplayName = user?.email ? user.email.split("@")[0] : "User";
const userEmail = user?.email || (isLoadingUser ? "Loading..." : "Unknown");
@ -412,6 +451,10 @@ export const AppSidebar = memo(function AppSidebar({
<NavProjects chats={processedRecentChats} />
</div>
)}
<div className="space-y-2">
<NavNotes notes={processedRecentNotes} onAddNote={onAddNote} />
</div>
</SidebarContent>
<SidebarFooter>
{pageUsage && (

View file

@ -0,0 +1,214 @@
"use client";
import {
ChevronRight,
ExternalLink,
FileText,
type LucideIcon,
MoreHorizontal,
Plus,
RefreshCw,
Share,
Trash2,
} from "lucide-react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useCallback, useState } from "react";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
// Map of icon names to their components
const actionIconMap: Record<string, LucideIcon> = {
ExternalLink,
FileText,
Share,
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 NavNotesProps {
notes: NoteItem[];
onAddNote?: () => void;
defaultOpen?: boolean;
}
export function NavNotes({ notes, onAddNote, defaultOpen = true }: NavNotesProps) {
const t = useTranslations("sidebar");
const { isMobile } = useSidebar();
const router = useRouter();
const [isDeleting, setIsDeleting] = useState<number | null>(null);
const [isOpen, setIsOpen] = useState(defaultOpen);
// Handle note deletion with loading state
const handleDeleteNote = useCallback(async (noteId: number, deleteAction: () => void) => {
setIsDeleting(noteId);
try {
await deleteAction();
} finally {
setIsDeleting(null);
}
}, []);
// Enhanced note item component
const NoteItemComponent = useCallback(
({ note }: { note: NoteItem }) => {
const isDeletingNote = isDeleting === note.id;
return (
<SidebarMenuItem key={note.id ? `note-${note.id}` : `note-${note.name}`}>
<SidebarMenuButton
onClick={() => router.push(note.url)}
disabled={isDeletingNote}
className={`group/item relative ${isDeletingNote ? "opacity-50" : ""}`}
size="sm"
>
<note.icon className="h-4 w-4 shrink-0" />
<span className={`truncate ${isDeletingNote ? "opacity-50" : ""}`}>{note.name}</span>
</SidebarMenuButton>
{note.actions && note.actions.length > 0 && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction
showOnHover
className="opacity-0 group-hover/item:opacity-100 transition-opacity"
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-48"
side={isMobile ? "bottom" : "right"}
align={isMobile ? "end" : "start"}
>
{note.actions.map((action, actionIndex) => {
const ActionIcon = actionIconMap[action.icon] || FileText;
const isDeleteAction = action.name.toLowerCase().includes("delete");
return (
<DropdownMenuItem
key={`${action.name}-${actionIndex}`}
onClick={() => {
if (isDeleteAction) {
handleDeleteNote(note.id || 0, action.onClick);
} else {
action.onClick();
}
}}
disabled={isDeletingNote}
className={isDeleteAction ? "text-destructive" : ""}
>
<ActionIcon className="mr-2 h-4 w-4" />
<span>{isDeletingNote && isDeleteAction ? "Deleting..." : action.name}</span>
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
)}
</SidebarMenuItem>
);
},
[isDeleting, router, isMobile, handleDeleteNote]
);
return (
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<div className="flex items-center justify-between group/header">
<CollapsibleTrigger asChild>
<SidebarGroupLabel className="cursor-pointer rounded-md px-2 py-1.5 -mx-2 transition-colors flex items-center gap-1.5">
<ChevronRight
className={`h-3.5 w-3.5 text-muted-foreground transition-all duration-200 shrink-0 hover:text-sidebar-foreground ${
isOpen ? "rotate-90" : ""
}`}
/>
<span className="text-xs font-medium text-sidebar-foreground/70">
{t("notes") || "Notes"}
</span>
</SidebarGroupLabel>
</CollapsibleTrigger>
{onAddNote && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onAddNote();
}}
className="opacity-0 group-hover/header:opacity-100 transition-opacity p-1 hover:bg-sidebar-accent rounded-md -mr-2 shrink-0"
aria-label="Add note"
>
<Plus className="h-3.5 w-3.5 text-muted-foreground" />
</button>
)}
</div>
<CollapsibleContent>
<SidebarGroupContent>
<SidebarMenu>
{/* Note Items */}
{notes.length > 0 ? (
notes.map((note) => <NoteItemComponent key={note.id || note.name} note={note} />)
) : (
/* Empty state with create button */
<SidebarMenuItem>
{onAddNote ? (
<SidebarMenuButton
onClick={onAddNote}
className="text-muted-foreground hover:text-sidebar-foreground text-xs"
size="sm"
>
<Plus className="h-4 w-4" />
<span>{t("create_new_note") || "Create a new note"}</span>
</SidebarMenuButton>
) : (
<SidebarMenuButton disabled className="text-muted-foreground text-xs" size="sm">
<FileText className="h-4 w-4" />
<span>{t("no_notes") || "No notes yet"}</span>
</SidebarMenuButton>
)}
</SidebarMenuItem>
)}
</SidebarMenu>
</SidebarGroupContent>
</CollapsibleContent>
</Collapsible>
</SidebarGroup>
);
}

View file

@ -148,19 +148,6 @@ export function NavProjects({ chats }: { chats: ChatItem[] }) {
return (
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>{t("recent_chats")}</SidebarGroupLabel>
{/* Search Input */}
{showSearch && (
<div className="px-2 pb-2">
<SidebarInput
placeholder={t("search_chats")}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="h-8"
/>
</div>
)}
<SidebarMenu>
{/* Chat Items */}
{filteredChats.length > 0 ? (

View file

@ -1,6 +1,6 @@
"use client";
import { Mail } from "lucide-react";
import { ChevronRight, Mail } from "lucide-react";
import { Progress } from "@/components/ui/progress";
import {
SidebarGroup,
@ -8,6 +8,7 @@ import {
SidebarGroupLabel,
useSidebar,
} from "@/components/ui/sidebar";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
interface PageUsageDisplayProps {
pagesUsed: number;
@ -21,19 +22,16 @@ export function PageUsageDisplay({ pagesUsed, pagesLimit }: PageUsageDisplayProp
return (
<SidebarGroup>
<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
<>
<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">
<div className="flex justify-between items-center text-xs">
<span className="text-muted-foreground">
{pagesUsed.toLocaleString()} / {pagesLimit.toLocaleString()} pages
@ -54,10 +52,18 @@ export function PageUsageDisplay({ pagesUsed, pagesLimit }: PageUsageDisplayProp
to increase limits
</p>
</div>
</>
)}
</div>
</SidebarGroupContent>
</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>
)}
</SidebarGroup>
);
}
}

View file

@ -0,0 +1,148 @@
import { z } from "zod";
import { ValidationError } from "../error";
import { baseApiService } from "./base-api.service";
// Request/Response schemas
const createNoteRequest = z.object({
search_space_id: z.number(),
title: z.string().min(1),
blocknote_document: z.array(z.any()).optional(),
});
const createNoteResponse = z.object({
id: z.number(),
title: z.string(),
document_type: z.string(),
content: z.string(),
content_hash: z.string(),
unique_identifier_hash: z.string().nullable(),
document_metadata: z.record(z.any()).nullable(),
search_space_id: z.number(),
created_at: z.string(),
updated_at: z.string().nullable(),
});
const getNotesRequest = z.object({
search_space_id: z.number(),
skip: z.number().optional(),
page: z.number().optional(),
page_size: z.number().optional(),
});
const noteItem = z.object({
id: z.number(),
title: z.string(),
document_type: z.string(),
content: z.string(),
content_hash: z.string(),
unique_identifier_hash: z.string().nullable(),
document_metadata: z.record(z.any()).nullable(),
search_space_id: z.number(),
created_at: z.string(),
updated_at: z.string().nullable(),
});
const getNotesResponse = z.object({
items: z.array(noteItem),
total: z.number(),
page: z.number(),
page_size: z.number(),
has_more: z.boolean(),
});
const deleteNoteRequest = z.object({
search_space_id: z.number(),
note_id: z.number(),
});
const deleteNoteResponse = z.object({
message: z.string(),
note_id: z.number(),
});
// Type exports
export type CreateNoteRequest = z.infer<typeof createNoteRequest>;
export type CreateNoteResponse = z.infer<typeof createNoteResponse>;
export type GetNotesRequest = z.infer<typeof getNotesRequest>;
export type GetNotesResponse = z.infer<typeof getNotesResponse>;
export type NoteItem = z.infer<typeof noteItem>;
export type DeleteNoteRequest = z.infer<typeof deleteNoteRequest>;
export type DeleteNoteResponse = z.infer<typeof deleteNoteResponse>;
class NotesApiService {
/**
* Create a new note
*/
createNote = async (request: CreateNoteRequest) => {
const parsedRequest = createNoteRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
const { search_space_id, title, blocknote_document } = parsedRequest.data;
// Send both title and blocknote_document in request body
const body = {
title,
...(blocknote_document && { blocknote_document }),
};
return baseApiService.post(
`/api/v1/search-spaces/${search_space_id}/notes`,
createNoteResponse,
{ body }
);
};
/**
* Get list of notes
*/
getNotes = async (request: GetNotesRequest) => {
const parsedRequest = getNotesRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
const { search_space_id, skip, page, page_size } = parsedRequest.data;
// Build query params
const params = new URLSearchParams();
if (skip !== undefined) params.append("skip", String(skip));
if (page !== undefined) params.append("page", String(page));
if (page_size !== undefined) params.append("page_size", String(page_size));
return baseApiService.get(
`/api/v1/search-spaces/${search_space_id}/notes?${params.toString()}`,
getNotesResponse
);
};
/**
* Delete a note
*/
deleteNote = async (request: DeleteNoteRequest) => {
const parsedRequest = deleteNoteRequest.safeParse(request);
if (!parsedRequest.success) {
console.error("Invalid request:", parsedRequest.error);
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
throw new ValidationError(`Invalid request: ${errorMessage}`);
}
const { search_space_id, note_id } = parsedRequest.data;
return baseApiService.delete(
`/api/v1/search-spaces/${search_space_id}/notes/${note_id}`,
deleteNoteResponse
);
};
}
export const notesApiService = new NotesApiService();

View file

@ -642,7 +642,10 @@
"no_chats_found": "No chats found",
"no_recent_chats": "No recent chats",
"view_all_chats": "View All Chats",
"search_space": "Search Space"
"search_space": "Search Space",
"notes": "Notes",
"no_notes": "No notes yet",
"create_new_note": "Create a new note"
},
"errors": {
"something_went_wrong": "Something went wrong",