Install nextjs npx create-next-app@latest https://nextjs.org/docs/getting-started/installation
Install the following
npm i mongoose-zod
https://github.com/andreww2012/mongoose-zod
A library which allows to author mongoose ("a MongoDB object modeling tool") schemas using zod ("a TypeScript-first schema declaration and validation library").
npm install react-daisyui
https://www.npmjs.com/package/react-daisyui
daisyUI components built with React, Typescript and TailwindCSS
npm install react-hot-toast
https://www.npmjs.com/package/react-hot-toast
Add beautiful notifications to your React app with react-hot-toast.
edit tailwind.config.ts Add daisyui to plugins
edit tailwind.config.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | //edit tailwind.config.ts import type { Config } from 'tailwindcss' const config: Config = { content: [ './pages/**/*.{js,ts,jsx,tsx,mdx}' , './components/**/*.{js,ts,jsx,tsx,mdx}' , './app/**/*.{js,ts,jsx,tsx,mdx}' , ], theme: { extend: { backgroundImage: { 'gradient-radial' : 'radial-gradient(var(--tw-gradient-stops))' , 'gradient-conic' : 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))' , }, }, }, } export default config |
1 | MONGODB_URI=mongodb: //127.0.0.1/nextjs14 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | //app\page.tsx import Image from 'next/image' //npm i mongoose zod daisyui react-hot-toast import { Toaster } from 'react-hot-toast' import CreateForm from './create-form' import dbConnect from '@/lib/db-connect' import ProductModel, { Product } from '@/lib/product-model' import DeleteForm from './delete-form' export default async function Home() { await dbConnect() const products = (await ProductModel.find({}).sort({ _id: -1, })) as Product[] return ( <div className= "mx-auto max-w-2xl lg:max-w-7xl" > <div className= "flex justify-between items-center" > <h1 className= "font-bold py-10 text-2xl" >Next.js 14 Server Actions MongoDB - List all data, Create Product and Delete </h1> <Toaster /> <CreateForm /> </div> <div className= "inline-block min-w-full align-middle" > <table className= "min-w-full divide-y divide-gray-200 table-fixed dark:divide-gray-700" > <thead className= "bg-gray-100 dark:bg-gray-700" > <tr> <th scope= "col" className= "p-4" > <div className= "flex items-center" > <input id= "checkbox-all" type= "checkbox" className= "w-4 h-4 text-blue-600 bg-gray-100 rounded border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" /> <label htmlFor= "checkbox-all" className= "sr-only" >checkbox</label> </div> </th> <th className= "py-3 text-left" >Image</th> <th className= "py-3 text-left" >Product Name</th> <th className= "py-3 text-left" >Price</th> <th className= "py-3 text-left" >Category</th> <th className= "py-3 text-left" >Actions</th> </tr> </thead> <tbody className= "bg-white divide-y divide-gray-200 dark:bg-gray-800 dark:divide-gray-700" > {products.length === 0 ? ( <tr> <td colSpan={5}>No product found</td> </tr> ) : ( products.map((product: Product) => ( <tr key={product._id} className= "hover:bg-gray-100 dark:hover:bg-gray-700" > <td className= "p-4 w-4" > <div className= "flex items-center" > <input id= "checkbox-table-1" type= "checkbox" className= "w-4 h-4 text-blue-600 bg-gray-100 rounded border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" /> <label htmlFor= "checkbox-table-1" className= "sr-only" >checkbox</label> </div> </td> <td> <Image src={product.image} alt={product.name} width={80} height={80} className= "rounded-lg" /> </td> <td>{product.name}</td> <td>${product.price}</td> <td>{product.category}</td> <td> <DeleteForm _id={product._id.toString()} name={product.name} /> </td> </tr> )) )} </tbody> </table> </div> </div> ) } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | //app\layout.tsx import type { Metadata } from 'next' import { Inter } from 'next/font/google' import './globals.css' const inter = Inter({ subsets: [ 'latin' ] }) export const metadata: Metadata = { title: 'Next.js 14 Server Actions MongoDB - List all data, Create and Delete' , description: 'Generated by create next app' , } export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang= "en" > <body className={inter.className}>{children}</body> </html> ) } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | // 'use client' import { useFormState, useFormStatus } from 'react-dom' import { createProduct } from '@/lib/actions' import { useEffect, useRef } from 'react' import toast from 'react-hot-toast' export default function CreateForm() { const [state, formAction] = useFormState(createProduct, { message: '' , }) const { pending } = useFormStatus() const ref = useRef<HTMLFormElement>(null) useEffect(() => { if (state.message.indexOf( 'Created product' ) === 0) { ; (document.getElementById( 'my_modal' ) as any)!.close() ref.current?.reset() toast(state.message) } else if (state.message) { toast(state.message) } }, [state.message]) return ( <div> <button className= "btn btn-primary" onClick={() => (document.getElementById( 'my_modal' )! as any).showModal() } > Create Product </button> <dialog id= "my_modal" className= "modal" > <div className= "modal-box" > <h2 className= "tex-2xl font-bold pm-4" >Create Product</h2> <form ref={ref} action={formAction}> <div className= "form-control w-full max-w-xs py-4" > <label htmlFor= "name" >Name</label> <input type= "text" id= "name" name= "name" className= "input input-bordered w-full max-w-xs" required /> </div> <div className= "form-control w-full max-w-xs py-4" > <label htmlFor= "image" >Image</label> <input type= "text" id= "image" name= "image" className= "input input-bordered w-full max-w-xs" required defaultValue= "/images/1.jpg" /> </div> <div className= "form-control w-full max-w-xs py-4" > <label htmlFor= "price" >Price</label> <input type= "number" id= "price" name= "price" className= "input input-bordered w-full max-w-xs" required defaultValue= "1" /> </div> <div className= "form-control w-full max-w-xs py-4" > <label htmlFor= "name" >Category</label> <input type= "text" id= "category" name= "category" className= "input input-bordered w-full max-w-xs" required /> </div> <button className= "btn btn-primary mr-3" type= "submit" disabled={pending} > Create </button> <button type= "button" className= "btn btn-ghost" onClick={() => (document.getElementById( 'my_modal' ) as any).close() } > Back </button> </form> </div> </dialog> </div> ) } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | //app\delete-form.tsx 'use client' import { deleteProduct } from '@/lib/actions' import { useFormStatus } from 'react-dom' import toast from 'react-hot-toast' export default function DeleteForm({ _id, name, }: { _id: string name: string }) { const { pending } = useFormStatus() return ( <form action={async (formData) => { const res = await deleteProduct(formData) toast(res.message) }} > <input type= "hidden" name= "_id" value={_id} /> <input type= "hidden" name= "name" value={name} /> <button type= "submit" disabled={pending} className= "btn btn-ghost" > Delete </button> </form> ) } |
1 2 3 4 5 6 7 8 9 10 11 | //lib\db-connect.ts import mongoose from 'mongoose' export default async function dbConnect() { try { await mongoose.connect(process.env.MONGODB_URI!) console.log( "Success Connection" ); } catch (error) { throw new Error( 'Connection failed!' ) } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | //lib\product-model.ts import mongoose from 'mongoose' export type Product = { _id: string name: string image: string price: number category: string } const productSchema = new mongoose.Schema( { name: { type: String, required: true, unique: true }, image: { type: String, required: true }, price: { type: Number, required: true }, category: { type: String, required: true }, }, { timestamps: true, } ) const ProductModel = mongoose.models.Product || mongoose.model( 'Product' , productSchema) export default ProductModel |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | //lib\actions.ts 'use server' import { revalidatePath } from 'next/cache' import ProductModel from './product-model' import dbConnect from './db-connect' import { z } from 'zod' export async function createProduct(prevState: any, formData: FormData) { const schema = z.object({ name: z.string().min(3), image: z.string().min(1), price: z.number().min(1), category: z.string().min(1), }) const parse = schema.safeParse({ name: formData.get( 'name' ), image: formData.get( 'image' ), price: Number(formData.get( 'price' )), category: formData.get( 'category' ), }) if (!parse.success) { console.log(parse.error) return { message: 'Form data is not valid' } } const data = parse.data try { await dbConnect() const product = new ProductModel(data) await product.save() revalidatePath( '/' ) return { message: `Created product ${data.name}` } } catch (e) { return { message: 'Failed to create product' } } } export async function deleteProduct(formData: FormData) { const schema = z.object({ _id: z.string().min(1), name: z.string().min(1), }) const data = schema.parse({ _id: formData.get( '_id' ), name: formData.get( 'name' ), }) try { await dbConnect() await ProductModel.findOneAndDelete({ _id: data._id }) revalidatePath( '/' ) console.log({ message: `Deleted product ${data.name}` }) return { message: `Deleted product ${data.name}` } } catch (e) { return { message: 'Failed to delete product' } } } |