├── .eslintrc.json ├── app ├── globals.css ├── favicon.ico ├── page.tsx ├── products │ ├── layout.tsx │ ├── page.tsx │ ├── deleteProduct.tsx │ ├── addProduct.tsx │ └── updateProduct.tsx ├── layout.tsx └── api │ └── products │ ├── route.ts │ └── [id] │ └── route.ts ├── postcss.config.js ├── .vscode └── settings.json ├── prisma ├── migrations │ ├── migration_lock.toml │ └── 20230410180625_1 │ │ └── migration.sql └── schema.prisma ├── next.config.js ├── .env ├── .gitignore ├── tailwind.config.js ├── public ├── vercel.svg └── next.svg ├── tsconfig.json ├── package.json └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mfikricom/CRUD-Next.js-13-Prisma-PostgreSQL/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | const Home = () => { 2 | return
Home Page
; 3 | }; 4 | 5 | export default Home; 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } -------------------------------------------------------------------------------- /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 = "postgresql" -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | appDir: true, 5 | }, 6 | } 7 | 8 | module.exports = nextConfig 9 | -------------------------------------------------------------------------------- /app/products/layout.tsx: -------------------------------------------------------------------------------- 1 | export const metadata = { 2 | title: "Products", 3 | }; 4 | 5 | const ProductLayout = ({ children }: { children: React.ReactNode }) => { 6 | return
{children}
; 7 | }; 8 | 9 | export default ProductLayout; 10 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css' 2 | 3 | export const metadata = { 4 | title: 'Create Next App', 5 | description: 'Generated by create next app', 6 | } 7 | 8 | export default function RootLayout({ 9 | children, 10 | }: { 11 | children: React.ReactNode 12 | }) { 13 | return ( 14 | 15 | {children} 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Environment variables declared in this file are automatically made available to Prisma. 2 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema 3 | 4 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. 5 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings 6 | 7 | DATABASE_URL="postgresql://postgres:123456@localhost:5432/next_db?schema=public" -------------------------------------------------------------------------------- /.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 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./pages/**/*.{js,ts,jsx,tsx}", 5 | "./components/**/*.{js,ts,jsx,tsx}", 6 | "./app/**/*.{js,ts,jsx,tsx}", 7 | ], 8 | theme: { 9 | extend: { 10 | backgroundImage: { 11 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 12 | "gradient-conic": 13 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 14 | }, 15 | }, 16 | }, 17 | plugins: [require("daisyui")], 18 | }; 19 | -------------------------------------------------------------------------------- /app/api/products/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { PrismaClient } from "@prisma/client"; 3 | import type { Product } from "@prisma/client"; 4 | const prisma = new PrismaClient(); 5 | 6 | export const POST = async (request: Request) =>{ 7 | const body: Product = await request.json(); 8 | const product = await prisma.product.create({ 9 | data:{ 10 | title: body.title, 11 | price: body.price, 12 | brandId: body.brandId 13 | } 14 | }); 15 | return NextResponse.json(product, {status: 201}); 16 | } -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "postgresql" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model Brand { 14 | id Int @id @default(autoincrement()) 15 | name String 16 | products Product[] 17 | } 18 | 19 | model Product { 20 | id Int @id @default(autoincrement()) 21 | title String 22 | price Int 23 | createdAt DateTime @default(now()) 24 | updatedAt DateTime @updatedAt 25 | brand Brand @relation(fields: [brandId], references: [id]) 26 | brandId Int 27 | } 28 | -------------------------------------------------------------------------------- /prisma/migrations/20230410180625_1/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Brand" ( 3 | "id" SERIAL NOT NULL, 4 | "name" TEXT NOT NULL, 5 | 6 | CONSTRAINT "Brand_pkey" PRIMARY KEY ("id") 7 | ); 8 | 9 | -- CreateTable 10 | CREATE TABLE "Product" ( 11 | "id" SERIAL NOT NULL, 12 | "title" TEXT NOT NULL, 13 | "price" INTEGER NOT NULL, 14 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 15 | "updatedAt" TIMESTAMP(3) NOT NULL, 16 | "brandId" INTEGER NOT NULL, 17 | 18 | CONSTRAINT "Product_pkey" PRIMARY KEY ("id") 19 | ); 20 | 21 | -- AddForeignKey 22 | ALTER TABLE "Product" ADD CONSTRAINT "Product_brandId_fkey" FOREIGN KEY ("brandId") REFERENCES "Brand"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fullstack", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@prisma/client": "^4.12.0", 13 | "@types/node": "18.15.11", 14 | "@types/react": "18.0.34", 15 | "@types/react-dom": "18.0.11", 16 | "autoprefixer": "10.4.14", 17 | "axios": "^1.3.5", 18 | "daisyui": "^2.51.5", 19 | "eslint": "8.38.0", 20 | "eslint-config-next": "13.3.0", 21 | "next": "13.3.0", 22 | "postcss": "8.4.21", 23 | "react": "18.2.0", 24 | "react-dom": "18.2.0", 25 | "tailwindcss": "3.3.1", 26 | "typescript": "5.0.4" 27 | }, 28 | "devDependencies": { 29 | "prisma": "^4.12.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/api/products/[id]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { PrismaClient } from "@prisma/client"; 3 | import type { Product } from "@prisma/client"; 4 | const prisma = new PrismaClient(); 5 | 6 | export const PATCH = async (request: Request, {params}: {params: {id: string}}) =>{ 7 | const body: Product = await request.json(); 8 | const product = await prisma.product.update({ 9 | where:{ 10 | id: Number(params.id) 11 | }, 12 | data:{ 13 | title: body.title, 14 | price: body.price, 15 | brandId: body.brandId 16 | } 17 | }); 18 | return NextResponse.json(product, {status: 200}); 19 | } 20 | 21 | export const DELETE = async (request: Request, {params}: {params: {id: string}}) =>{ 22 | const product = await prisma.product.delete({ 23 | where:{ 24 | id: Number(params.id) 25 | } 26 | }); 27 | return NextResponse.json(product, {status: 200}); 28 | } -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 18 | 19 | [http://localhost:3000/api/hello](http://localhost:3000/api/hello) is an endpoint that uses [Route Handlers](https://beta.nextjs.org/docs/routing/route-handlers). This endpoint can be edited in `app/api/hello/route.ts`. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /app/products/page.tsx: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | import AddProduct from "./addProduct"; 3 | import DeleteProduct from "./deleteProduct"; 4 | import UpdateProduct from "./updateProduct"; 5 | const prisma = new PrismaClient(); 6 | 7 | const getProducts = async () => { 8 | const res = await prisma.product.findMany({ 9 | select: { 10 | id: true, 11 | title: true, 12 | price: true, 13 | brandId: true, 14 | brand: true, 15 | }, 16 | }); 17 | return res; 18 | }; 19 | 20 | const getBrands = async () => { 21 | const res = await prisma.brand.findMany(); 22 | return res; 23 | }; 24 | 25 | const Product = async () => { 26 | const [products, brands] = await Promise.all([getProducts(), getBrands()]); 27 | 28 | return ( 29 |
30 |
31 | 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | {products.map((product, index) => ( 46 | 47 | 48 | 49 | 50 | 51 | 55 | 56 | ))} 57 | 58 |
#Product NamePriceBrandActions
{index + 1}{product.title}{product.price}{product.brand.name} 52 | 53 | 54 |
59 |
60 | ); 61 | }; 62 | 63 | export default Product; 64 | -------------------------------------------------------------------------------- /app/products/deleteProduct.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useState } from "react"; 3 | import { useRouter } from "next/navigation"; 4 | import axios from "axios"; 5 | 6 | type Product = { 7 | id: number; 8 | title: string; 9 | price: number; 10 | brandId: number; 11 | }; 12 | 13 | const DeleteProduct = ({ product }: { product: Product }) => { 14 | const [isOpen, setIsOpen] = useState(false); 15 | const [isLoading, setIsLoading] = useState(false); 16 | 17 | const router = useRouter(); 18 | 19 | const handleDelete = async (productId: number) => { 20 | setIsLoading(true); 21 | await axios.delete(`/api/products/${productId}`); 22 | setIsLoading(false); 23 | router.refresh(); 24 | setIsOpen(false); 25 | }; 26 | 27 | const handleModal = () => { 28 | setIsOpen(!isOpen); 29 | }; 30 | 31 | return ( 32 |
33 | 36 | 37 |
38 |
39 |

40 | Are sure to delete {product.title}? 41 |

42 | 43 |
44 | 47 | {!isLoading ? ( 48 | 55 | ) : ( 56 | 59 | )} 60 |
61 |
62 |
63 |
64 | ); 65 | }; 66 | 67 | export default DeleteProduct; 68 | -------------------------------------------------------------------------------- /app/products/addProduct.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useState, SyntheticEvent } from "react"; 3 | import type { Brand } from "@prisma/client"; 4 | import { useRouter } from "next/navigation"; 5 | import axios from "axios"; 6 | 7 | const AddProduct = ({ brands }: { brands: Brand[] }) => { 8 | const [title, setTitle] = useState(""); 9 | const [price, setPrice] = useState(""); 10 | const [brand, setBrand] = useState(""); 11 | const [isOpen, setIsOpen] = useState(false); 12 | const [isLoading, setIsLoading] = useState(false); 13 | 14 | const router = useRouter(); 15 | 16 | const handleSubmit = async (e: SyntheticEvent) => { 17 | e.preventDefault(); 18 | setIsLoading(true); 19 | await axios.post("/api/products", { 20 | title: title, 21 | price: Number(price), 22 | brandId: Number(brand), 23 | }); 24 | setIsLoading(false); 25 | setTitle(""); 26 | setPrice(""); 27 | setBrand(""); 28 | router.refresh(); 29 | setIsOpen(false); 30 | }; 31 | 32 | const handleModal = () => { 33 | setIsOpen(!isOpen); 34 | }; 35 | 36 | return ( 37 |
38 | 41 | 42 |
43 |
44 |

Add New Product

45 |
46 |
47 | 48 | setTitle(e.target.value)} 52 | className="input input-bordered" 53 | placeholder="Product Name" 54 | /> 55 |
56 |
57 | 58 | setPrice(e.target.value)} 62 | className="input input-bordered" 63 | placeholder="Price" 64 | /> 65 |
66 |
67 | 68 | 82 |
83 |
84 | 87 | {!isLoading ? ( 88 | 91 | ) : ( 92 | 95 | )} 96 |
97 |
98 |
99 |
100 |
101 | ); 102 | }; 103 | 104 | export default AddProduct; 105 | -------------------------------------------------------------------------------- /app/products/updateProduct.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useState, SyntheticEvent } from "react"; 3 | import type { Brand } from "@prisma/client"; 4 | import { useRouter } from "next/navigation"; 5 | import axios from "axios"; 6 | 7 | type Product = { 8 | id: number; 9 | title: string; 10 | price: number; 11 | brandId: number; 12 | }; 13 | 14 | const UpdateProduct = ({ 15 | brands, 16 | product, 17 | }: { 18 | brands: Brand[]; 19 | product: Product; 20 | }) => { 21 | const [title, setTitle] = useState(product.title); 22 | const [price, setPrice] = useState(product.price); 23 | const [brand, setBrand] = useState(product.brandId); 24 | const [isOpen, setIsOpen] = useState(false); 25 | const [isLoading, setIsLoading] = useState(false); 26 | 27 | const router = useRouter(); 28 | 29 | const handleUpdate = async (e: SyntheticEvent) => { 30 | e.preventDefault(); 31 | setIsLoading(true); 32 | await axios.patch(`/api/products/${product.id}`, { 33 | title: title, 34 | price: Number(price), 35 | brandId: Number(brand), 36 | }); 37 | setIsLoading(false); 38 | router.refresh(); 39 | setIsOpen(false); 40 | }; 41 | 42 | const handleModal = () => { 43 | setIsOpen(!isOpen); 44 | }; 45 | 46 | return ( 47 |
48 | 51 | 52 |
53 |
54 |

Update {product.title}

55 |
56 |
57 | 58 | setTitle(e.target.value)} 62 | className="input input-bordered" 63 | placeholder="Product Name" 64 | /> 65 |
66 |
67 | 68 | setPrice(Number(e.target.value))} 72 | className="input input-bordered" 73 | placeholder="Price" 74 | /> 75 |
76 |
77 | 78 | 89 |
90 |
91 | 94 | {!isLoading ? ( 95 | 98 | ) : ( 99 | 102 | )} 103 |
104 |
105 |
106 |
107 |
108 | ); 109 | }; 110 | 111 | export default UpdateProduct; 112 | --------------------------------------------------------------------------------