article

Monday, January 8, 2024

Next.js 14 Server Actions MongoDB - List all data, Create and Delete

Next.js 14 Server Actions MongoDB - List all data, Create and Delete

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
//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))',
      },
    },
  },
  plugins: [require('daisyui')], //https://www.npmjs.com/package/react-daisyui
}
export default config
.env
MONGODB_URI=mongodb://127.0.0.1/nextjs14
app\page.tsx
//app\page.tsx
import Image from 'next/image' 
//https://www.npmjs.com/package/react-hot-toast
//https://www.npmjs.com/package/react-daisyui
//https://github.com/andreww2012/mongoose-zod
//https://www.npmjs.com/package/mongoose
//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>
  )
}
app\layout.tsx
//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>
  )
}
app\create-form.tsx
//
'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>
    )
}
app\delete-form.tsx
//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>
    )
}
lib\db-connect.ts
//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!')
    }
}
lib\product-model.ts
//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
lib\actions.ts
//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' }
    }
}
run C:\nextjs>npm run dev

Related Post