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",