Skip to content

MiladJoodi/Admin_Products_Next.js14

Repository files navigation

Admin Products By Next.js 14 Server Actions

  1. app/layout.tsx

    export const metadata: Metadata = {
      title: 'Admin Products By Next.js 14 Server Actions',
      description: 'Generated by create next app',
    }
  2. npm i mongoose zod daisyui react-hot-toast

  3. tailwind.config.ts

    plugins: [require('daisyui')]
  4. create mongodb atlas db and copy connection string

  5. .env

    MONGODB_URI = your - connection - string
  6. lib/db-connect.ts

    import mongoose from 'mongoose'
    
    export default async function dbConnect() {
      try {
        await mongoose.connect(process.env.MONGODB_URI!)
      } catch (error) {
        throw new Error('Connection failed!')
      }
    }
  7. lib/ProductModel.ts

    import mongoose from 'mongoose'
    
    export type Product = {
      _id: string
      name: string
      image: string
      price: number
      rating: number
    }
    const productSchema = new mongoose.Schema(
      {
        name: { type: String, required: true, unique: true },
        image: { type: String, required: true },
        price: { type: Number, required: true },
        rating: { type: Number, required: true, default: 0 },
      },
      {
        timestamps: true,
      }
    )
    const ProductModel =
      mongoose.models.Product || mongoose.model('Product', productSchema)
    export default ProductModel
  8. lib/actions.ts

    'use server'
    import { revalidatePath } from 'next/cache'
    import ProductModel from './ProductModel'
    import dbConnect from './dbConnect'
    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),
        rating: z.number().min(1).max(5),
      })
      const parse = schema.safeParse({
        name: formData.get('name'),
        image: formData.get('image'),
        price: Number(formData.get('price')),
        rating: Math.ceil(Math.random() * 5),
      })
      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' }
      }
    }
  9. app/create-form.tsx

    'use client'
    
    import { useFormState } from 'react-dom'
    import { 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()
      useEffect(() => {
        if (state.message.indexOf('Created product') === 0) {
          document.getElementById('my_modal_3')!.close()
          ref.current?.reset()
          toast(state.message)
        } else if (state.message) {
          toast(state.message)
        }
      }, [state.message])
      const ref = useRef<HTMLFormElement>(null)
    
      return (
        <div>
          <button
            className="btn btn-primary"
            onClick={() => document.getElementById('my_modal_3')!.showModal()}
          >
            Create Product
          </button>
          <dialog id="my_modal_3" 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/shirt1.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>
    
                <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_3').close()}
                >
                  Back
                </button>
              </form>
            </div>
          </dialog>
        </div>
      )
    }
  10. 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>
      )
    }
  11. app/Rating.tsx

    export default function Rating({ value }: { value: number }) {
      const Full = () => (
        <svg
          xmlns="http://www.w3.org/2000/svg"
          className="text-yellow-500 w-5 h-auto fill-current"
          viewBox="0 0 16 16"
        >
          <path d="M3.612 15.443c-.386.198-.824-.149-.746-.592l.83-4.73L.173 6.765c-.329-.314-.158-.888.283-.95l4.898-.696L7.538.792c.197-.39.73-.39.927 0l2.184 4.327 4.898.696c.441.062.612.636.282.95l-3.522 3.356.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256z" />
        </svg>
      )
      const Half = () => (
        <svg
          xmlns="http://www.w3.org/2000/svg"
          className="text-yellow-500 w-5 h-auto fill-current"
          viewBox="0 0 16 16"
        >
          <path d="M5.354 5.119 7.538.792A.516.516 0 0 1 8 .5c.183 0 .366.097.465.292l2.184 4.327 4.898.696A.537.537 0 0 1 16 6.32a.548.548 0 0 1-.17.445l-3.523 3.356.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256a.52.52 0 0 1-.146.05c-.342.06-.668-.254-.6-.642l.83-4.73L.173 6.765a.55.55 0 0 1-.172-.403.58.58 0 0 1 .085-.302.513.513 0 0 1 .37-.245l4.898-.696zM8 12.027a.5.5 0 0 1 .232.056l3.686 1.894-.694-3.957a.565.565 0 0 1 .162-.505l2.907-2.77-4.052-.576a.525.525 0 0 1-.393-.288L8.001 2.223 8 2.226v9.8z" />
        </svg>
      )
      const Empty = () => (
        <svg
          xmlns="http://www.w3.org/2000/svg"
          className="text-yellow-500 w-5 h-auto fill-current"
          viewBox="0 0 16 16"
        >
          <path d="M2.866 14.85c-.078.444.36.791.746.593l4.39-2.256 4.389 2.256c.386.198.824-.149.746-.592l-.83-4.73 3.522-3.356c.33-.314.16-.888-.282-.95l-4.898-.696L8.465.792a.513.513 0 0 0-.927 0L5.354 5.12l-4.898.696c-.441.062-.612.636-.283.95l3.523 3.356-.83 4.73zm4.905-2.767-3.686 1.894.694-3.957a.565.565 0 0 0-.163-.505L1.71 6.745l4.052-.576a.525.525 0 0 0 .393-.288L8 2.223l1.847 3.658a.525.525 0 0 0 .393.288l4.052.575-2.906 2.77a.565.565 0 0 0-.163.506l.694 3.957-3.686-1.894a.503.503 0 0 0-.461 0z" />
        </svg>
      )
    
      return (
        <div className="flex gap-2">
          <div className="flex gap-1">
            {value >= 1 ? <Full /> : value >= 0.5 ? <Half /> : <Empty />}
            {value >= 2 ? <Full /> : value >= 1.5 ? <Half /> : <Empty />}
            {value >= 3 ? <Full /> : value >= 2.5 ? <Half /> : <Empty />}
            {value >= 4 ? <Full /> : value >= 3.5 ? <Half /> : <Empty />}
            {value >= 5 ? <Full /> : value >= 4.5 ? <Half /> : <Empty />}
          </div>
        </div>
      )
    }
  12. copy & paste images in public folder

  13. app/page.tsx

    import dbConnect from '@/lib/dbConnect'
    import ProductModel, { Product } from '@/lib/ProductModel'
    import Image from 'next/image'
    import { Rating } from './Rating'
    import CreateForm from './create-form'
    import { DeleteForm } from './delete-form'
    import { Toaster } from 'react-hot-toast'
    
    export default async function HomePage() {
      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">
              Admin Products By Next.js 14 Server Actions
            </h1>
            <Toaster />
            <CreateForm />
          </div>
          <table className="table">
            <thead>
              <tr>
                <th>Image</th>
                <th>Name</th>
                <th>Price</th>
                <th>Rating</th>
                <th>Actions</th>
              </tr>
            </thead>
            <tbody>
              {products.length === 0 ? (
                <tr>
                  <td className="col-span-5">No product found</td>
                </tr>
              ) : (
                products.map((product: Product) => (
                  <tr key={product._id}>
                    <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>
                      <Rating value={product.rating} />
                    </td>
                    <td>
                      <DeleteForm
                        _id={product._id.toString()}
                        name={product.name}
                      />
                    </td>
                  </tr>
                ))
              )}
            </tbody>
          </table>
        </div>
      )
    }

About

Admin Products By Next.js 14 Server Actions

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published