├── .gitignore ├── packages ├── server │ ├── .gitignore │ ├── .env │ ├── prisma │ │ ├── dev.db │ │ ├── migrations │ │ │ ├── migration_lock.toml │ │ │ └── 20221027120701_note_entity │ │ │ │ └── migration.sql │ │ └── schema.prisma │ ├── tsconfig.json │ ├── package.json │ └── src │ │ ├── note.schema.ts │ │ ├── app.ts │ │ └── note.controller.ts └── client │ ├── src │ ├── vite-env.d.ts │ ├── utils │ │ └── trpc.ts │ ├── type.ts │ ├── main.tsx │ ├── index.css │ ├── components │ │ ├── note.modal.tsx │ │ ├── LoadingButton.tsx │ │ ├── Spinner.tsx │ │ └── notes │ │ │ ├── note.component.tsx │ │ │ ├── create.note.tsx │ │ │ └── update.note.tsx │ ├── App.tsx │ └── assets │ │ └── react.svg │ ├── postcss.config.cjs │ ├── vite.config.ts │ ├── tsconfig.node.json │ ├── .gitignore │ ├── index.html │ ├── tsconfig.json │ ├── tailwind.config.cjs │ ├── package.json │ ├── public │ └── vite.svg │ └── yarn.lock ├── package.json ├── README.md └── readMe.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /packages/server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /packages/server/.env: -------------------------------------------------------------------------------- 1 | DATABASE_URL="file:./dev.db" -------------------------------------------------------------------------------- /packages/client/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/server/prisma/dev.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wpcodevo/node-react-trpc-crud-app/HEAD/packages/server/prisma/dev.db -------------------------------------------------------------------------------- /packages/client/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /packages/server/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" -------------------------------------------------------------------------------- /packages/client/src/utils/trpc.ts: -------------------------------------------------------------------------------- 1 | import { createTRPCReact } from "@trpc/react-query"; 2 | import type { AppRouter } from "api-server"; 3 | export const trpc = createTRPCReact(); 4 | -------------------------------------------------------------------------------- /packages/client/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /packages/client/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 | -------------------------------------------------------------------------------- /packages/client/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/client/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 7 | 8 | 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trpc-node-react", 3 | "private": "true", 4 | "scripts": { 5 | "start": "concurrently \"wsrun --parallel start\"" 6 | }, 7 | "workspaces": [ 8 | "packages/*" 9 | ], 10 | "devDependencies": { 11 | "concurrently": "^7.5.0", 12 | "wsrun": "^5.2.4" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/client/src/index.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 | } -------------------------------------------------------------------------------- /packages/client/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "experimentalDecorators": true, 5 | "emitDecoratorMetadata": true, 6 | "module": "commonjs", 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "strictPropertyInitialization": false, 11 | "skipLibCheck": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/server/prisma/migrations/20221027120701_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 | -------------------------------------------------------------------------------- /packages/server/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 | -------------------------------------------------------------------------------- /packages/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 12 | 13 | 14 |
15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /packages/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /packages/client/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{vue,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 | -------------------------------------------------------------------------------- /packages/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-server", 3 | "version": "1.0.0", 4 | "main": "src/app.ts", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "ts-node-dev --respawn --transpile-only src/app.ts", 8 | "db:migrate": "npx prisma migrate dev --name note-entity --create-only && yarn prisma generate", 9 | "db:push": "npx prisma db push" 10 | }, 11 | "devDependencies": { 12 | "@types/cors": "^2.8.12", 13 | "@types/express": "^4.17.14", 14 | "@types/morgan": "^1.9.3", 15 | "@types/node": "^18.11.5", 16 | "morgan": "^1.10.0", 17 | "prisma": "^4.5.0", 18 | "ts-node-dev": "^2.0.0", 19 | "typescript": "^4.8.4" 20 | }, 21 | "dependencies": { 22 | "@prisma/client": "^4.5.0", 23 | "@trpc/server": "^10.0.0-proxy-beta.26", 24 | "cors": "^2.8.5", 25 | "express": "^4.18.2", 26 | "zod": "^3.19.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/client/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 | -------------------------------------------------------------------------------- /packages/server/src/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 | -------------------------------------------------------------------------------- /packages/client/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 | -------------------------------------------------------------------------------- /packages/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-crud-app", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "start": "vite --host localhost --port 3000", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@hookform/resolvers": "^2.9.10", 13 | "@tanstack/react-query": "^4.13.0", 14 | "@tanstack/react-query-devtools": "^4.13.0", 15 | "@trpc/client": "^10.0.0-proxy-beta.26", 16 | "@trpc/react-query": "^10.0.0-proxy-beta.26", 17 | "@trpc/server": "^10.0.0-proxy-beta.26", 18 | "api-server": "^1.0.5", 19 | "date-fns": "^2.29.3", 20 | "react": "^18.2.0", 21 | "react-dom": "^18.2.0", 22 | "react-hook-form": "^7.38.0", 23 | "react-toastify": "^9.0.8", 24 | "tailwind-merge": "^1.7.0", 25 | "zod": "^3.19.1" 26 | }, 27 | "devDependencies": { 28 | "@types/react": "^18.0.17", 29 | "@types/react-dom": "^18.0.6", 30 | "@vitejs/plugin-react": "^2.1.0", 31 | "autoprefixer": "^10.4.12", 32 | "postcss": "^8.4.18", 33 | "tailwindcss": "^3.2.1", 34 | "typescript": "^4.6.4", 35 | "vite": "^3.1.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build a FullStack tRPC CRUD App with TypeScript 2 | 3 | In this guide, you'll create a full-stack note application that follows the CRUD (Create, Read, Update, and Delete) architecture with tRPC and use Prisma ORM to store data in an SQLite database. We'll build the tRPC API with Node.js and the UI with React.js. 4 | 5 | ![Build a FullStack tRPC CRUD App with TypeScript](https://codevoweb.com/wp-content/uploads/2022/10/Build-a-FullStack-tRPC-CRUD-App-with-TypeScript.webp) 6 | 7 | ## Topics Covered 8 | 9 | - Run the tRPC App Locally 10 | - Setup the tRPC Project 11 | - Create the tRPC API with Node.js 12 | - Add Prisma ORM and SQLite 13 | - Create Zod Validation Schemas 14 | - Create the tRPC Procedures 15 | - Setup the Express and tRPC Servers 16 | - Create the tRPC Client with React.js 17 | - Setup the tRPC Client 18 | - Setup Tailwind CSS 19 | - Create Reusable Components 20 | - Create Note Component 21 | - Update Note Component 22 | - Delete Note Component 23 | - Get all Notes Component 24 | 25 | Read the entire article here: [https://codevoweb.com/build-a-fullstack-trpc-crud-app-with-typescript/](https://codevoweb.com/build-a-fullstack-trpc-crud-app-with-typescript/) 26 | 27 | -------------------------------------------------------------------------------- /readMe.md: -------------------------------------------------------------------------------- 1 | # Build a FullStack tRPC CRUD App with TypeScript 2 | 3 | In this guide, you'll create a full-stack note application that follows the CRUD (Create, Read, Update, and Delete) architecture with tRPC and use Prisma ORM to store data in an SQLite database. We'll build the tRPC API with Node.js and the UI with React.js. 4 | 5 | ![Build a FullStack tRPC CRUD App with TypeScript](https://codevoweb.com/wp-content/uploads/2022/10/Build-a-FullStack-tRPC-CRUD-App-with-TypeScript.webp) 6 | 7 | ## Topics Covered 8 | 9 | - Run the tRPC App Locally 10 | - Setup the tRPC Project 11 | - Create the tRPC API with Node.js 12 | - Add Prisma ORM and SQLite 13 | - Create Zod Validation Schemas 14 | - Create the tRPC Procedures 15 | - Setup the Express and tRPC Servers 16 | - Create the tRPC Client with React.js 17 | - Setup the tRPC Client 18 | - Setup Tailwind CSS 19 | - Create Reusable Components 20 | - Create Note Component 21 | - Update Note Component 22 | - Delete Note Component 23 | - Get all Notes Component 24 | 25 | Read the entire article here: [https://codevoweb.com/build-a-fullstack-trpc-crud-app-with-typescript/](https://codevoweb.com/build-a-fullstack-trpc-crud-app-with-typescript/) 26 | 27 | -------------------------------------------------------------------------------- /packages/client/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/client/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 | -------------------------------------------------------------------------------- /packages/server/src/app.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import morgan from "morgan"; 3 | import cors from "cors"; 4 | import { initTRPC } from "@trpc/server"; 5 | import * as trpcExpress from "@trpc/server/adapters/express"; 6 | import { PrismaClient } from "@prisma/client"; 7 | import { 8 | createNoteSchema, 9 | filterQuery, 10 | params, 11 | updateNoteSchema, 12 | } from "./note.schema"; 13 | import { 14 | createNoteController, 15 | deleteNoteController, 16 | findAllNotesController, 17 | findNoteController, 18 | updateNoteController, 19 | } from "./note.controller"; 20 | 21 | export const prisma = new PrismaClient(); 22 | const t = initTRPC.create(); 23 | 24 | const appRouter = t.router({ 25 | getHello: t.procedure.query((req) => { 26 | return { message: "Welcome to Full-Stack tRPC CRUD App" }; 27 | }), 28 | createNote: t.procedure 29 | .input(createNoteSchema) 30 | .mutation(({ input }) => createNoteController({ input })), 31 | updateNote: t.procedure 32 | .input(updateNoteSchema) 33 | .mutation(({ input }) => 34 | updateNoteController({ paramsInput: input.params, input: input.body }) 35 | ), 36 | deleteNote: t.procedure 37 | .input(params) 38 | .mutation(({ input }) => deleteNoteController({ paramsInput: input })), 39 | getNote: t.procedure 40 | .input(params) 41 | .query(({ input }) => findNoteController({ paramsInput: input })), 42 | getNotes: t.procedure 43 | .input(filterQuery) 44 | .query(({ input }) => findAllNotesController({ filterQuery: input })), 45 | }); 46 | 47 | export type AppRouter = typeof appRouter; 48 | 49 | const app = express(); 50 | if (process.env.NODE_ENV !== "production") app.use(morgan("dev")); 51 | 52 | app.use( 53 | cors({ 54 | origin: ["http://localhost:3000"], 55 | credentials: true, 56 | }) 57 | ); 58 | app.use( 59 | "/api/trpc", 60 | trpcExpress.createExpressMiddleware({ 61 | router: appRouter, 62 | }) 63 | ); 64 | 65 | const port = 8000; 66 | app.listen(port, () => { 67 | console.log(`🚀 Server listening on port ${port}`); 68 | }); 69 | -------------------------------------------------------------------------------- /packages/client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import "react-toastify/dist/ReactToastify.css"; 2 | import { useState } from "react"; 3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 4 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 5 | import { getFetch, httpBatchLink } from "@trpc/client"; 6 | import NoteModal from "./components/note.modal"; 7 | import CreateNote from "./components/notes/create.note"; 8 | import NoteItem from "./components/notes/note.component"; 9 | import { trpc } from "./utils/trpc"; 10 | import { ToastContainer, toast } from "react-toastify"; 11 | 12 | function AppContent() { 13 | const [openNoteModal, setOpenNoteModal] = useState(false); 14 | const { data: notes } = trpc.getNotes.useQuery( 15 | { limit: 10, page: 1 }, 16 | { 17 | staleTime: 5 * 1000, 18 | select: (data) => data.notes, 19 | onError(err) { 20 | toast(err.message, { 21 | type: "error", 22 | position: "top-right", 23 | }); 24 | }, 25 | } 26 | ); 27 | 28 | return ( 29 |
30 |
31 |
32 |
setOpenNoteModal(true)} 34 | 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" 35 | > 36 | 37 |
38 |

setOpenNoteModal(true)} 40 | className="text-lg font-medium text-ct-blue-600 mt-5 cursor-pointer" 41 | > 42 | Add new note 43 |

44 |
45 | {/* Note Items */} 46 | 47 | {notes?.map((note) => ( 48 | 49 | ))} 50 | 51 | {/* Create Note Modal */} 52 | 56 | 57 | 58 |
59 |
60 | ); 61 | } 62 | 63 | export default function App() { 64 | const [queryClient] = useState(() => new QueryClient()); 65 | const [trpcClient] = useState(() => 66 | trpc.createClient({ 67 | links: [ 68 | httpBatchLink({ 69 | url: "http://localhost:8000/api/trpc", 70 | fetch: async (input, init?) => { 71 | const fetch = getFetch(); 72 | return fetch(input, { 73 | ...init, 74 | credentials: "include", 75 | }); 76 | }, 77 | }), 78 | ], 79 | }) 80 | ); 81 | return ( 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /packages/server/src/note.controller.ts: -------------------------------------------------------------------------------- 1 | import { Prisma } from "@prisma/client"; 2 | import { TRPCError } from "@trpc/server"; 3 | import { prisma } from "./app"; 4 | import { 5 | CreateNoteInput, 6 | FilterQueryInput, 7 | ParamsInput, 8 | UpdateNoteInput, 9 | } from "./note.schema"; 10 | 11 | export const createNoteController = async ({ 12 | input, 13 | }: { 14 | input: CreateNoteInput; 15 | }) => { 16 | try { 17 | const note = await prisma.note.create({ 18 | data: { 19 | title: input.title, 20 | content: input.content, 21 | category: input.category, 22 | published: input.published, 23 | }, 24 | }); 25 | 26 | return { 27 | status: "success", 28 | data: { 29 | note, 30 | }, 31 | }; 32 | } catch (error) { 33 | if (error instanceof Prisma.PrismaClientKnownRequestError) { 34 | if (error.code === "P2002") { 35 | throw new TRPCError({ 36 | code: "CONFLICT", 37 | message: "Note with that title already exists", 38 | }); 39 | } 40 | } 41 | throw error; 42 | } 43 | }; 44 | 45 | export const updateNoteController = async ({ 46 | paramsInput, 47 | input, 48 | }: { 49 | paramsInput: ParamsInput; 50 | input: UpdateNoteInput["body"]; 51 | }) => { 52 | try { 53 | const updatedNote = await prisma.note.update({ 54 | where: { id: paramsInput.noteId }, 55 | data: input, 56 | }); 57 | 58 | return { 59 | status: "success", 60 | note: updatedNote, 61 | }; 62 | } catch (error) { 63 | if (error instanceof Prisma.PrismaClientKnownRequestError) { 64 | if (error.code === "P2025") { 65 | throw new TRPCError({ 66 | code: "CONFLICT", 67 | message: "Note with that title already exists", 68 | }); 69 | } 70 | } 71 | throw error; 72 | } 73 | }; 74 | 75 | export const findNoteController = async ({ 76 | paramsInput, 77 | }: { 78 | paramsInput: ParamsInput; 79 | }) => { 80 | try { 81 | const note = await prisma.note.findFirst({ 82 | where: { id: paramsInput.noteId }, 83 | }); 84 | 85 | if (!note) { 86 | throw new TRPCError({ 87 | code: "NOT_FOUND", 88 | message: "Note with that ID not found", 89 | }); 90 | } 91 | 92 | return { 93 | status: "success", 94 | note, 95 | }; 96 | } catch (error) { 97 | throw error; 98 | } 99 | }; 100 | 101 | export const findAllNotesController = async ({ 102 | filterQuery, 103 | }: { 104 | filterQuery: FilterQueryInput; 105 | }) => { 106 | try { 107 | const page = filterQuery.page || 1; 108 | const limit = filterQuery.limit || 10; 109 | const skip = (page - 1) * limit; 110 | 111 | const notes = await prisma.note.findMany({ skip, take: limit }); 112 | 113 | return { 114 | status: "success", 115 | results: notes.length, 116 | notes, 117 | }; 118 | } catch (error) { 119 | throw error; 120 | } 121 | }; 122 | 123 | export const deleteNoteController = async ({ 124 | paramsInput, 125 | }: { 126 | paramsInput: ParamsInput; 127 | }) => { 128 | try { 129 | await prisma.note.delete({ where: { id: paramsInput.noteId } }); 130 | 131 | return { 132 | status: "success", 133 | }; 134 | } catch (error) { 135 | if (error instanceof Prisma.PrismaClientKnownRequestError) { 136 | if (error.code === "P2025") { 137 | throw new TRPCError({ 138 | code: "NOT_FOUND", 139 | message: "Note with that ID not found", 140 | }); 141 | } 142 | } 143 | throw error; 144 | } 145 | }; 146 | -------------------------------------------------------------------------------- /packages/client/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/client/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(String(note.createdAt)), "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 | -------------------------------------------------------------------------------- /packages/client/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 |