-
app/layout.tsx
export const metadata: Metadata = { title: 'Admin Products By Next.js 14 Server Actions', description: 'Generated by create next app', }
-
npm i mongoose zod daisyui react-hot-toast
-
tailwind.config.ts
plugins: [require('daisyui')]
-
create mongodb atlas db and copy connection string
-
.env
MONGODB_URI = your - connection - string
-
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!') } }
-
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
-
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' } } }
-
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> ) }
-
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> ) }
-
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> ) }
-
copy & paste images in public folder
-
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> ) }
-
Notifications
You must be signed in to change notification settings - Fork 0
MiladJoodi/Admin_Products_Next.js14
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
 |  | |||
 |  | |||
 |  | |||
 |  | |||
 |  | |||
 |  | |||
 |  | |||
 |  | |||
 |  | |||
 |  | |||
 |  | |||
 |  | |||
 |  | |||
 |  | |||
Repository files navigation
About
Admin Products By Next.js 14 Server Actions
Resources
Stars
Watchers
Forks
Releases
No releases published
Packages 0
No packages published