View + Route.ts to change true to false in DB in NextJS
Hello guys, today I want to show you how to create the component and route.ts that will change into database the state of “isOn” from true to false and in the reverse order if you want to.
The usecase of this one I want to introduce you is, for example if you want to turn off Support Bot on your website and you want to have from the admin side the checkbox that after cliking make the support bot off or on — here is the best way for you.
The checkbox after cliking make the change from FALSE to TRUE and in the reverse order.
I know that you very often need the component in which you will change the state from FALSE to TRUE, because frequently we want to give the possibility for users, admins to turn off or turn on something give the possibility to have a choice in functions like these.
So to do that we will use database, backend, frontend — We do not want to let the frontend be a responsible side for this only.
First of all from what we have to start?
DATABASE model
Okay guys it’s time to create a database model:
// lib\models\ProductLackShowOnOff.ts
// lib\models\ProductLackShowOnOff.ts
import mongoose, { Schema, Document } from 'mongoose';
interface IProductLackShowOnOff extends Document {
isOn: boolean;
}
const ProductLackShowOnOffSchema: Schema = new Schema({
isOn: { type: Boolean, required: true, default: false }
});
export default mongoose.models.ProductLackShowOnOff || mongoose.model<IProductLackShowOnOff>('ProductLackShowOnOff', ProductLackShowOnOffSchema);
- Understanding the File Path:
- The file is located in a directory called
lib
and then inside a sub-directory namedmodels
.
The file itself is named ProductLackShowOnOff.ts
.
2. Imports:
- The first line imports
mongoose
, which is a tool used to interact with a database called MongoDB. Schema
andDocument
are specific features frommongoose
.Schema
helps define the structure of data, andDocument
represents a single item of data.
3. Interface Definition:
- The code defines an interface called
IProductLackShowOnOff
. - An interface is like a blueprint. It describes how an object (in this case, a product status) should look.
- This interface says that any object of this type will have one property:
isOn
, which is a true/false value (a boolean).
4. Schema Creation:
- Next, a schema called
ProductLackShowOnOffSchema
is created. - A schema is like a detailed description of the structure of data.
- This schema describes an object that must have a
isOn
property. - The
isOn
property must be a boolean (true or false) and it is required (must be present). - If not specified,
isOn
will default tofalse
.
5. Exporting the Model:
- The last line is exporting (making available) a model called
ProductLackShowOnOff
. - A model is a fancy term for the way you interact with the data in the database.
- This line checks if a model named
ProductLackShowOnOff
already exists. If it does, it uses the existing one. If not, it creates a new one based on theProductLackShowOnOffSchema
.
Sum up:
- This code sets up a structure for storing and working with data about whether a certain product feature (probably indicating lack of stock) is turned on or off.
- It ensures that each item in the database has a boolean property named
isOn
that defaults tofalse
if not specified. - Finally, it prepares this structure to be used easily within the rest of the application.
Now it’s time on route.ts
// app\api\admin\products\lack\route.ts
import { NextResponse } from 'next/server';
import dbConnect from '@/lib/dbConnect';
import ProductLackShowOnOff from '@/lib/models/ProductLackShowOnOff';
import { auth } from '@/lib/auth';
export const PATCH = auth(async (req) => {
await dbConnect();
if (!req.auth || !req.auth.user || !req.auth.user.isAdmin) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 });
}
const { isOn } = await req.json();
try {
let status = await ProductLackShowOnOff.findOne();
if (!status) {
status = new ProductLackShowOnOff({ isOn });
} else {
status.isOn = isOn;
}
await status.save();
return NextResponse.json({ success: true, data: status }, { status: 200 });
} catch (error) {
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
}
});
export const GET = auth(async (req) => {
await dbConnect();
if (!req.auth || !req.auth.user || !req.auth.user.isAdmin) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 });
}
try {
let status = await ProductLackShowOnOff.findOne();
if (!status) {
status = new ProductLackShowOnOff({ isOn: false });
await status.save();
}
return NextResponse.json({ success: true, data: status }, { status: 200 });
} catch (error) {
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
}
});
- Understanding the File Path:
- The file is located in the
app
directory, inside the sub-directoryapi/admin/products/lack
, and the file is namedroute.ts
.
2. Imports:
NextResponse
is imported from a library callednext/server
, which is used for sending responses in a server environment.dbConnect
is imported from a file that likely handles connecting to the database.ProductLackShowOnOff
is the model we discussed earlier, which handles the product status.auth
is a function for handling authorization, ensuring only certain users can perform actions.
3. PATCH Handler:
- This function handles update requests.
auth
wraps the function to ensure only authorized users can access it.await dbConnect();
connects to the database.- It checks if the request has valid authorization information and if the user is an admin. If not, it returns an “Unauthorized” response.
- It extracts the
isOn
value from the request body. - It tries to find the current status in the database:
- If it doesn’t exist, it creates a new status with the
isOn
value. - If it exists, it updates the
isOn
value. - It saves the status to the database.
- If everything goes well, it returns a success response with the status data. If there is an error, it returns an error response.
4. GET Handler:
- This function handles retrieval requests.
- Similar to the PATCH handler, it ensures the user is authorized.
- Connects to the database.
- Checks if the request is authorized and if the user is an admin. If not, it returns an “Unauthorized” response.
- It tries to find the current status in the database:
- If it doesn’t exist, it creates a new status with
isOn
set tofalse
. - It saves the new status if it was created.
- If everything goes well, it returns a success response with the status data. If there is an error, it returns an error response.
Summing up:
- This file contains two main functions to handle requests related to a product feature’s on/off status.
- The PATCH function updates this status, while the GET function retrieves it.
- Both functions ensure the user is an admin before proceeding.
- They connect to the database, handle the necessary logic, and return appropriate responses based on the success or failure of the operations.
In NextJS we use the architecture like this, so we use route.ts.
Now it’s time to component
ye, in my app the component is the checkbox with the text “Show Lack Products”.
// components/LackProductButton.tsx
"use client"
import { useState, useEffect } from 'react';
const LackProductButton = () => {
const [isOn, setIsOn] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchStatus = async () => {
try {
const response = await fetch('/api/admin/products/lack', {
method: 'GET',
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
console.log('Fetched status:', data); // Log response data
setIsOn(data?.data?.isOn ?? false);
} catch (error) {
console.error('Failed to fetch status:', error);
} finally {
setLoading(false);
}
};
fetchStatus();
}, []);
const handleToggle = async () => {
const newStatus = !isOn;
try {
const response = await fetch('/api/admin/products/lack', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ isOn: newStatus }),
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
console.log('Updated status:', data); // Log response data
if (data.success) {
setIsOn(newStatus);
} else {
alert('Failed to update status');
}
} catch (error) {
console.error('Failed to update status:', error);
}
};
if (loading) {
return <div>Loading...</div>;
}
return (
<label>
Show Lack Products
<input
type="checkbox"
checked={isOn}
onChange={handleToggle}
/>
</label>
);
};
export default LackProductButton;
ye, this component is simple as could be and it’s ok we have no time to make the great design of it.
Let’s describe it:
- Understanding the File Path:
- The file is located in the
components
directory and is namedLackProductButton.tsx
.
2. Imports:
useState
anduseEffect
are functions imported from a library calledreact
. These functions help manage the component's state and handle side effects, respectively.
3. Component Definition:
- The component
LackProductButton
is defined as a function.
4. State Initialization:
const [isOn, setIsOn] = useState(false);
- This line creates a state variable called
isOn
with a default value offalse
.setIsOn
is a function to updateisOn
. const [loading, setLoading] = useState(true);
- This line creates another state variable called
loading
with a default value oftrue
.setLoading
is a function to updateloading
.
5 Fetching Status with useEffect:
useEffect
runs a function when the component is first rendered.- Inside this function,
fetchStatus
is defined and immediately called. fetchStatus
is an asynchronous function that:- Makes a GET request to
/api/admin/products/lack
to fetch the current status. - If the response is okay, it updates
isOn
with the fetched status. - If there is an error, it logs the error.
- Finally, it sets
loading
tofalse
indicating the loading process is complete.
6. Handling Toggle:
handleToggle
is defined as an asynchronous function that:- Toggles the
isOn
state to its opposite value (!isOn
). - Makes a PATCH request to update the status on the server.
- If the response is okay and the update is successful, it updates
isOn
with the new status. - If there is an error, it logs the error or alerts the user.
7. Rendering:
- If
loading
istrue
, the component renders a loading message. - Otherwise, it renders a label with a checkbox.
- The checkbox is checked based on the
isOn
state. - When the checkbox is changed, it calls
handleToggle
to update the status.
Summary:
- This code defines a button component that shows whether a product feature (probably indicating lack of stock) is on or off.
- It fetches the current status from the server when the component loads.
- It allows the user to toggle this status on or off by clicking the checkbox.
- It updates the server with the new status whenever the checkbox is toggled.
- It handles loading states and errors appropriately, ensuring a smooth user experience.
REMEMBER TO CONNECT COMPONENT TO YOUR VIEW!
Yes it is the detail and you know it very well I guess but some people don’t.
If you have the checkbox component ready to go, you have to connnect him and put him to the view!
In our case we put our component: import LackProductButton from ‘@/components/LackProductButton’;
into:
app\admin\products\Products.tsx
// app\admin\products\Products.tsx
'use client'
import { useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import toast from 'react-hot-toast'
import useSWR from 'swr'
import useSWRMutation from 'swr/mutation'
import { v4 as uuidv4 } from 'uuid';
import LackProductButton from '@/components/LackProductButton';
export default function Products() {
const [page, setPage] = useState(1)
const { data: productsData, error: productsError } = useSWR(`/api/admin/products?page=${page}&limit=15`)
const { data: categories, error: categoriesError, mutate: mutateCategories } = useSWR(`/api/admin/categories`)
const [showModal, setShowModal] = useState(false)
const [productToDelete, setProductToDelete] = useState(null)
const router = useRouter()
const generateSlug = (name) => `${name.toLowerCase().replace(/ /g, '-')}-${uuidv4()}`;
const getRandomCategory = () => {
if (!categories || categories.length === 0) {
return null; // Return null if no categories exist
}
const randomIndex = Math.floor(Math.random() * categories.length);
return categories[randomIndex]._id;
};
const createDefaultCategory = async () => {
const response = await fetch('/api/admin/categories', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ name: 'Sample Category' })
});
const newCategory = await response.json();
mutateCategories([...categories, newCategory], false); // Update SWR cache without re-fetching
return newCategory._id;
};
const newProductData = async () => {
let categoryId = getRandomCategory();
if (!categoryId) {
categoryId = await createDefaultCategory(); // Create a default category if none exist
}
return {
name: 'Sample Product Name',
slug: generateSlug('Sample Product Name'),
price: 100.00,
categoryId,
image: '/images/default-product.jpg',
brand: 'Sample Brand',
countInStock: 0,
description: 'Sample product description'
};
};
const { trigger: deleteProduct } = useSWRMutation(
`/api/admin/products`,
async (url, { arg }: { arg: { productId: string } }) => {
const toastId = toast.loading('Deleting product...')
const res = await fetch(`${url}/${arg.productId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
})
const data = await res.json()
setShowModal(false) // Hide modal after operation
if (res.ok) {
setTimeout(() => {
toast.success('Product deleted successfully', {
id: toastId,
})
router.refresh()
}, 1000) // Odświeżenie strony po 1 sekundzie
} else {
toast.error(data.message, {
id: toastId,
})
}
}
)
const { trigger: createProduct, isMutating: isCreating } = useSWRMutation(
`/api/admin/products`,
async (url) => {
const toastId = toast.loading('Creating product...');
try {
const productData = await newProductData(); // Dodaj await tutaj
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(productData)
});
const data = await res.json();
if (!res.ok) {
toast.error(data.message, {
id: toastId,
});
return;
}
toast.success('Product created successfully', {
id: toastId,
});
router.push(`/admin/products/${data.product._id}`);
} catch (error) {
toast.error('Failed to create the product.', {
id: toastId,
});
console.error('Failed to create product:', error);
}
}
);
if (productsError || categoriesError) return <p>An error has occurred.</p>
if (!productsData || !categories) return <p>Loading...</p>
const { products, totalPages } = productsData
const handleDeleteClick = (productId: string) => {
setProductToDelete(productId)
setShowModal(true)
}
const confirmDelete = () => {
if (productToDelete) {
deleteProduct({ productId: productToDelete })
}
}
const handlePreviousPage = () => {
if (page > 1) setPage(page - 1)
}
const handleNextPage = () => {
if (page < totalPages) setPage(page + 1)
}
return (
<div>
<div className="flex justify-between items-center">
<h1 className="py-4 text-2xl">Products</h1>
<LackProductButton /> {/* Dodanie komponentu */}
<button
disabled={isCreating}
onClick={() => createProduct()}
className="btn btn-primary btn-sm"
>
{isCreating ? (
<span className="loading loading-spinner"></span>
) : (
"Create"
)}
</button>
</div>
<div className="overflow-x-auto">
<table className="table table-zebra w-full">
<thead>
<tr>
<th><div className="badge">id</div></th>
<th><div className="badge">name</div></th>
<th><div className="badge">price</div></th>
<th><div className="badge">category</div></th>
<th><div className="badge">count in stock</div></th>
{/* <th><div className="badge">rating</div></th> */}
<th><div className="badge">actions</div></th>
</tr>
</thead>
<tbody>
{products.map((product: any) => (
<tr key={product._id}>
<td>{product._id}</td>
<td>{product.name}</td>
<td>${product.price}</td>
<td>{product.category}</td>
<td>{product.countInStock}</td>
{/* <td>{product.rating}</td> */}
<td>
<Link href={`/admin/products/${product._id}`}>
<button type="button" className="btn btn-info btn-sm">
Edit
</button>
</Link>
<button
onClick={() => handleDeleteClick(product._id)}
type="button"
className="btn btn-error btn-sm"
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="flex justify-center items-center mt-4 space-x-4">
<button
onClick={handlePreviousPage}
disabled={page === 1}
className="bg-blue-500 text-white py-2 px-4 rounded disabled:opacity-50"
>
Previous
</button>
<span className="text-gray-700">Page {page} of {totalPages}</span>
<button
onClick={handleNextPage}
disabled={page === totalPages}
className="bg-blue-500 text-white py-2 px-4 rounded disabled:opacity-50"
>
Next
</button>
</div>
{/* Confirmation Modal */}
{showModal && (
<div className="modal modal-open">
<div className="modal-box">
<h3 className="font-bold text-lg">Are you sure you want to delete this product?</h3>
<div className="modal-action">
<button onClick={confirmDelete} className="btn btn-error">
Yes
</button>
<button onClick={() => setShowModal(false)} className="btn">
No
</button>
</div>
</div>
</div>
)}
</div>
)
}
And that’s it! all is working well great and the state of true or false working well!
Thanks for reading bros.
Peace and love, no war.