done
This commit is contained in:
parent
1549c32eb6
commit
3750a089f4
|
@ -29,7 +29,7 @@ db.connect((err) => {
|
||||||
console.log("Connected to the MYSQL database.");
|
console.log("Connected to the MYSQL database.");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Controller function
|
// Controller function to get the data from the table from MYSQL with no pagination and huge data cant be getting
|
||||||
// export const searchMySQL = (req, res) => {
|
// export const searchMySQL = (req, res) => {
|
||||||
// const q = "SELECT * FROM client_information";
|
// const q = "SELECT * FROM client_information";
|
||||||
// // const q = "SELECT * FROM home_information";
|
// // const q = "SELECT * FROM home_information";
|
||||||
|
@ -42,21 +42,60 @@ db.connect((err) => {
|
||||||
// });
|
// });
|
||||||
// };
|
// };
|
||||||
|
|
||||||
|
// Controller function to get all data from MYSQL table with pagination
|
||||||
|
// export const searchMySQL = (req, res) => {
|
||||||
|
// const limit = parseInt(req.query.limit) || 10; // Default to 10 items per page
|
||||||
|
// const offset = parseInt(req.query.offset) || 0; // Default to the first page
|
||||||
|
// const q = `SELECT * FROM home_information LIMIT ${limit} OFFSET ${offset}`;
|
||||||
|
|
||||||
|
// db.query(q, (err, data) => {
|
||||||
|
// if (err) {
|
||||||
|
// console.log(err);
|
||||||
|
// return res.status(500).json({ error: "Database query failed" });
|
||||||
|
// }
|
||||||
|
// // Set cache-control headers to prevent caching
|
||||||
|
// res.set('Cache-Control', 'no-store');
|
||||||
|
// return res.json(data);
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
|
||||||
|
// Controller function to get all data from MYSQL table with pagination and total records
|
||||||
export const searchMySQL = (req, res) => {
|
export const searchMySQL = (req, res) => {
|
||||||
const limit = parseInt(req.query.limit) || 10; // Default to 10 items per page
|
const limit = parseInt(req.query.limit) || 10; // Default to 10 items per page
|
||||||
const offset = parseInt(req.query.offset) || 0; // Default to the first page
|
const offset = parseInt(req.query.offset) || 0; // Default to the first page
|
||||||
const q = `SELECT * FROM home_information LIMIT ${limit} OFFSET ${offset}`;
|
const searchQuery = req.query.search || ''; // Get search query if provided
|
||||||
|
|
||||||
db.query(q, (err, data) => {
|
// Query to get total count of records
|
||||||
|
const countQuery = `SELECT COUNT(*) as total FROM home_information WHERE address LIKE ?`;
|
||||||
|
|
||||||
|
// Query to fetch paginated data
|
||||||
|
const dataQuery = `SELECT * FROM home_information WHERE address LIKE ? LIMIT ? OFFSET ?`;
|
||||||
|
|
||||||
|
// Perform count query
|
||||||
|
db.query(countQuery, [`%${searchQuery}%`], (err, countResult) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
return res.status(500).json({ error: "Database query failed" });
|
return res.status(500).json({ error: "Database query failed" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the total count from the query result
|
||||||
|
const totalRecords = countResult[0].total;
|
||||||
|
|
||||||
|
// Perform data query
|
||||||
|
db.query(dataQuery, [`%${searchQuery}%`, limit, offset], (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
console.log(err);
|
||||||
|
return res.status(500).json({ error: "Database query failed" });
|
||||||
|
}
|
||||||
|
|
||||||
// Set cache-control headers to prevent caching
|
// Set cache-control headers to prevent caching
|
||||||
res.set('Cache-Control', 'no-store');
|
res.set('Cache-Control', 'no-store');
|
||||||
return res.json(data);
|
|
||||||
|
// Return both count and paginated data in the response
|
||||||
|
return res.json({
|
||||||
|
data,
|
||||||
|
totalRecords
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -42,8 +42,8 @@
|
||||||
<!-- <script src="https://stackpath.bootstrapcdn.com/bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script> -->
|
<!-- <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 src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|
||||||
<script type="module" crossorigin src="/assets/index-vceAjfBL.js"></script>
|
<script type="module" crossorigin src="/assets/index-Br5i4FR3.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-DPr78kNB.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DlbKQED5.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useCallback } from "react";
|
||||||
|
import { NavLink } from "react-router-dom";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import Navbar from "./Navbar";
|
import Navbar from "./Navbar";
|
||||||
import Footer from "./Footer";
|
import Footer from "./Footer";
|
||||||
|
@ -9,26 +10,82 @@ const SearchMysql = () => {
|
||||||
const [totalRecords, setTotalRecords] = useState(0);
|
const [totalRecords, setTotalRecords] = useState(0);
|
||||||
const [page, setPage] = useState(0); // Page number
|
const [page, setPage] = useState(0); // Page number
|
||||||
const [limit] = useState(10); // Items per page
|
const [limit] = useState(10); // Items per page
|
||||||
|
const [search, setSearch] = useState(""); // Search query state
|
||||||
|
const [loading, setLoading] = useState(false); // Loader state
|
||||||
|
|
||||||
|
// Fetch properties from API
|
||||||
const fetchProperties = async () => {
|
const fetchProperties = async () => {
|
||||||
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await axios.get(
|
const res = await axios.get(
|
||||||
`${import.meta.env.VITE_REACT_APP_SECRET}/mysql/searchmysql`,
|
`${import.meta.env.VITE_REACT_APP_SECRET}/mysql/searchmysql`,
|
||||||
{
|
{
|
||||||
params: { limit, offset: page * limit },
|
params: { limit, offset: page * limit, search },
|
||||||
headers: { "Cache-Control": "no-cache" }, // Disable caching
|
headers: { "Cache-Control": "no-cache" }, // Disable caching
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
setProperties(res.data);
|
setProperties(res.data.data); // Set properties
|
||||||
setTotalRecords(res.data.total); // Get total records from backend
|
setTotalRecords(res.data.totalRecords); // Set total records
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("Error fetching data:", err);
|
console.log("Error fetching data:", err);
|
||||||
|
} finally {
|
||||||
|
setLoading(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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle key press events (Enter)
|
||||||
|
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
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch data when page changes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchProperties();
|
// Fetch default properties when the component mounts or page changes
|
||||||
|
if (search.trim() === "") {
|
||||||
|
fetchProperties(); // Fetch default properties
|
||||||
|
}
|
||||||
}, [page]);
|
}, [page]);
|
||||||
|
|
||||||
const totalPages = Math.ceil(totalRecords / limit);
|
const totalPages = Math.ceil(totalRecords / limit);
|
||||||
|
@ -38,7 +95,6 @@ const SearchMysql = () => {
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<br /> <br /> <br /> <br /> <br /> <br />
|
<br /> <br /> <br /> <br /> <br /> <br />
|
||||||
{/* Display properties */}
|
{/* Display properties */}
|
||||||
{properties.length > 0 ? (
|
|
||||||
<div className="container col-12">
|
<div className="container col-12">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-lg-12 card-margin col-12">
|
<div className="col-lg-12 card-margin col-12">
|
||||||
|
@ -55,6 +111,10 @@ const SearchMysql = () => {
|
||||||
className="form-control"
|
className="form-control"
|
||||||
id="search"
|
id="search"
|
||||||
name="search"
|
name="search"
|
||||||
|
value={search}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
onKeyDown={handleKeyPress}
|
||||||
|
onBlur={handleBlur} // Trigger search on blur
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-lg-1 col-md-3 col-sm-12 p-0"></div>
|
<div className="col-lg-1 col-md-3 col-sm-12 p-0"></div>
|
||||||
|
@ -70,6 +130,9 @@ const SearchMysql = () => {
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<div className="card card-margin">
|
<div className="card card-margin">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
|
{loading ? (
|
||||||
|
<div className="loader">Loading...</div> // Loader component
|
||||||
|
) : (
|
||||||
<div className="row search-body">
|
<div className="row search-body">
|
||||||
<div className="col-lg-12">
|
<div className="col-lg-12">
|
||||||
<div className="search-result col-12">
|
<div className="search-result col-12">
|
||||||
|
@ -82,6 +145,10 @@ const SearchMysql = () => {
|
||||||
<button
|
<button
|
||||||
onClick={() => setPage(page - 1)}
|
onClick={() => setPage(page - 1)}
|
||||||
disabled={page === 0}
|
disabled={page === 0}
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#fda417",
|
||||||
|
border: "#fda417",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Previous
|
Previous
|
||||||
</button>
|
</button>
|
||||||
|
@ -92,54 +159,68 @@ const SearchMysql = () => {
|
||||||
<button
|
<button
|
||||||
onClick={() => setPage(page + 1)}
|
onClick={() => setPage(page + 1)}
|
||||||
disabled={page + 1 >= totalPages}
|
disabled={page + 1 >= totalPages}
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#fda417",
|
||||||
|
border: "#fda417",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Next
|
Next
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="result-body">
|
<div className="result-body">
|
||||||
<div className="table-responsive">
|
<div className="table-responsive">
|
||||||
<table className="table widget-26">
|
<table className="table widget-26">
|
||||||
<tbody>
|
{/* Add table headers */}
|
||||||
{properties.map((property, index) => (
|
<thead>
|
||||||
<div key={index} className="property">
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<th>Details</th>
|
||||||
<div className="widget-26-job-emp-img">
|
<th>Total Living Square Foot</th>
|
||||||
<img
|
<th>Year Built</th>
|
||||||
src="https://bootdey.com/img/Content/avatar/avatar2.png"
|
<th>Cost per Square Foot</th>
|
||||||
alt="Company"
|
</tr>
|
||||||
/>
|
</thead>
|
||||||
</div>
|
<tbody>
|
||||||
</td>
|
{properties.length > 0 ? (
|
||||||
|
properties.map((property, index) => (
|
||||||
|
<tr key={index}>
|
||||||
<td>
|
<td>
|
||||||
<div className="widget-26-job-title">
|
<div className="widget-26-job-title">
|
||||||
<a href="#">{property.address}</a>
|
<NavLink
|
||||||
<p className="m-0">
|
to={`/property/${property.house_id}`}
|
||||||
<span
|
className="link-primary text-decoration-none"
|
||||||
className="employer-name"
|
|
||||||
>
|
>
|
||||||
{property.city}, {property.county}, {property.state}
|
{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>{" "}
|
</span>{" "}
|
||||||
<p className="text-muted m-0">
|
<p className="text-muted m-0">
|
||||||
House Id: {property.house_id}
|
House Id: {property.house_id}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div className="widget-26-job-info">
|
<div className="widget-26-job-info">
|
||||||
<p className="type m-0">Total Living Square foot</p>
|
<p className="m-0">
|
||||||
<p className="text-muted m-0">
|
|
||||||
is{" "}
|
|
||||||
<span className="location">
|
|
||||||
{property.total_living_sqft}
|
{property.total_living_sqft}
|
||||||
</span>
|
</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="widget-26-job-info">
|
||||||
|
<p className="m-0">
|
||||||
|
{property.year_built}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -148,51 +229,26 @@ const SearchMysql = () => {
|
||||||
$ {property.cost_per_sqft}/sqft
|
$ {property.cost_per_sqft}/sqft
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
|
||||||
<div className="widget-26-job-info">
|
|
||||||
<p className="type m-0">Year built</p>
|
|
||||||
<p className="text-muted m-0">
|
|
||||||
|
|
||||||
<span className="location">
|
|
||||||
{property.year_built}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
|
|
||||||
{/* <td>
|
|
||||||
<div className="widget-26-job-starred">
|
|
||||||
<a href="#">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={2}
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
className="feather feather-star"
|
|
||||||
>
|
|
||||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</td> */}
|
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
<hr style={{color: "#fda417"}} />
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan="4">No results found</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
))}
|
|
||||||
{/* Pagination Controls */}
|
{/* Pagination Controls */}
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setPage(page - 1)}
|
onClick={() => setPage(page - 1)}
|
||||||
disabled={page === 0}
|
disabled={page === 0}
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#fda417",
|
||||||
|
border: "#fda417",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Previous
|
Previous
|
||||||
</button>
|
</button>
|
||||||
|
@ -203,26 +259,23 @@ const SearchMysql = () => {
|
||||||
<button
|
<button
|
||||||
onClick={() => setPage(page + 1)}
|
onClick={() => setPage(page + 1)}
|
||||||
disabled={page + 1 >= totalPages}
|
disabled={page + 1 >= totalPages}
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#fda417",
|
||||||
|
border: "#fda417",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Next
|
Next
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p>No properties found.</p>
|
|
||||||
)}
|
)}
|
||||||
{/* Pagination Controls */}
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -37,7 +37,7 @@ body{
|
||||||
margin: 0;
|
margin: 0;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #3c4142;
|
color: #000000;
|
||||||
font-size: 0.9125rem;
|
font-size: 0.9125rem;
|
||||||
color: #3c4142;
|
color: #3c4142;
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ body{
|
||||||
|
|
||||||
.widget-26 .widget-26-job-info p {
|
.widget-26 .widget-26-job-info p {
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
color: #3c4142;
|
color: #000000;
|
||||||
font-size: 0.9125rem;
|
font-size: 0.9125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@ body{
|
||||||
.widget-26 .widget-26-job-salary {
|
.widget-26 .widget-26-job-salary {
|
||||||
min-width: 70px;
|
min-width: 70px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: #3c4142;
|
color: #000000;
|
||||||
font-size: 0.9125rem;
|
font-size: 0.9125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ body{
|
||||||
|
|
||||||
.widget-26 .widget-26-job-category span {
|
.widget-26 .widget-26-job-category span {
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
color: #3c4142;
|
color: #000000;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,7 +170,7 @@ body{
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-body .search-filters .filter-list .title {
|
.search-body .search-filters .filter-list .title {
|
||||||
color: #3c4142;
|
color: #000000;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,7 +183,7 @@ body{
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-body .search-result .result-header .records {
|
.search-body .search-result .result-header .records {
|
||||||
color: #3c4142;
|
color: #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-body .search-result .result-header .result-actions {
|
.search-body .search-result .result-header .result-actions {
|
||||||
|
@ -208,7 +208,7 @@ body{
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-body .search-result .result-header .result-actions .result-sorting select option {
|
.search-body .search-result .result-header .result-actions .result-sorting select option {
|
||||||
color: #3c4142;
|
color: #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) and (max-width: 991.98px) {
|
@media (min-width: 768px) and (max-width: 991.98px) {
|
||||||
|
@ -253,5 +253,32 @@ body{
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
border: 8px solid #f3f3f3;
|
||||||
|
border-top: 8px solid #3498db;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue