From e007c25f2d8cc2452ed5884507d5d3d846d38294 Mon Sep 17 00:00:00 2001 From: bercivarga <65171545+bercivarga@users.noreply.github.com> Date: Mon, 11 Mar 2024 20:49:49 +0100 Subject: [PATCH] add trash page and adjust data fetching to fit trash requirements --- app/(platform)/layout.tsx | 6 +- app/(platform)/map/layout.tsx | 1 - app/(platform)/notes/notes-pagination.tsx | 17 +++- app/(platform)/notes/page.tsx | 27 +++++- app/(platform)/notes/trash/page.tsx | 87 +++++++++++++++++++ app/(platform)/notes/trash/restore-button.tsx | 26 ++++++ components/ui/tooltip.tsx | 33 +++++++ helpers/notes/deleteNote.ts | 22 +++++ helpers/notes/getAllNotes.ts | 31 +++++-- helpers/notes/restoreNote.ts | 37 ++++++++ package-lock.json | 62 ++++++++++++- package.json | 1 + 12 files changed, 330 insertions(+), 20 deletions(-) create mode 100644 app/(platform)/notes/trash/page.tsx create mode 100644 app/(platform)/notes/trash/restore-button.tsx create mode 100644 components/ui/tooltip.tsx create mode 100644 helpers/notes/restoreNote.ts diff --git a/app/(platform)/layout.tsx b/app/(platform)/layout.tsx index 87ec0b6..023a00b 100644 --- a/app/(platform)/layout.tsx +++ b/app/(platform)/layout.tsx @@ -6,9 +6,11 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( -
+
-
{children}
+
+ {children} +
); } diff --git a/app/(platform)/map/layout.tsx b/app/(platform)/map/layout.tsx index d646133..f366e23 100644 --- a/app/(platform)/map/layout.tsx +++ b/app/(platform)/map/layout.tsx @@ -2,7 +2,6 @@ import { Metadata } from "next"; export const metadata: Metadata = { title: "Map of your notes", - description: "Explore your notes and their relationships.", }; export default function MapLayout({ diff --git a/app/(platform)/notes/notes-pagination.tsx b/app/(platform)/notes/notes-pagination.tsx index 4e844b9..0b9a123 100644 --- a/app/(platform)/notes/notes-pagination.tsx +++ b/app/(platform)/notes/notes-pagination.tsx @@ -1,3 +1,5 @@ +"use client"; + import { Pagination, PaginationContent, @@ -11,11 +13,13 @@ import { type Props = { currentPage?: number; totalPages?: number; + type?: "notes" | "trash"; }; export default function NotesPagination({ currentPage = 1, totalPages = 1, + type = "notes", }: Props) { if (currentPage === 1 && totalPages === 1) { return null; @@ -24,16 +28,21 @@ export default function NotesPagination({ const isThereAPreviousPage = currentPage > 1; const isThereANextPage = totalPages > currentPage; + const path = type === "notes" ? "/notes" : "/notes/trash"; + + const prevPageHref = `${path}?page=${currentPage - 1}`; + const nextPageHref = `${path}?page=${currentPage + 1}`; + return ( {isThereAPreviousPage && ( <> - + - + {currentPage - 1} @@ -50,12 +59,12 @@ export default function NotesPagination({ {isThereANextPage && ( <> - + {currentPage + 1} - + )} diff --git a/app/(platform)/notes/page.tsx b/app/(platform)/notes/page.tsx index 147ece4..3f1a258 100644 --- a/app/(platform)/notes/page.tsx +++ b/app/(platform)/notes/page.tsx @@ -1,6 +1,8 @@ +import { PlusCircledIcon, TrashIcon } from "@radix-ui/react-icons"; import { Metadata } from "next"; import Link from "next/link"; +import { Button } from "@/components/ui/button"; import { Table, TableBody, @@ -34,15 +36,32 @@ export default async function AllNotesPage({ searchParams }: Props) { return (
- +
+
Notes
+
+ + + + + + +
+
+
- A list of your recent notes ({notes?.length ?? 0} of {count}) + A list of your recent notes (showing {notes?.length ?? 0} of {count}) Title Snippet - Tags + Tags Updated @@ -63,7 +82,7 @@ export default async function AllNotesPage({ searchParams }: Props) { : note.content} - + {note.tags.map((tag) => tag.name).join(", ")} diff --git a/app/(platform)/notes/trash/page.tsx b/app/(platform)/notes/trash/page.tsx new file mode 100644 index 0000000..7574d3a --- /dev/null +++ b/app/(platform)/notes/trash/page.tsx @@ -0,0 +1,87 @@ +import { Metadata } from "next"; + +import { + Table, + TableBody, + TableCaption, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { getAllNotes } from "@/helpers/notes/getAllNotes"; + +import NotesPagination from "../notes-pagination"; +import RestoreButton from "./restore-button"; + +type Props = { + params: {}; + searchParams: { page?: string }; +}; + +export const metadata: Metadata = { + title: "Trash bin", +}; + +export default async function TrashPage({ searchParams }: Props) { + const currentPage = searchParams.page ? parseInt(searchParams.page) : 1; + + const { count, notes, totalPages } = + (await getAllNotes({ + page: currentPage, + paginate: true, + results: "deleted", + })) ?? {}; + + return ( +
+
+
Trash bin
+
+
+ + A list of your discarded notes (showing {notes?.length ?? 0} of{" "} + {count}) + + + + Action + Title + Snippet + Updated + + + + {notes?.map((note) => ( + + + + + + {note.title.length > 20 + ? note.title.slice(0, 20) + "..." + : note.title} + + + {note.content.length > 50 + ? note.content.slice(0, 50) + "..." + : note.content} + + + {new Date(note.updatedAt).toLocaleString()} + + + ))} + +
+ +
+ ); +} + +export const dynamic = "force-dynamic"; +export const fetchCache = "force-no-store"; diff --git a/app/(platform)/notes/trash/restore-button.tsx b/app/(platform)/notes/trash/restore-button.tsx new file mode 100644 index 0000000..e3adda3 --- /dev/null +++ b/app/(platform)/notes/trash/restore-button.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { ResetIcon } from "@radix-ui/react-icons"; +import { useRouter } from "next/navigation"; + +import { Button } from "@/components/ui/button"; +import { restoreNote } from "@/helpers/notes/restoreNote"; + +type Props = { + noteId: string; +}; + +export default function RestoreButton({ noteId }: Props) { + const router = useRouter(); + + async function handleRestore() { + await restoreNote(noteId); + router.refresh(); + } + + return ( + + ); +} diff --git a/components/ui/tooltip.tsx b/components/ui/tooltip.tsx new file mode 100644 index 0000000..dbbba14 --- /dev/null +++ b/components/ui/tooltip.tsx @@ -0,0 +1,33 @@ +"use client"; + +/* eslint-disable react/prop-types */ +/* eslint-disable react/no-unknown-property */ + +import * as TooltipPrimitive from "@radix-ui/react-tooltip"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const TooltipProvider = TooltipPrimitive.Provider; + +const Tooltip = TooltipPrimitive.Root; + +const TooltipTrigger = TooltipPrimitive.Trigger; + +const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + +)); +TooltipContent.displayName = TooltipPrimitive.Content.displayName; + +export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }; diff --git a/helpers/notes/deleteNote.ts b/helpers/notes/deleteNote.ts index ff087a6..4bd6279 100644 --- a/helpers/notes/deleteNote.ts +++ b/helpers/notes/deleteNote.ts @@ -26,11 +26,33 @@ export async function deleteNote(noteId: string) { data: { deleted: true }, }); + // Need to disconnect relations after note is deleted + const notesWithThisNoteAsRelated = await prisma.note.findMany({ + where: { + OR: [ + { relatedNotes: { some: { id: noteId } } }, + { relatedTo: { some: { id: noteId } } }, + ], + }, + }); + + for (const note of notesWithThisNoteAsRelated) { + await prisma.note.update({ + where: { id: note.id }, + data: { + relatedNotes: { + disconnect: { id: noteId }, + }, + }, + }); + } + return note; } catch (error) { return null; } finally { revalidatePath("/notes"); + revalidatePath("/notes/trash"); revalidatePath(`/notes/${noteId}`, "page"); } } diff --git a/helpers/notes/getAllNotes.ts b/helpers/notes/getAllNotes.ts index 8f72399..c0ac0d8 100644 --- a/helpers/notes/getAllNotes.ts +++ b/helpers/notes/getAllNotes.ts @@ -4,15 +4,19 @@ import { redirect } from "next/navigation"; import { prisma } from "@/lib/db"; -const PAGE_SIZE = 12; +const PAGE_SIZE = 24; + +type GetAllNotesOptions = { + page?: number; + paginate?: boolean; + results?: "all" | "deleted" | "active"; +}; export async function getAllNotes({ page, paginate = false, -}: { - page?: number; - paginate?: boolean; -} = {}) { + results = "active", +}: GetAllNotesOptions = {}) { const { userId } = auth(); if (!userId) { @@ -28,12 +32,25 @@ export async function getAllNotes({ return null; } + let deletedOption = undefined; + + switch (results) { + case "all": + break; + case "deleted": + deletedOption = true; + break; + case "active": + deletedOption = false; + break; + } + const transaction = await prisma.$transaction([ prisma.note.count({ - where: { authorId: dbUser.id, deleted: false }, + where: { authorId: dbUser.id, deleted: deletedOption }, }), prisma.note.findMany({ - where: { authorId: dbUser.id, deleted: false }, + where: { authorId: dbUser.id, deleted: deletedOption }, include: { tags: true, relatedNotes: true, relatedTo: true }, orderBy: { updatedAt: "desc" }, take: paginate ? PAGE_SIZE : undefined, diff --git a/helpers/notes/restoreNote.ts b/helpers/notes/restoreNote.ts new file mode 100644 index 0000000..399a523 --- /dev/null +++ b/helpers/notes/restoreNote.ts @@ -0,0 +1,37 @@ +"use server"; + +import { auth } from "@clerk/nextjs"; +import { revalidatePath } from "next/cache"; + +import { prisma } from "@/lib/db"; + +export async function restoreNote(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: false }, + }); + + return note; + } catch (error) { + return null; + } finally { + revalidatePath("/notes"); + revalidatePath("/notes/trash"); + revalidatePath(`/notes/${noteId}`, "page"); + } +} diff --git a/package-lock.json b/package-lock.json index 4063e5d..f169ab1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "cf-showcase", + "name": "second-brain-showcase", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "cf-showcase", + "name": "second-brain-showcase", "version": "0.1.0", "dependencies": { "@clerk/nextjs": "^4.29.9", @@ -14,6 +14,7 @@ "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-menubar": "^1.0.4", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-tooltip": "^1.0.7", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "cmdk": "^1.0.0", @@ -2116,6 +2117,40 @@ } } }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz", + "integrity": "sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==", + "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-id": "1.0.1", + "@radix-ui/react-popper": "1.1.3", + "@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", + "@radix-ui/react-visually-hidden": "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-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", @@ -2222,6 +2257,29 @@ } } }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz", + "integrity": "sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==", + "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/rect": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.1.tgz", diff --git a/package.json b/package.json index a55c1ec..17dbc50 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-menubar": "^1.0.4", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-tooltip": "^1.0.7", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "cmdk": "^1.0.0",