This commit is contained in:
omkieit 2024-09-20 16:48:56 +05:30
parent 3b144c93f5
commit f05a2f5432
11 changed files with 391 additions and 216 deletions

View File

@ -1,6 +1,5 @@
import mysql from "mysql2";
import pool from "../db/db.js";
const db = mysql.createConnection({
host: "db-mysql-nyc1-99306-do-user-12431193-0.b.db.ondigitalocean.com",
@ -99,3 +98,28 @@ export const searchMySQL = (req, res) => {
});
});
};
// Controller function to search for MYSQL properties by house_id
export const PropertiesMysqlView = async (req, res) => {
const { house_id } = req.params;
console.log("hu", house_id);
try {
const query = `
SELECT address, city, county, state, total_living_sqft, year_built, cost_per_sqft
FROM home_information
WHERE house_id = ?
`;
const [rows] = await pool.query(query, [house_id]);
if (rows.length > 0) {
res.status(200).json(rows[0]); // Send property details if found
} else {
res.status(404).json({ message: "Property not found" });
}
} catch (error) {
console.error("Error fetching property details:", error);
res.status(500).json({ error: "Internal Server Error" });
}
};

View File

@ -31,7 +31,7 @@ export const createProperty = async (req, res) => {
};
// Fetch property by userId
// Fetch property by userId.. Gets the specif user properties
export const getUserProperties = async (req, res) => {
try {
const userId = req.params.userId;
@ -42,7 +42,7 @@ export const getUserProperties = async (req, res) => {
}
};
// Fetch property by ID
// Fetch property by ID.. which is property view page
export const getPropertyById = async (req, res) => {
const { propertyId } = req.params;
try {

26
ef-api/db/db.js Normal file
View File

@ -0,0 +1,26 @@
import mysql from "mysql2/promise"; // Use promise-based API
// Create a pool instead of a single connection
const pool = mysql.createPool({
host: "db-mysql-nyc1-99306-do-user-12431193-0.b.db.ondigitalocean.com",
user: "doadmin",
password: "AVNS_EPHqPilzmVjZfm8GH4G",
database: "defaultdb",
port: "25060",
waitForConnections: true,
connectionLimit: 10, // Number of connections in the pool
queueLimit: 0, // No limit on queued connection requests
connectTimeout: 20000 // 20 seconds
});
// Test the pool connection
pool.getConnection()
.then(conn => {
console.log("Connected to the MySQL POOL database.");
conn.release(); // Release the connection back to the pool
})
.catch(err => {
console.error("Database connection failed: " + err.stack);
});
export default pool;

132
ef-api/package-lock.json generated
View File

@ -20,6 +20,7 @@
"morgan": "^1.10.0",
"mysql": "^2.18.1",
"mysql2": "^3.11.3",
"mysql2-promise": "^0.1.4",
"nodemailer": "^6.9.14",
"nodemon": "^3.1.5",
"uuid": "^10.0.0"
@ -62,6 +63,12 @@
"node": ">= 0.6"
}
},
"node_modules/ansicolors": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.2.1.tgz",
"integrity": "sha512-tOIuy1/SK/dr94ZA0ckDohKXNeBNqZ4us6PjMVLs5h1w2GBB6uPtOknp2+VF4F/zcy9LI70W+Z+pE2Soajky1w==",
"license": "MIT"
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
@ -141,6 +148,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/bn.js": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-2.0.0.tgz",
"integrity": "sha512-NmOLApC80+n+P28y06yHgwGlOCkq/X4jRh5s590959FZXSrM+I/61h0xxuIaYsg0mD44mEAZYG/rnclWuRoz+A==",
"license": "MIT"
},
"node_modules/body-parser": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
@ -230,6 +243,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/cardinal": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/cardinal/-/cardinal-0.4.4.tgz",
"integrity": "sha512-3MxV0o9wOpQcobrcSrRpaSxlYkohCcZu0ytOjJUww/Yo/223q4Ecloo7odT+M0SI5kPgb1JhvSaF4EEuVXOLAQ==",
"license": "MIT",
"dependencies": {
"ansicolors": "~0.2.1",
"redeyed": "~0.4.0"
},
"bin": {
"cdl": "bin/cdl.js"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@ -381,6 +407,12 @@
"url": "https://dotenvx.com"
}
},
"node_modules/double-ended-queue": {
"version": "2.0.0-0",
"resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.0.0-0.tgz",
"integrity": "sha512-t5ouWOpItmHrm0J0+bX/cFrIjBFWnJkk5LbIJq6bbU/M4aLX2c3LrM4QYsBptwvlPe3WzdpQefQ0v1pe/A5wjg==",
"license": "MIT"
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@ -436,6 +468,18 @@
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/esprima": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz",
"integrity": "sha512-rp5dMKN8zEs9dfi9g0X1ClLmV//WRyk/R15mppFNICIFRG5P92VP7Z04p8pk++gABo9W2tY+kHyu6P1mEHgmTA==",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@ -1203,6 +1247,74 @@
"node": ">= 8.0"
}
},
"node_modules/mysql2-promise": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/mysql2-promise/-/mysql2-promise-0.1.4.tgz",
"integrity": "sha512-/h8ubU/36aIPpbfB6CENw9ZdbzIhZMZOIbstJUHVKp4J9JBRSLScrYImVx+3yZilgag732UhpQMMK5+ktdhLCw==",
"license": "MIT",
"dependencies": {
"mysql2": "^0.15.7",
"q": "^1.3.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/mysql2-promise/node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
"license": "MIT"
},
"node_modules/mysql2-promise/node_modules/lru-cache": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.5.0.tgz",
"integrity": "sha512-dVmQmXPBlTgFw77hm60ud//l2bCuDKkqC2on1EBoM7s9Urm9IQDrnujwZ93NFnAq0dVZ0HBXTS7PwEG+YE7+EQ==",
"license": "MIT"
},
"node_modules/mysql2-promise/node_modules/mysql2": {
"version": "0.15.8",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-0.15.8.tgz",
"integrity": "sha512-3x5o6C20bfwJYPSoT74MOoad7/chJoq4qXHDL5VAuRBBrIyErovLoj04Dz/5EY9X2kTxWSGNiTegtxpROTd2YQ==",
"license": "MIT",
"dependencies": {
"bn.js": "2.0.0",
"cardinal": "0.4.4",
"double-ended-queue": "2.0.0-0",
"named-placeholders": "0.1.3",
"readable-stream": "1.0.33"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/mysql2-promise/node_modules/named-placeholders": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-0.1.3.tgz",
"integrity": "sha512-Mt79RtxZ6MYTIEemPGv/YDKpbuavcAyGHb0r37xB2mnE5jej3uBzc4+nzOeoZ4nZiii1M32URKt9IjkSTZAmTA==",
"license": "MIT",
"dependencies": {
"lru-cache": "2.5.0"
}
},
"node_modules/mysql2-promise/node_modules/readable-stream": {
"version": "1.0.33",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.33.tgz",
"integrity": "sha512-72KxhcKi8bAvHP/cyyWSP+ODS5ef0DIRs0OzrhGXw31q41f19aoELCbvd42FjhpyEDxQMRiiC5rq9rfE5PzTqg==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"node_modules/mysql2-promise/node_modules/string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==",
"license": "MIT"
},
"node_modules/mysql2/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@ -1417,6 +1529,17 @@
"node": ">=6"
}
},
"node_modules/q": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
"integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==",
"deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)",
"license": "MIT",
"engines": {
"node": ">=0.6.0",
"teleport": ">=0.2.0"
}
},
"node_modules/qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
@ -1498,6 +1621,15 @@
"node": ">=8.10.0"
}
},
"node_modules/redeyed": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/redeyed/-/redeyed-0.4.4.tgz",
"integrity": "sha512-pnk1vsaNLu1UAAClKsImKz9HjBvg9i8cbRqTRzJbiCjGF0fZSMqpdcA5W3juO3c4etFvTrabECkq9wjC45ZyxA==",
"license": "MIT",
"dependencies": {
"esprima": "~1.0.4"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",

View File

@ -22,6 +22,7 @@
"morgan": "^1.10.0",
"mysql": "^2.18.1",
"mysql2": "^3.11.3",
"mysql2-promise": "^0.1.4",
"nodemailer": "^6.9.14",
"nodemon": "^3.1.5",
"uuid": "^10.0.0"

View File

@ -1,9 +1,10 @@
import express from "express";
import { searchMySQL } from "../controllers/mysqlproperty.js";
import { searchMySQL, PropertiesMysqlView } from "../controllers/mysqlproperty.js";
const router = express.Router();
// Define the route
router.get("/searchmysql", searchMySQL);
router.get("/properties/:house_id", PropertiesMysqlView);
export default router;

File diff suppressed because one or more lines are too long

View File

@ -42,7 +42,7 @@
<!-- <script src="https://stackpath.bootstrapcdn.com/bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script> -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/js/bootstrap.bundle.min.js"></script>
<script type="module" crossorigin src="/assets/index-tUPF5Gax.js"></script>
<script type="module" crossorigin src="/assets/index-DdbR_cuq.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DlbKQED5.css">
</head>

View File

@ -15,6 +15,7 @@ import Registrationsuccess from "./components/Registrationsuccess";
import PropertyView from "./components/PropertyView";
import SearchMysql from "./components/SearchMysql";
import Services from "./components/Services";
import PropertyMysqlView from "./components/PropertyMysqlView";
const App = () => {
@ -58,8 +59,7 @@ const App = () => {
{/* <Route path="/addproperty" element={ <PrivateRoute><Addproperty /></PrivateRoute>}></Route> */}
<Route path="/property/:id" element={<PropertyView />} />
<Route path="/properties/:id" element={<PropertyView />} />
<Route path="/properties/:house_id" element={<PropertyMysqlView />} />
<Route path="/searchmyproperties" element={<SearchMysql />} />

View File

@ -0,0 +1,60 @@
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import axios from "axios";
import Navbar from "./Navbar";
import Footer from "./Footer";
const PropertyMysqlView = () => {
const { house_id } = useParams(); // Get house_id from the URL
console.log("house_id", house_id);
const [propertyDetails, setPropertyDetails] = useState(null); // State for property details
const [loading, setLoading] = useState(true); // Loader state
// Fetch property details from API
useEffect(() => {
const fetchPropertyDetails = async () => {
try {
const res = await axios.get(
`${import.meta.env.VITE_REACT_APP_SECRET}/mysql/properties/${house_id}`
);
setPropertyDetails(res.data); // Set the property details
} catch (err) {
console.log("Error fetching property details:", err);
} finally {
setLoading(false); // Stop loading
}
};
fetchPropertyDetails();
}, [house_id]);
return (
<>
<Navbar />
<br /> <br /> <br /> <br /> <br /> <br /> <br /> <br />
<div className="container">
{loading ? (
<div className="loader">Loading...</div> // Loader
) : propertyDetails ? (
<div className="property-details">
<h2>Property Details</h2>
<p><strong>Address:</strong> {propertyDetails.address}</p>
<p><strong>City:</strong> {propertyDetails.city}</p>
<p><strong>County:</strong> {propertyDetails.county}</p>
<p><strong>State:</strong> {propertyDetails.state}</p>
<p><strong>Total Living Square Foot:</strong> {propertyDetails.total_living_sqft}</p>
<p><strong>Year Built:</strong> {propertyDetails.year_built}</p>
<p><strong>Cost per Square Foot:</strong> ${propertyDetails.cost_per_sqft}/sqft</p>
</div>
) : (
<p>No property details found.</p>
)}
</div>
<br /> <br /> <br /> <br />
<Footer />
</>
);
};
export default PropertyMysqlView;

View File

@ -1,4 +1,4 @@
import { useEffect, useState, useCallback } from "react";
import { useEffect, useState } from "react";
import { NavLink } from "react-router-dom";
import axios from "axios";
import Navbar from "./Navbar";
@ -8,59 +8,45 @@ import "../searchmysqlresults.css";
const SearchMysql = () => {
const [properties, setProperties] = useState([]);
const [totalRecords, setTotalRecords] = useState(0);
const [page, setPage] = useState(0); // Page number
const [limit] = useState(10); // Items per page
const [search, setSearch] = useState(""); // Search query state
const [loading, setLoading] = useState(false); // Loader state
const [page, setPage] = useState(0);
const [limit] = useState(10);
const [search, setSearch] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [showResultsMessage, setShowResultsMessage] = useState(false);
// Fetch properties from API
const fetchProperties = async () => {
setLoading(true);
setIsLoading(true);
setShowResultsMessage(false); // Reset message before fetching
try {
const res = await axios.get(
`${import.meta.env.VITE_REACT_APP_SECRET}/mysql/searchmysql`,
{
params: { limit, offset: page * limit, search },
headers: { "Cache-Control": "no-cache" }, // Disable caching
headers: { "Cache-Control": "no-cache" },
}
);
setProperties(res.data.data); // Set properties
setTotalRecords(res.data.totalRecords); // Set total records
setProperties(res.data.data);
setTotalRecords(res.data.totalRecords);
if (search.trim()) {
setShowResultsMessage(true); // Show message if there's a search term
}
} catch (err) {
console.log("Error fetching data:", err);
} finally {
setLoading(false);
setIsLoading(false);
}
};
// Debounce the search input
const debounce = (func, delay) => {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => func(...args), delay);
};
};
// Debounced fetch properties
const debouncedFetchProperties = useCallback(
debounce(() => {
if (search.trim() === "") {
fetchProperties(); // Fetch default properties when search is empty
} else {
fetchProperties(); // Fetch properties with new search term
}
}, 3000), // 3 seconds delay
[search, page]
);
// Handle search input change
const handleSearchChange = (e) => {
setSearch(e.target.value);
// Trigger search only if input is non-empty
if (e.target.value.trim() === "") {
setPage(0); // Reset to first page
fetchProperties(); // Fetch default properties immediately
setProperties([]); // Clear properties
setTotalRecords(0); // Clear total records
setShowResultsMessage(false); // Hide message
fetchProperties(); // Fetch default properties
}
};
@ -68,33 +54,21 @@ const SearchMysql = () => {
const handleKeyPress = (e) => {
if (e.key === "Enter") {
e.preventDefault();
if (search.trim()) {
debouncedFetchProperties(); // Fetch properties on Enter key
}
}
};
// Handle input blur to fetch results after typing
const handleBlur = () => {
if (search.trim() !== "") {
debouncedFetchProperties(); // Fetch properties after typing is complete
fetchProperties(); // Fetch properties on Enter key
}
};
// Use effect to fetch properties when search or page changes
useEffect(() => {
// Fetch default properties when the component mounts or page changes
if (search.trim() === "") {
fetchProperties(); // Fetch default properties
}
}, [page]);
fetchProperties();
}, [search, page]); // Fetch when search or page changes
const totalPages = Math.ceil(totalRecords / limit);
return (
<>
<Navbar />
<br /> <br /> <br /> <br /> <br /> <br />
{/* Display properties */}
<br /> <br /> <br /> <br /> <br /> <br />
<div className="container col-12">
<div className="row">
<div className="col-lg-12 card-margin col-12">
@ -114,10 +88,8 @@ const SearchMysql = () => {
value={search}
onChange={handleSearchChange}
onKeyDown={handleKeyPress}
onBlur={handleBlur} // Trigger search on blur
/>
</div>
<div className="col-lg-1 col-md-3 col-sm-12 p-0"></div>
</div>
</div>
</div>
@ -130,147 +102,106 @@ const SearchMysql = () => {
<div className="col-12">
<div className="card card-margin">
<div className="card-body">
{loading ? (
<div className="loader" style={{color:"#fda417"}}><span style={{color:"#fda417", fontSize: "25px"}}>Loading...</span></div>
) : (
<div className="row search-body">
<div className="col-lg-12">
<div className="search-result col-12">
<div className="result-header">
<div className="row">
<div className="col-lg-6">
<div className="records">
{/* Pagination Controls */}
<div>
<button
onClick={() => setPage(page - 1)}
disabled={page === 0}
style={{
backgroundColor: "#fda417",
border: "#fda417",
}}
>
Previous
</button>
<span>
{" "}
Page {page + 1} of {totalPages}{" "}
</span>
<button
onClick={() => setPage(page + 1)}
disabled={page + 1 >= totalPages}
style={{
backgroundColor: "#fda417",
border: "#fda417",
}}
>
Next
</button>
</div>
</div>
</div>
</div>
</div>
<div className="result-body">
<div className="table-responsive">
<table className="table widget-26">
{/* Add table headers */}
<thead style={{color:"#fda417", fontSize: "15px"}}>
<tr>
<th>Details</th>
<th>Total Living Square Foot</th>
<th>Year Built</th>
<th>Cost per Square Foot</th>
</tr>
</thead>
<tbody>
{properties.length > 0 ? (
properties.map((property, index) => (
<tr key={index}>
<td>
<div className="widget-26-job-title">
<NavLink
to={`/properties/${property.house_id}`}
className="link-primary text-decoration-none"
>
{property.address}
</NavLink>
<p className="m-0">
<span className="employer-name">
<i
className="fa fa-map-marker"
style={{ color: "#F74B02" }}
/>
{property.city}, {property.county},
{property.state}
</span>{" "}
<p className="text-muted m-0">
House Id: {property.house_id}
</p>
</p>
</div>
</td>
<td>
<div className="widget-26-job-info">
<p className="m-0">
{property.total_living_sqft}
</p>
</div>
</td>
<td>
<div className="widget-26-job-info">
<p className="m-0">
{property.year_built}
</p>
</div>
</td>
<td>
<div className="widget-26-job-salary">
$ {property.cost_per_sqft}/sqft
</div>
</td>
</tr>
))
) : (
<tr>
<td colSpan="4">No results found</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
{/* Pagination Controls */}
<div>
<button
onClick={() => setPage(page - 1)}
disabled={page === 0}
style={{
backgroundColor: "#fda417",
border: "#fda417",
}}
>
Previous
</button>
<span>
{" "}
Page {page + 1} of {totalPages}{" "}
</span>
<button
onClick={() => setPage(page + 1)}
disabled={page + 1 >= totalPages}
style={{
backgroundColor: "#fda417",
border: "#fda417",
}}
>
Next
</button>
</div>
</div>
</div>
{showResultsMessage && properties.length > 0 && (
<div>
Showing search results for the keyword <span style={{ color: "#fda417", fontSize: "25px" }}>{search}</span>
</div>
)}
<div className="table-responsive">
<table className="table widget-26">
<thead style={{ color: "#fda417", fontSize: "15px" }}>
<tr>
<th>Details</th>
<th>Total Living Square Foot</th>
<th>Year Built</th>
<th>Cost per Square Foot</th>
</tr>
</thead>
<tbody>
{properties.length > 0 ? (
properties.map((property, index) => (
<tr key={index}>
<td>
<div className="widget-26-job-title">
<NavLink
to={`/properties/${property.house_id}`}
className="link-primary text-decoration-none"
target="_blank"
>
{property.address}
</NavLink>
<p className="m-0">
<span className="employer-name">
<i
className="fa fa-map-marker"
style={{ color: "#F74B02" }}
/>
{property.city}, {property.county},
{property.state}
</span>
<p className="text-muted m-0">
House Id: {property.house_id}
</p>
</p>
</div>
</td>
<td>
<div className="widget-26-job-info">
<p className="m-0">
{property.total_living_sqft}
</p>
</div>
</td>
<td>
<div className="widget-26-job-info">
<p className="m-0">
{property.year_built}
</p>
</div>
</td>
<td>
<div className="widget-26-job-salary">
$ {property.cost_per_sqft}/sqft
</div>
</td>
</tr>
))
) : (
<tr>
<td colSpan="4">No results found</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Pagination Controls */}
<div>
<button
onClick={() => setPage(page - 1)}
disabled={page === 0 || isLoading}
style={{
backgroundColor: "#fda417",
border: "#fda417",
}}
>
Previous
</button>
<span>
{" "}
Page {page + 1} of {totalPages}{" "}
</span>
<button
onClick={() => setPage(page + 1)}
disabled={page + 1 >= totalPages || isLoading}
style={{
backgroundColor: "#fda417",
border: "#fda417",
}}
>
Next
</button>
</div>
</div>
</div>
</div>