From f42deba3a3422cfdad2018c7586aaa3b45c49b2a Mon Sep 17 00:00:00 2001 From: bercivarga <65171545+bercivarga@users.noreply.github.com> Date: Sat, 9 Mar 2024 18:32:00 +0100 Subject: [PATCH] Add node creation flow and setup for future pages (#3) * add sidebar nav items, add command implementation for suggestions * add note creation logic and redirects * add all notes table, adjust schema and add helpers to fetch and delete note(s) --- app/{ => (platform)}/dashboard/page.tsx | 0 app/(platform)/layout.tsx | 14 + app/(platform)/navItems.ts | 31 ++ app/(platform)/notes/[noteId]/page.tsx | 25 ++ app/(platform)/notes/new/route.ts | 51 +++ app/(platform)/notes/page.tsx | 57 +++ app/(platform)/search-bar.tsx | 79 +++++ app/(platform)/side-nav.tsx | 28 ++ app/dashboard/layout.tsx | 21 -- components/ui/command.tsx | 158 +++++++++ components/ui/dialog.tsx | 125 +++++++ components/ui/table.tsx | 123 +++++++ helpers/notes/deleteNote.ts | 32 ++ helpers/notes/getAllNotes.ts | 35 ++ helpers/notes/getNote.ts | 31 ++ package-lock.json | 454 +++++++++++++++++++++++- package.json | 3 + prisma/schema.prisma | 3 +- 18 files changed, 1247 insertions(+), 23 deletions(-) rename app/{ => (platform)}/dashboard/page.tsx (100%) create mode 100644 app/(platform)/layout.tsx create mode 100644 app/(platform)/navItems.ts create mode 100644 app/(platform)/notes/[noteId]/page.tsx create mode 100644 app/(platform)/notes/new/route.ts create mode 100644 app/(platform)/notes/page.tsx create mode 100644 app/(platform)/search-bar.tsx create mode 100644 app/(platform)/side-nav.tsx delete mode 100644 app/dashboard/layout.tsx create mode 100644 components/ui/command.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/table.tsx create mode 100644 helpers/notes/deleteNote.ts create mode 100644 helpers/notes/getAllNotes.ts create mode 100644 helpers/notes/getNote.ts diff --git a/app/dashboard/page.tsx b/app/(platform)/dashboard/page.tsx similarity index 100% rename from app/dashboard/page.tsx rename to app/(platform)/dashboard/page.tsx diff --git a/app/(platform)/layout.tsx b/app/(platform)/layout.tsx new file mode 100644 index 0000000..1d90566 --- /dev/null +++ b/app/(platform)/layout.tsx @@ -0,0 +1,14 @@ +import SideNav from "./side-nav"; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( +
+ +
{children}
+
+ ); +} diff --git a/app/(platform)/navItems.ts b/app/(platform)/navItems.ts new file mode 100644 index 0000000..a7eb9a0 --- /dev/null +++ b/app/(platform)/navItems.ts @@ -0,0 +1,31 @@ +import { + CardStackIcon, + GridIcon, + HomeIcon, + Pencil2Icon, +} from "@radix-ui/react-icons"; + +const navItems = [ + { + label: "Dashboard", + href: "/dashboard", + icon: HomeIcon, + }, + { + label: "New note", + href: "/notes/new", + icon: Pencil2Icon, + }, + { + label: "All notes", + href: "/notes", + icon: CardStackIcon, + }, + { + label: "Map", + href: "/map", + icon: GridIcon, + }, +]; + +export default navItems; diff --git a/app/(platform)/notes/[noteId]/page.tsx b/app/(platform)/notes/[noteId]/page.tsx new file mode 100644 index 0000000..9d3de67 --- /dev/null +++ b/app/(platform)/notes/[noteId]/page.tsx @@ -0,0 +1,25 @@ +import { redirect } from "next/navigation"; + +import { getNote } from "@/helpers/notes/getNote"; + +export default async function EditNotePage({ + params, +}: { + params: { noteId: string }; +}) { + const { noteId } = params; + const note = await getNote(noteId); + + if (!note) { + redirect("/notes"); + } + + return ( +
+

Edit note

+
+        {JSON.stringify(note, null, 2)}
+      
+
+ ); +} diff --git a/app/(platform)/notes/new/route.ts b/app/(platform)/notes/new/route.ts new file mode 100644 index 0000000..536a994 --- /dev/null +++ b/app/(platform)/notes/new/route.ts @@ -0,0 +1,51 @@ +import { auth } from "@clerk/nextjs"; +import { redirect } from "next/navigation"; +import { NextResponse } from "next/server"; + +import { prisma } from "@/lib/db"; + +// Handles the creation of a new note, fills the note up with placeholder data, then redirects the user +export async function GET() { + const { userId } = auth(); + + if (!userId) { + return new NextResponse("Unauthorized", { status: 401 }); + } + + let newNoteId: string; + + try { + const dbUser = await prisma.user.findUnique({ + where: { clerkId: userId }, + }); + + if (!dbUser) { + return new NextResponse("User in own database not found", { + status: 404, + }); + } + + const newNote = await prisma.note.create({ + data: { + title: "New note", + content: "Start writing here...", + authorId: dbUser.id, + }, + }); + + const { id } = newNote; + + newNoteId = id; + } catch (error) { + return new NextResponse( + `Error creating note: ${(error as Error).message}`, + { + status: 500, + } + ); + } + + // This is necessary to be done here because Next's app directory routing throws an error internally + // that makes it impossible to use it in a try-catch block. Pretty annoying but this is the cleanest solution without a workaround. + redirect(`/notes/${newNoteId}`); +} diff --git a/app/(platform)/notes/page.tsx b/app/(platform)/notes/page.tsx new file mode 100644 index 0000000..a0cee3e --- /dev/null +++ b/app/(platform)/notes/page.tsx @@ -0,0 +1,57 @@ +import { CrossCircledIcon } from "@radix-ui/react-icons"; +import { Edit2Icon, Edit3Icon } from "lucide-react"; +import Link from "next/link"; + +import { Button } from "@/components/ui/button"; +import { + Table, + TableBody, + TableCaption, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { getAllNotes } from "@/helpers/notes/getAllNotes"; + +export default async function AllNotesPage() { + const notes = await getAllNotes(); + + return ( +
+ + A list of your recent notes. + + + Title + Snippet + Tags + Updated + + + + {notes?.map((note) => ( + + + {note.title} + + + + {note.content.slice(0, 50)} + + + + {note.tags.map((tag) => tag.name).join(", ")} + + + + {new Date(note.updatedAt).toLocaleString()} + + + + ))} + +
+
+ ); +} diff --git a/app/(platform)/search-bar.tsx b/app/(platform)/search-bar.tsx new file mode 100644 index 0000000..c445ed7 --- /dev/null +++ b/app/(platform)/search-bar.tsx @@ -0,0 +1,79 @@ +"use client"; + +import { MagnifyingGlassIcon } from "@radix-ui/react-icons"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { + KeyboardEvent as ReactKeyboardEvent, + useEffect, + useState, +} from "react"; + +import { Button } from "@/components/ui/button"; +import { + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; + +import navItems from "./navItems"; + +export default function SearchBar() { + const [showSearchCommand, setShowSearchCommand] = useState(false); + + const router = useRouter(); + + useEffect(() => { + const down = (e: KeyboardEvent) => { + if (e.key === "k" && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + setShowSearchCommand((open) => !open); + } + }; + document.addEventListener("keydown", down); + return () => document.removeEventListener("keydown", down); + }, []); + + return ( + <> + + + + + No results found. + + {navItems.map((item) => ( + + { + router.push(item.href); + setShowSearchCommand(false); + }} + > + + {item.label} + + + ))} + + + + + ); +} diff --git a/app/(platform)/side-nav.tsx b/app/(platform)/side-nav.tsx new file mode 100644 index 0000000..ad550b5 --- /dev/null +++ b/app/(platform)/side-nav.tsx @@ -0,0 +1,28 @@ +import { UserButton } from "@clerk/nextjs"; +import Link from "next/link"; + +import { Button } from "@/components/ui/button"; + +import navItems from "./navItems"; +import SearchBar from "./search-bar"; + +export default function SideNav() { + return ( + + ); +} diff --git a/app/dashboard/layout.tsx b/app/dashboard/layout.tsx deleted file mode 100644 index 17b749b..0000000 --- a/app/dashboard/layout.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { UserButton } from "@clerk/nextjs"; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( -
- - {children} -
- ); -} diff --git a/components/ui/command.tsx b/components/ui/command.tsx new file mode 100644 index 0000000..bac3973 --- /dev/null +++ b/components/ui/command.tsx @@ -0,0 +1,158 @@ +"use client"; + +/* eslint-disable react/prop-types */ +/* eslint-disable react/no-unknown-property */ + +import { type DialogProps } from "@radix-ui/react-dialog"; +import { MagnifyingGlassIcon } from "@radix-ui/react-icons"; +import { Command as CommandPrimitive } from "cmdk"; +import * as React from "react"; + +import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { cn } from "@/lib/utils"; + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Command.displayName = CommandPrimitive.displayName; + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + + + + {children} + + + + ); +}; + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)); + +CommandInput.displayName = CommandPrimitive.Input.displayName; + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandList.displayName = CommandPrimitive.List.displayName; + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)); + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName; + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandGroup.displayName = CommandPrimitive.Group.displayName; + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CommandSeparator.displayName = CommandPrimitive.Separator.displayName; + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandItem.displayName = CommandPrimitive.Item.displayName; + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ); +}; +CommandShortcut.displayName = "CommandShortcut"; + +export { + Command, + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, + CommandShortcut, +}; diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx new file mode 100644 index 0000000..5535fcf --- /dev/null +++ b/components/ui/dialog.tsx @@ -0,0 +1,125 @@ +"use client"; + +/* eslint-disable react/prop-types */ +/* eslint-disable react/no-unknown-property */ + +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { Cross2Icon } from "@radix-ui/react-icons"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = "DialogHeader"; + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = "DialogFooter"; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +}; diff --git a/components/ui/table.tsx b/components/ui/table.tsx new file mode 100644 index 0000000..a293e41 --- /dev/null +++ b/components/ui/table.tsx @@ -0,0 +1,123 @@ +/* eslint-disable react/prop-types */ +/* eslint-disable react/no-unknown-property */ + +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)); +Table.displayName = "Table"; + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableHeader.displayName = "TableHeader"; + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableBody.displayName = "TableBody"; + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)); +TableFooter.displayName = "TableFooter"; + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableRow.displayName = "TableRow"; + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)); +TableHead.displayName = "TableHead"; + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)); +TableCell.displayName = "TableCell"; + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +TableCaption.displayName = "TableCaption"; + +export { + Table, + TableBody, + TableCaption, + TableCell, + TableFooter, + TableHead, + TableHeader, + TableRow, +}; diff --git a/helpers/notes/deleteNote.ts b/helpers/notes/deleteNote.ts new file mode 100644 index 0000000..0ff83c6 --- /dev/null +++ b/helpers/notes/deleteNote.ts @@ -0,0 +1,32 @@ +"use server"; + +import { auth } from "@clerk/nextjs"; + +import { prisma } from "@/lib/db"; + +export async function deleteNote(noteId: string) { + const { userId } = auth(); + + if (!userId) { + return null; + } + + try { + const dbUser = await prisma.user.findUnique({ + where: { clerkId: userId }, + }); + + if (!dbUser) { + return null; + } + + const note = await prisma.note.update({ + where: { id: noteId, authorId: dbUser.id }, + data: { deleted: true }, + }); + + return note; + } catch (error) { + return null; + } +} diff --git a/helpers/notes/getAllNotes.ts b/helpers/notes/getAllNotes.ts new file mode 100644 index 0000000..8329774 --- /dev/null +++ b/helpers/notes/getAllNotes.ts @@ -0,0 +1,35 @@ +"use server"; + +import { auth } from "@clerk/nextjs"; +import { redirect } from "next/navigation"; + +import { prisma } from "@/lib/db"; + +export async function getAllNotes() { + const { userId } = auth(); + + if (!userId) { + redirect("/sign-in"); + } + + try { + const dbUser = await prisma.user.findUnique({ + where: { clerkId: userId }, + }); + + if (!dbUser) { + return null; + } + + const notes = await prisma.note.findMany({ + where: { authorId: dbUser.id, deleted: false }, + include: { tags: true }, + orderBy: { updatedAt: "desc" }, + take: 10, + }); + + return notes; + } catch (error) { + return null; + } +} diff --git a/helpers/notes/getNote.ts b/helpers/notes/getNote.ts new file mode 100644 index 0000000..b432dbd --- /dev/null +++ b/helpers/notes/getNote.ts @@ -0,0 +1,31 @@ +"use server"; + +import { auth } from "@clerk/nextjs"; + +import { prisma } from "@/lib/db"; + +export async function getNote(noteId: string) { + const { userId } = auth(); + + if (!userId) { + return null; + } + + try { + const dbUser = await prisma.user.findUnique({ + where: { clerkId: userId }, + }); + + if (!dbUser) { + return null; + } + + const note = await prisma.note.findUnique({ + where: { id: noteId, authorId: dbUser.id }, + }); + + return note; + } catch (error) { + return null; + } +} diff --git a/package-lock.json b/package-lock.json index 9772137..59c90d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,12 @@ "dependencies": { "@clerk/nextjs": "^4.29.9", "@prisma/client": "^5.10.2", + "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-slot": "^1.0.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", + "cmdk": "^1.0.0", "lucide-react": "^0.348.0", "next": "14.1.2", "react": "^18", @@ -1613,6 +1616,14 @@ "@prisma/debug": "5.10.2" } }, + "node_modules/@radix-ui/primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", @@ -1630,6 +1641,224 @@ } } }, + "node_modules/@radix-ui/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz", + "integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz", + "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-escape-keydown": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", + "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz", + "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-icons": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.0.tgz", + "integrity": "sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==", + "peerDependencies": { + "react": "^16.x || ^17.x || ^18.x" + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", + "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz", + "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", + "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", @@ -1648,6 +1877,76 @@ } } }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", + "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", + "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", + "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.12.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.1.tgz", @@ -2022,7 +2321,7 @@ "version": "18.2.20", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.20.tgz", "integrity": "sha512-HXN/biJY8nv20Cn9ZbCFq3liERd4CozVZmKbaiZ9KiKTrWqsP7eoGDO6OOGvJQwoVFuiXaiJ7nBBjiFFbRmQMQ==", - "dev": true, + "devOptional": true, "dependencies": { "@types/react": "*" } @@ -2647,6 +2946,17 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/aria-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz", + "integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -3229,6 +3539,19 @@ "node": ">=6" } }, + "node_modules/cmdk": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.0.tgz", + "integrity": "sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==", + "dependencies": { + "@radix-ui/react-dialog": "1.0.5", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3445,6 +3768,11 @@ "node": ">=6" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -4499,6 +4827,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/get-stream": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", @@ -4890,6 +5226,14 @@ "node": ">= 0.4" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/is-array-buffer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", @@ -6614,6 +6958,73 @@ "node": ">=0.10.0" } }, + "node_modules/react-remove-scroll": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", + "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.3", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.5.tgz", + "integrity": "sha512-3cqjOqg6s0XbOjWvmasmqHch+RLxIEk2r/70rzGXuz3iIGQsQheEQyqYCBb5EECoD01Vo2SIbDqW4paLeLTASw==", + "dependencies": { + "react-style-singleton": "^2.2.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", + "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", + "dependencies": { + "get-nonce": "^1.0.0", + "invariant": "^2.2.4", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -7969,6 +8380,47 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.1.tgz", + "integrity": "sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", + "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", diff --git a/package.json b/package.json index bc53f95..376a4d4 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,12 @@ "dependencies": { "@clerk/nextjs": "^4.29.9", "@prisma/client": "^5.10.2", + "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-slot": "^1.0.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", + "cmdk": "^1.0.0", "lucide-react": "^0.348.0", "next": "14.1.2", "react": "^18", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 319f4c8..c2a7e00 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -56,11 +56,12 @@ model Note { title String content String @db.Text author User @relation(fields: [authorId], references: [id]) - authorId String @unique + authorId String tags Tag[] // Many-to-many relationship: one note can have multiple tags sentimentAnalysis SentimentAnalysis? // One-to-one relationship: one note can have one sentiment analysis relatedNotes Note[] @relation("RelatedNotes") // Many-to-many relationship: a note can be related to multiple other notes relatedTo Note[] @relation("RelatedNotes") // Reverse relation for related notes + deleted Boolean @default(false) @@index([authorId], name: "author_index") }