├── .env ├── prisma ├── dev.db ├── migrations │ ├── migration_lock.toml │ └── 20221122110124_note_entity │ │ └── migration.sql └── schema.prisma ├── src ├── public │ ├── favicon.ico │ └── vercel.svg ├── type.ts ├── pages │ ├── api │ │ ├── trpc │ │ │ └── [trpc].ts │ │ └── hello.ts │ ├── _document.tsx │ ├── _app.tsx │ └── index.tsx ├── styles │ └── globals.css ├── components │ ├── note.modal.tsx │ ├── LoadingButton.tsx │ ├── Spinner.tsx │ └── notes │ │ ├── note.component.tsx │ │ ├── create.note.tsx │ │ └── update.note.tsx ├── server │ ├── note.schema.ts │ ├── app.router.ts │ └── note.controller.ts └── utils │ └── trpc.ts ├── next.config.js ├── postcss.config.js ├── .gitignore ├── tsconfig.json ├── tailwind.config.js ├── README.md ├── package.json └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | DATABASE_URL="file:./dev.db" -------------------------------------------------------------------------------- /prisma/dev.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcodevo/nextjs-trpc-crud-app/HEAD/prisma/dev.db -------------------------------------------------------------------------------- /src/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcodevo/nextjs-trpc-crud-app/HEAD/src/public/favicon.ico -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | } 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /src/type.ts: -------------------------------------------------------------------------------- 1 | export type INote = { 2 | id: string; 3 | title: string; 4 | content: string; 5 | category: string | null; 6 | published: boolean | null; 7 | createdAt: Date; 8 | updatedAt: Date; 9 | }; 10 | -------------------------------------------------------------------------------- /src/pages/api/trpc/[trpc].ts: -------------------------------------------------------------------------------- 1 | import * as trpcNext from "@trpc/server/adapters/next"; 2 | import { appRouter } from "~/server/app.router"; 3 | 4 | export default trpcNext.createNextApiHandler({ 5 | router: appRouter, 6 | }); 7 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap'); 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | html { 8 | font-family: 'Poppins', sans-serif; 9 | } 10 | 11 | body { 12 | background-color: #88abff; 13 | } -------------------------------------------------------------------------------- /src/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /prisma/migrations/20221122110124_note_entity/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "notes" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "title" TEXT NOT NULL, 5 | "content" TEXT NOT NULL, 6 | "category" TEXT, 7 | "published" BOOLEAN DEFAULT false, 8 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | "updatedAt" DATETIME NOT NULL 10 | ); 11 | 12 | -- CreateIndex 13 | CREATE UNIQUE INDEX "notes_title_key" ON "notes"("title"); 14 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "sqlite" 7 | url = env("DATABASE_URL") 8 | } 9 | 10 | model Note { 11 | id String @id @default(uuid()) 12 | title String @unique 13 | content String 14 | category String? 15 | published Boolean? @default(false) 16 | 17 | createdAt DateTime @default(now()) 18 | updatedAt DateTime @updatedAt 19 | 20 | @@map(name: "notes") 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import "react-toastify/dist/ReactToastify.css"; 3 | import type { AppProps } from "next/app"; 4 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 5 | import { ToastContainer } from "react-toastify"; 6 | import { trpc } from "~/utils/trpc"; 7 | 8 | function MyApp({ Component, pageProps }: AppProps) { 9 | return ( 10 | <> 11 | 12 | 13 | 14 | 15 | ); 16 | } 17 | 18 | export default trpc.withTRPC(MyApp); 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "strictNullChecks": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "noUncheckedIndexedAccess": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "incremental": true, 19 | "baseUrl": ".", 20 | "paths": { 21 | "~/*": ["src/*"] 22 | } 23 | }, 24 | "include": [ 25 | "next-env.d.ts", 26 | "**/*.ts", 27 | "**/*.tsx", 28 | "./*.js", 29 | "./src/**/*.js" 30 | ], 31 | "exclude": ["node_modules"] 32 | } -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './src/pages/**/*.{js,ts,jsx,tsx}', 5 | './src/components/**/*.{js,ts,jsx,tsx}', 6 | ], 7 | theme: { 8 | extend: { 9 | colors: { 10 | "ct-dark-600": "#222", 11 | "ct-dark-200": "#575757", 12 | "ct-dark-100": "#6d6d6d", 13 | "ct-blue-600": "#88abff", 14 | "ct-blue-700": "#6a93f8", 15 | "ct-yellow-600": "#f9d13e", 16 | }, 17 | fontFamily: { 18 | Poppins: ["Poppins, sans-serif"], 19 | }, 20 | container: { 21 | center: true, 22 | padding: "1rem", 23 | screens: { 24 | lg: "1125px", 25 | xl: "1125px", 26 | "2xl": "1125px", 27 | "3xl": "1500px", 28 | }, 29 | }, 30 | }, 31 | }, 32 | plugins: [], 33 | } 34 | -------------------------------------------------------------------------------- /src/components/note.modal.tsx: -------------------------------------------------------------------------------- 1 | import ReactDom from "react-dom"; 2 | import React, { FC } from "react"; 3 | 4 | type INoteModal = { 5 | openNoteModal: boolean; 6 | setOpenNoteModal: (open: boolean) => void; 7 | children: React.ReactNode; 8 | }; 9 | 10 | const NoteModal: FC = ({ 11 | openNoteModal, 12 | setOpenNoteModal, 13 | children, 14 | }) => { 15 | if (!openNoteModal) return null; 16 | return ReactDom.createPortal( 17 | <> 18 |
setOpenNoteModal(false)} 21 | >
22 |
23 | {children} 24 |
25 | , 26 | document.getElementById("note-modal") as HTMLElement 27 | ); 28 | }; 29 | 30 | export default NoteModal; 31 | -------------------------------------------------------------------------------- /src/components/LoadingButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { twMerge } from "tailwind-merge"; 3 | import Spinner from "./Spinner"; 4 | 5 | type LoadingButtonProps = { 6 | loading: boolean; 7 | btnColor?: string; 8 | textColor?: string; 9 | children: React.ReactNode; 10 | }; 11 | 12 | export const LoadingButton: React.FC = ({ 13 | textColor = "text-white", 14 | btnColor = "bg-ct-blue-700", 15 | children, 16 | loading = false, 17 | }) => { 18 | return ( 19 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/server/note.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const createNoteSchema = z.object({ 4 | title: z.string({ 5 | required_error: "Title is required", 6 | }), 7 | content: z.string({ 8 | required_error: "Content is required", 9 | }), 10 | category: z.string().optional(), 11 | published: z.boolean().optional(), 12 | }); 13 | 14 | export const params = z.object({ 15 | noteId: z.string(), 16 | }); 17 | 18 | export const updateNoteSchema = z.object({ 19 | params, 20 | body: z 21 | .object({ 22 | title: z.string(), 23 | content: z.string(), 24 | category: z.string(), 25 | published: z.boolean(), 26 | }) 27 | .partial(), 28 | }); 29 | 30 | export const filterQuery = z.object({ 31 | limit: z.number().default(1), 32 | page: z.number().default(10), 33 | }); 34 | 35 | export type ParamsInput = z.TypeOf; 36 | export type FilterQueryInput = z.TypeOf; 37 | export type CreateNoteInput = z.TypeOf; 38 | export type UpdateNoteInput = z.TypeOf; 39 | -------------------------------------------------------------------------------- /src/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build a Full Stack tRPC CRUD App with Next.js and Prisma ORM 2 | 3 | This article will teach you how to build a full-stack tRPC CRUD (Create, Read, Update, and Delete) app with Next.js. The tRPC API will be built on Next.js edge runtime and the tRPC client will be created with React.js. For data storage, we'll use Prisma ORM to query and mutate an SQLite database. 4 | 5 | ![Build a Full Stack tRPC CRUD App with Next.js and Prisma ORM](https://codevoweb.com/wp-content/uploads/2022/10/Build-a-Full-Stack-tRPC-CRUD-App-with-Next.js-and-Prisma-ORM.webp) 6 | 7 | ## Topics Covered 8 | 9 | - Run the Next.js tRPC CRUD App Locally 10 | - Setup Next.js as a Monorepo 11 | - Setup Prisma and Create the Database Model 12 | - Create the Next.js tRPC API 13 | - Create Zod Validation Schemas 14 | - Create the tRPC Procedures 15 | - Create the tRPC Server 16 | - Connect the tRPC Router to Next.js 17 | - Create the Next.js tRPC Client 18 | - Create Reusable React Components 19 | - React Query Create Record Mutation 20 | - React Query Update Record Mutation 21 | - React Query Delete Record Mutation 22 | - React Query Fetch All Records Query 23 | 24 | Read the entire article here: [https://codevoweb.com/build-a-fullstack-trpc-crud-app-with-nextjs/](https://codevoweb.com/build-a-fullstack-trpc-crud-app-with-nextjs/) 25 | 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "next dev", 5 | "build": "next build", 6 | "start": "next start", 7 | "db:migrate": "npx prisma migrate dev --name note-entity --create-only && npx prisma generate", 8 | "db:push": "npx prisma db push", 9 | "postinstall": "prisma db push && prisma generate" 10 | }, 11 | "dependencies": { 12 | "@hookform/resolvers": "^2.9.10", 13 | "@prisma/client": "^4.6.1", 14 | "@tanstack/react-query": "^4.13.0", 15 | "@tanstack/react-query-devtools": "^4.13.0", 16 | "@trpc/client": "^10.0.0-proxy-beta.26", 17 | "@trpc/next": "^10.0.0-proxy-beta.26", 18 | "@trpc/react-query": "^10.0.0-proxy-beta.26", 19 | "@trpc/server": "^10.0.0-proxy-beta.26", 20 | "cors": "^2.8.5", 21 | "date-fns": "^2.29.3", 22 | "next": "latest", 23 | "react": "18.2.0", 24 | "react-dom": "18.2.0", 25 | "react-hook-form": "^7.38.0", 26 | "react-toastify": "^9.0.8", 27 | "superjson": "^1.11.0", 28 | "tailwind-merge": "^1.7.0", 29 | "zod": "^3.19.1" 30 | }, 31 | "devDependencies": { 32 | "@types/cors": "^2.8.12", 33 | "@types/node": "18.11.3", 34 | "@types/react": "18.0.21", 35 | "@types/react-dom": "18.0.6", 36 | "autoprefixer": "^10.4.12", 37 | "postcss": "^8.4.18", 38 | "prisma": "^4.6.1", 39 | "tailwindcss": "^3.2.1", 40 | "typescript": "4.8.4" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/server/app.router.ts: -------------------------------------------------------------------------------- 1 | import { initTRPC } from "@trpc/server"; 2 | import superjson from "superjson"; 3 | import { 4 | createNoteController, 5 | deleteNoteController, 6 | findAllNotesController, 7 | findNoteController, 8 | updateNoteController, 9 | } from "./note.controller"; 10 | import { 11 | createNoteSchema, 12 | filterQuery, 13 | params, 14 | updateNoteSchema, 15 | } from "./note.schema"; 16 | 17 | const t = initTRPC.create({ 18 | transformer: superjson, 19 | }); 20 | 21 | export const appRouter = t.router({ 22 | getHello: t.procedure.query((req) => { 23 | return { message: "Welcome to Full-Stack tRPC CRUD App with Next.js" }; 24 | }), 25 | createNote: t.procedure 26 | .input(createNoteSchema) 27 | .mutation(({ input }) => createNoteController({ input })), 28 | updateNote: t.procedure 29 | .input(updateNoteSchema) 30 | .mutation(({ input }) => 31 | updateNoteController({ paramsInput: input.params, input: input.body }) 32 | ), 33 | deleteNote: t.procedure 34 | .input(params) 35 | .mutation(({ input }) => deleteNoteController({ paramsInput: input })), 36 | getNote: t.procedure 37 | .input(params) 38 | .query(({ input }) => findNoteController({ paramsInput: input })), 39 | getNotes: t.procedure 40 | .input(filterQuery) 41 | .query(({ input }) => findAllNotesController({ filterQuery: input })), 42 | }); 43 | 44 | export type AppRouter = typeof appRouter; 45 | -------------------------------------------------------------------------------- /src/utils/trpc.ts: -------------------------------------------------------------------------------- 1 | import { getFetch, httpBatchLink } from "@trpc/client"; 2 | import { createTRPCNext } from "@trpc/next"; 3 | import superjson from "superjson"; 4 | import { AppRouter } from "~/server/app.router"; 5 | 6 | export const trpc = createTRPCNext({ 7 | config({ ctx }) { 8 | const url = process.env.NEXT_PUBLIC_VERCEL_URL 9 | ? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}/api/trpc` 10 | : "http://localhost:3000/api/trpc/"; 11 | 12 | if (typeof window !== "undefined") { 13 | return { 14 | transformer: superjson, 15 | links: [ 16 | httpBatchLink({ 17 | url: "/api/trpc", 18 | }), 19 | ], 20 | }; 21 | } 22 | 23 | return { 24 | queryClientConfig: { 25 | defaultOptions: { 26 | queries: { 27 | staleTime: 5 * 1000, 28 | }, 29 | }, 30 | }, 31 | headers() { 32 | if (ctx?.req) { 33 | return { 34 | ...ctx.req.headers, 35 | "x-ssr": "1", 36 | }; 37 | } 38 | return {}; 39 | }, 40 | links: [ 41 | httpBatchLink({ 42 | url, 43 | fetch: async (input, init?) => { 44 | const fetch = getFetch(); 45 | return fetch(input, { 46 | ...init, 47 | credentials: "include", 48 | }); 49 | }, 50 | }), 51 | ], 52 | transformer: superjson, 53 | }; 54 | }, 55 | ssr: true, 56 | }); 57 | -------------------------------------------------------------------------------- /src/components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { twMerge } from 'tailwind-merge'; 3 | type SpinnerProps = { 4 | width?: number; 5 | height?: number; 6 | color?: string; 7 | bgColor?: string; 8 | }; 9 | const Spinner: React.FC = ({ 10 | width = 5, 11 | height = 5, 12 | color, 13 | bgColor, 14 | }) => { 15 | return ( 16 | 26 | 30 | 34 | 35 | ); 36 | }; 37 | 38 | export default Spinner; 39 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import { useState } from "react"; 3 | import { toast } from "react-toastify"; 4 | import NoteModal from "~/components/note.modal"; 5 | import CreateNote from "~/components/notes/create.note"; 6 | import NoteItem from "~/components/notes/note.component"; 7 | import { trpc } from "~/utils/trpc"; 8 | 9 | const Home: NextPage = () => { 10 | const [openNoteModal, setOpenNoteModal] = useState(false); 11 | const { data: notes } = trpc.getNotes.useQuery( 12 | { limit: 10, page: 1 }, 13 | { 14 | staleTime: 5 * 1000, 15 | select: (data) => data.notes, 16 | onError(err) { 17 | toast(err.message, { 18 | type: "error", 19 | position: "top-right", 20 | }); 21 | }, 22 | } 23 | ); 24 | return ( 25 |
26 |
27 |
28 |
setOpenNoteModal(true)} 30 | className="flex items-center justify-center h-20 w-20 border-2 border-dashed border-ct-blue-600 rounded-full text-ct-blue-600 text-5xl cursor-pointer" 31 | > 32 | 33 |
34 |

setOpenNoteModal(true)} 36 | className="text-lg font-medium text-ct-blue-600 mt-5 cursor-pointer" 37 | > 38 | Add new note 39 |

40 |
41 | {/* Note Items */} 42 | 43 | {notes?.map((note) => ( 44 | 45 | ))} 46 | 47 | {/* Create Note Modal */} 48 | 52 | 53 | 54 |
55 |
56 | ); 57 | }; 58 | 59 | export default Home; 60 | -------------------------------------------------------------------------------- /src/server/note.controller.ts: -------------------------------------------------------------------------------- 1 | import { Prisma, PrismaClient } from "@prisma/client"; 2 | import { TRPCError } from "@trpc/server"; 3 | import { 4 | CreateNoteInput, 5 | FilterQueryInput, 6 | ParamsInput, 7 | UpdateNoteInput, 8 | } from "./note.schema"; 9 | 10 | const prisma = new PrismaClient(); 11 | 12 | export const createNoteController = async ({ 13 | input, 14 | }: { 15 | input: CreateNoteInput; 16 | }) => { 17 | try { 18 | const note = await prisma.note.create({ 19 | data: { 20 | title: input.title, 21 | content: input.content, 22 | category: input.category, 23 | published: input.published, 24 | }, 25 | }); 26 | 27 | return { 28 | status: "success", 29 | data: { 30 | note, 31 | }, 32 | }; 33 | } catch (error) { 34 | if (error instanceof Prisma.PrismaClientKnownRequestError) { 35 | if (error.code === "P2002") { 36 | throw new TRPCError({ 37 | code: "CONFLICT", 38 | message: "Note with that title already exists", 39 | }); 40 | } 41 | } 42 | throw error; 43 | } 44 | }; 45 | 46 | export const updateNoteController = async ({ 47 | paramsInput, 48 | input, 49 | }: { 50 | paramsInput: ParamsInput; 51 | input: UpdateNoteInput["body"]; 52 | }) => { 53 | try { 54 | const updatedNote = await prisma.note.update({ 55 | where: { id: paramsInput.noteId }, 56 | data: input, 57 | }); 58 | 59 | return { 60 | status: "success", 61 | note: updatedNote, 62 | }; 63 | } catch (error) { 64 | if (error instanceof Prisma.PrismaClientKnownRequestError) { 65 | if (error.code === "P2025") { 66 | throw new TRPCError({ 67 | code: "CONFLICT", 68 | message: "Note with that title already exists", 69 | }); 70 | } 71 | } 72 | throw error; 73 | } 74 | }; 75 | 76 | export const findNoteController = async ({ 77 | paramsInput, 78 | }: { 79 | paramsInput: ParamsInput; 80 | }) => { 81 | try { 82 | const note = await prisma.note.findFirst({ 83 | where: { id: paramsInput.noteId }, 84 | }); 85 | 86 | if (!note) { 87 | throw new TRPCError({ 88 | code: "NOT_FOUND", 89 | message: "Note with that ID not found", 90 | }); 91 | } 92 | 93 | return { 94 | status: "success", 95 | note, 96 | }; 97 | } catch (error) { 98 | throw error; 99 | } 100 | }; 101 | 102 | export const findAllNotesController = async ({ 103 | filterQuery, 104 | }: { 105 | filterQuery: FilterQueryInput; 106 | }) => { 107 | try { 108 | const page = filterQuery.page || 1; 109 | const limit = filterQuery.limit || 10; 110 | const skip = (page - 1) * limit; 111 | 112 | const notes = await prisma.note.findMany({ skip, take: limit }); 113 | 114 | return { 115 | status: "success", 116 | results: notes.length, 117 | notes, 118 | }; 119 | } catch (error) { 120 | throw error; 121 | } 122 | }; 123 | 124 | export const deleteNoteController = async ({ 125 | paramsInput, 126 | }: { 127 | paramsInput: ParamsInput; 128 | }) => { 129 | try { 130 | await prisma.note.delete({ where: { id: paramsInput.noteId } }); 131 | 132 | return { 133 | status: "success", 134 | }; 135 | } catch (error) { 136 | if (error instanceof Prisma.PrismaClientKnownRequestError) { 137 | if (error.code === "P2025") { 138 | throw new TRPCError({ 139 | code: "NOT_FOUND", 140 | message: "Note with that ID not found", 141 | }); 142 | } 143 | } 144 | throw error; 145 | } 146 | }; 147 | -------------------------------------------------------------------------------- /src/components/notes/note.component.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState } from "react"; 2 | import { format, parseISO } from "date-fns"; 3 | import { twMerge } from "tailwind-merge"; 4 | import NoteModal from "../note.modal"; 5 | import UpdateNote from "./update.note"; 6 | import { toast } from "react-toastify"; 7 | import { INote } from "~/type"; 8 | import { trpc } from "~/utils/trpc"; 9 | import { useQueryClient } from "@tanstack/react-query"; 10 | 11 | type NoteItemProps = { 12 | note: INote; 13 | }; 14 | 15 | const NoteItem: FC = ({ note }) => { 16 | const [openSettings, setOpenSettings] = useState(false); 17 | const [openNoteModal, setOpenNoteModal] = useState(false); 18 | 19 | const queryClient = useQueryClient(); 20 | const { mutate: deleteNote } = trpc.deleteNote.useMutation({ 21 | onSuccess() { 22 | queryClient.invalidateQueries([["getNotes"], { limit: 10, page: 1 }]); 23 | setOpenNoteModal(false); 24 | toast("Note deleted successfully", { 25 | type: "success", 26 | position: "top-right", 27 | }); 28 | }, 29 | onError(error) { 30 | setOpenNoteModal(false); 31 | toast(error.message, { 32 | type: "error", 33 | position: "top-right", 34 | }); 35 | }, 36 | }); 37 | 38 | const onDeleteHandler = (noteId: string) => { 39 | if (window.confirm("Are you sure")) { 40 | deleteNote({ noteId: noteId }); 41 | } 42 | }; 43 | return ( 44 | <> 45 |
46 |
47 |

48 | {note.title.length > 20 49 | ? note.title.substring(0, 20) + "..." 50 | : note.title} 51 |

52 |

53 | {note.content.length > 210 54 | ? note.content.substring(0, 210) + "..." 55 | : note.content} 56 |

57 |
58 |
59 | 60 | {format(parseISO(note.createdAt.toISOString()), "PPP")} 61 | 62 |
setOpenSettings(!openSettings)} 64 | className="text-ct-dark-100 text-lg cursor-pointer" 65 | > 66 | 67 |
68 |
75 |
    76 |
  • { 78 | setOpenSettings(false); 79 | setOpenNoteModal(true); 80 | }} 81 | className="py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer" 82 | > 83 | Edit 84 |
  • 85 |
  • { 87 | setOpenSettings(false); 88 | onDeleteHandler(note.id); 89 | }} 90 | className="py-2 px-4 text-sm text-red-600 hover:bg-gray-100 cursor-pointer" 91 | > 92 | Delete 93 |
  • 94 |
95 |
96 |
97 |
98 | 102 | 103 | 104 | 105 | ); 106 | }; 107 | 108 | export default NoteItem; 109 | -------------------------------------------------------------------------------- /src/components/notes/create.note.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { SubmitHandler, useForm } from "react-hook-form"; 3 | import { twMerge } from "tailwind-merge"; 4 | import { object, string, TypeOf } from "zod"; 5 | import { zodResolver } from "@hookform/resolvers/zod"; 6 | import { LoadingButton } from "../LoadingButton"; 7 | import { toast } from "react-toastify"; 8 | import { trpc } from "~/utils/trpc"; 9 | import { useQueryClient } from "@tanstack/react-query"; 10 | 11 | type ICreateNoteProps = { 12 | setOpenNoteModal: (open: boolean) => void; 13 | }; 14 | 15 | const createNoteSchema = object({ 16 | title: string().min(1, "Title is required"), 17 | content: string().min(1, "Content is required"), 18 | }); 19 | 20 | type CreateNoteInput = TypeOf; 21 | 22 | const CreateNote: FC = ({ setOpenNoteModal }) => { 23 | const queryClient = useQueryClient(); 24 | const { isLoading, mutate: createNote } = trpc.createNote.useMutation({ 25 | onSuccess() { 26 | queryClient.invalidateQueries([["getNotes"], { limit: 10, page: 1 }]); 27 | setOpenNoteModal(false); 28 | toast("Note created successfully", { 29 | type: "success", 30 | position: "top-right", 31 | }); 32 | }, 33 | onError(error) { 34 | setOpenNoteModal(false); 35 | toast(error.message, { 36 | type: "error", 37 | position: "top-right", 38 | }); 39 | }, 40 | }); 41 | const methods = useForm({ 42 | resolver: zodResolver(createNoteSchema), 43 | }); 44 | 45 | const { 46 | register, 47 | handleSubmit, 48 | formState: { errors }, 49 | } = methods; 50 | 51 | const onSubmitHandler: SubmitHandler = async (data) => { 52 | createNote(data); 53 | }; 54 | return ( 55 |
56 |
57 |

Create Note

58 |
setOpenNoteModal(false)} 60 | className="text-2xl text-gray-400 hover:bg-gray-200 hover:text-gray-900 rounded-lg p-1.5 ml-auto inline-flex items-center cursor-pointer" 61 | > 62 | 63 |
64 |
65 |
66 |
67 | 70 | 77 |

83 | {errors["title"]?.message as string} 84 |

85 |
86 |
87 | 90 |