diff --git a/src/app/ThemeProvider.tsx b/src/app/ThemeProvider.tsx new file mode 100644 index 0000000..e39e3a5 --- /dev/null +++ b/src/app/ThemeProvider.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { ThemeProvider } from "next-themes"; diff --git a/src/app/api/notes/route.ts b/src/app/api/notes/route.ts index 06cbee3..260b717 100644 --- a/src/app/api/notes/route.ts +++ b/src/app/api/notes/route.ts @@ -1,5 +1,9 @@ import prisma from "@/lib/db/prisma"; -import { createNoteSchema } from "@/lib/validation/note"; +import { + createNoteSchema, + deleteNoteSchema, + updateNoteSchema, +} from "@/lib/validation/note"; import { auth } from "@clerk/nextjs"; export async function POST(req: Request) { @@ -35,3 +39,77 @@ export async function POST(req: Request) { return Response.json({ error: "Internal server error" }, { status: 500 }); } } + +export async function PUT(req: Request) { + try { + const body = await req.json(); + + const parseResult = updateNoteSchema.safeParse(body); + + if (!parseResult.success) { + console.error(parseResult.error); + return Response.json({ error: "Invalid input" }, { status: 400 }); + } + + const { id, title, content } = parseResult.data; + + const note = await prisma.note.findUnique({ where: { id } }); + + if (!note) { + return Response.json({ error: "Note not found" }, { status: 404 }); + } + + const { userId } = auth(); + + if (!userId || userId !== note.userId) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + + const updatedNote = await prisma.note.update({ + where: { id }, + data: { + title, + content, + }, + }); + + return Response.json({ updatedNote }, { status: 200 }); + } catch (error) { + console.error(error); + return Response.json({ error: "Internal server error" }, { status: 500 }); + } +} + +export async function DELETE(req: Request) { + try { + const body = await req.json(); + + const parseResult = deleteNoteSchema.safeParse(body); + + if (!parseResult.success) { + console.error(parseResult.error); + return Response.json({ error: "Invalid input" }, { status: 400 }); + } + + const { id } = parseResult.data; + + const note = await prisma.note.findUnique({ where: { id } }); + + if (!note) { + return Response.json({ error: "Note not found" }, { status: 404 }); + } + + const { userId } = auth(); + + if (!userId || userId !== note.userId) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + + await prisma.note.delete({ where: { id } }); + + return Response.json({ message: "Note deleted" }, { status: 200 }); + } catch (error) { + console.error(error); + return Response.json({ error: "Internal server error" }, { status: 500 }); + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index bbc7aac..b534e29 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import { ClerkProvider } from "@clerk/nextjs"; import type { Metadata } from "next"; import { Inter } from "next/font/google"; +import { ThemeProvider } from "./ThemeProvider"; import "./globals.css"; const inter = Inter({ subsets: ["latin"] }); @@ -18,7 +19,9 @@ export default function RootLayout({ return ( - {children} + + {children} + ); diff --git a/src/app/notes/NavBar.tsx b/src/app/notes/NavBar.tsx index 4d3181f..16719ac 100644 --- a/src/app/notes/NavBar.tsx +++ b/src/app/notes/NavBar.tsx @@ -1,16 +1,21 @@ "use client"; import logo from "@/assets/logo.png"; -import AddNoteDialog from "@/components/AddNoteDialog"; +import AddEditNoteDialog from "@/components/AddEditNoteDialog"; +import ThemeToggleButton from "@/components/ThemeToggleButton"; import { Button } from "@/components/ui/button"; import { UserButton } from "@clerk/nextjs"; +import { dark } from "@clerk/themes"; import { Plus } from "lucide-react"; +import { useTheme } from "next-themes"; import Image from "next/image"; import Link from "next/link"; import { useState } from "react"; export default function NavBar() { - const [showAddNoteDialog, setShowAddNoteDialog] = useState(false); + const { theme } = useTheme(); + + const [showAddEditNoteDialog, setShowAddEditNoteDialog] = useState(false); return ( <> @@ -24,17 +29,22 @@ export default function NavBar() { - - + ); } diff --git a/src/components/AddNoteDialog.tsx b/src/components/AddEditNoteDialog.tsx similarity index 57% rename from src/components/AddNoteDialog.tsx rename to src/components/AddEditNoteDialog.tsx index 260cbd2..f3e134d 100644 --- a/src/components/AddNoteDialog.tsx +++ b/src/components/AddEditNoteDialog.tsx @@ -1,6 +1,8 @@ import { CreateNoteSchema, createNoteSchema } from "@/lib/validation/note"; import { zodResolver } from "@hookform/resolvers/zod"; +import { Note } from "@prisma/client"; import { useRouter } from "next/navigation"; +import { useState } from "react"; import { useForm } from "react-hook-form"; import { Dialog, @@ -21,35 +23,74 @@ import { Input } from "./ui/input"; import LoadingButton from "./ui/loading-button"; import { Textarea } from "./ui/textarea"; -interface AddNoteDialogProps { +interface AddEditNoteDialogProps { open: boolean; setOpen: (open: boolean) => void; + noteToEdit?: Note; } -export default function AddNoteDialog({ open, setOpen }: AddNoteDialogProps) { +export default function AddEditNoteDialog({ + open, + setOpen, + noteToEdit, +}: AddEditNoteDialogProps) { + const [deleteInProgress, setDeleteInProgress] = useState(false); + const router = useRouter(); const form = useForm({ resolver: zodResolver(createNoteSchema), defaultValues: { - title: "", - content: "", + title: noteToEdit?.title || "", + content: noteToEdit?.content || "", }, }); async function onSubmit(input: CreateNoteSchema) { + try { + if (noteToEdit) { + const response = await fetch("/api/notes", { + method: "PUT", + body: JSON.stringify({ + id: noteToEdit.id, + ...input, + }), + }); + if (!response.ok) throw Error("Status code: " + response.status); + } else { + const response = await fetch("/api/notes", { + method: "POST", + body: JSON.stringify(input), + }); + if (!response.ok) throw Error("Status code: " + response.status); + form.reset(); + } + router.refresh(); + setOpen(false); + } catch (error) { + console.error(error); + alert("Something went wrong. Please try again."); + } + } + + async function deleteNote() { + if (!noteToEdit) return; + setDeleteInProgress(true); try { const response = await fetch("/api/notes", { - method: "POST", - body: JSON.stringify(input), + method: "DELETE", + body: JSON.stringify({ + id: noteToEdit.id, + }), }); if (!response.ok) throw Error("Status code: " + response.status); - form.reset(); router.refresh(); setOpen(false); } catch (error) { console.error(error); alert("Something went wrong. Please try again."); + } finally { + setDeleteInProgress(false); } } @@ -57,7 +98,7 @@ export default function AddNoteDialog({ open, setOpen }: AddNoteDialogProps) { - Add Note + {noteToEdit ? "Edit Note" : "Add Note"}
@@ -87,10 +128,22 @@ export default function AddNoteDialog({ open, setOpen }: AddNoteDialogProps) { )} /> - + + {noteToEdit && ( + + Delete note + + )} Submit @@ -101,5 +154,3 @@ export default function AddNoteDialog({ open, setOpen }: AddNoteDialogProps) {
); } - - diff --git a/src/components/Note.tsx b/src/components/Note.tsx index 40b03ca..c8a839c 100644 --- a/src/components/Note.tsx +++ b/src/components/Note.tsx @@ -1,4 +1,8 @@ +"use client"; + import { Note as NoteModel } from "@prisma/client"; +import { useState } from "react"; +import AddEditNoteDialog from "./AddEditNoteDialog"; import { Card, CardContent, @@ -12,6 +16,8 @@ interface NoteProps { } export default function Note({ note }: NoteProps) { + const [showEditDialog, setShowEditDialog] = useState(false); + const wasUpdated = note.updatedAt > note.createdAt; const createdUpdatedAtTimestamp = ( @@ -19,17 +25,27 @@ export default function Note({ note }: NoteProps) { ).toDateString(); return ( - - - {note.title} - - {createdUpdatedAtTimestamp} - {wasUpdated && " (updated)"} - - - -

{note.content}

-
-
+ <> + setShowEditDialog(true)} + > + + {note.title} + + {createdUpdatedAtTimestamp} + {wasUpdated && " (updated)"} + + + +

{note.content}

+
+
+ + ); } diff --git a/src/components/ThemeToggleButton.tsx b/src/components/ThemeToggleButton.tsx new file mode 100644 index 0000000..3666a12 --- /dev/null +++ b/src/components/ThemeToggleButton.tsx @@ -0,0 +1,26 @@ +import { Moon, Sun } from "lucide-react"; +import { useTheme } from "next-themes"; +import { Button } from "./ui/button"; + +export default function ThemeToggleButton() { + const { theme, setTheme } = useTheme(); + + return ( + + ); +} diff --git a/src/lib/validation/note.ts b/src/lib/validation/note.ts index 482b7e4..9ecf820 100644 --- a/src/lib/validation/note.ts +++ b/src/lib/validation/note.ts @@ -6,3 +6,11 @@ export const createNoteSchema = z.object({ }); export type CreateNoteSchema = z.infer; + +export const updateNoteSchema = createNoteSchema.extend({ + id: z.string().min(1), +}); + +export const deleteNoteSchema = z.object({ + id: z.string().min(1), +});