View + Route.ts to change true to false in DB in NextJS

Dorian Szafranski
12 min readJun 7, 2024

--

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.

In this case after clicking the checkbox you change the state into database from false to true or from true to false.

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

Mongodb

Okay guys it’s time to create a database model:

// lib\models\ProductLackShowOnOff.ts

// 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);
  1. Understanding the File Path:
  • The file is located in a directory called lib and then inside a sub-directory named models.

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 and Document are specific features from mongoose. Schema helps define the structure of data, and Document represents a single item of data.
imports

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 to false.

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 the ProductLackShowOnOffSchema.

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 to false 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

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 });
}
});
  1. Understanding the File Path:
  • The file is located in the app directory, inside the sub-directory api/admin/products/lack, and the file is named route.ts.

2. Imports:

  • NextResponse is imported from a library called next/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 to false.
  • 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:

  1. Understanding the File Path:
  • The file is located in the components directory and is named LackProductButton.tsx.

2. Imports:

  • useState and useEffect are functions imported from a library called react. These functions help manage the component's state and handle side effects, respectively.

3. Component Definition:

const = function is js.
  • 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 of false. setIsOn is a function to update isOn.
  • const [loading, setLoading] = useState(true);
  • This line creates another state variable called loading with a default value of true. setLoading is a function to update loading.

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 to false 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 is true, 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:

component after clicked to true is true also in db
mongodb when is true component
  • 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!

RED IS COMPOENT, BLUE IS 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>
&nbsp;
<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.

--

--