Merge branch 'main' into mann-Branch

This commit is contained in:
Mann Patel
2025-04-21 01:04:03 -06:00
23 changed files with 1282 additions and 165 deletions

View File

@@ -1,25 +1,28 @@
### Some ground rules
1. Add both node_modules from Slient and Server to your ```gitignore``` file
2. Make a brach with the following naming conventionp, refix it with your name ```Your-Name Branch-Name```.
2. Make a brach with the following naming conventionp, prefix it with your name ```Your-Name Branch-Name```.
---
### Frontend
1. `cd frontend` into the dir and then type command
```Bash
#Install the needed lib with the command bellow
npm install
#Start The Server
npm run dev
1. npm install #Installs the needed packages
2. npm run dev #Start The Server
```
---
### Backend
1. `cd backend` into the dir and then type command
```Bash
#Install the needed lib with the command bellow
npm install
#Start The Server
npm run dev
1. npm install #Installs the needed packages
2. npm run dev #Start The Server
```
---
### Recommendation
1. `cd recommendation-engine` into the dir and then type command
```Bash
1. python3 server.py #Start The Server
```
---
### Recommendation system
@@ -34,7 +37,6 @@
2. To Create the DataBase use the command bellow:
```Bash
1. mysql -u root
2. use Marketplace;
3. \. PathToYour/Schema.sql
2. \. PathToYour/Schema.sql
3. \. PathToYour/Init-Data.sql
```

View File

@@ -1,5 +1,51 @@
const db = require("../utils/database");
exports.getAllCategoriesWithPagination = async (req, res) => {
const limit = +req.query?.limit;
const page = +req.query?.page;
const offset = (page - 1) * limit;
try {
const [data, _] = await db.execute(
"SELECT * FROM Category C ORDER BY C.CategoryID ASC LIMIT ? OFFSET ?",
[limit.toString(), offset.toString()],
);
const [result] = await db.execute("SELECT COUNT(*) AS count FROM Category");
const { count: total } = result[0];
return res.json({ data, total });
} catch (error) {
res.json({ error: "Cannot fetch categories from database!" });
}
};
exports.addCategory = async (req, res) => {
const { name } = req.body;
try {
const [result] = await db.execute(
"INSERT INTO Category (Name) VALUES (?)",
[name],
);
res.json({ message: "Adding new category successfully!" });
} catch (error) {
res.json({ error: "Cannot add new category!" });
}
};
exports.removeCategory = async (req, res) => {
const { id } = req.params;
try {
const [result] = await db.execute(
`DELETE FROM Category WHERE CategoryID = ?`,
[id],
);
res.json({ message: "Delete category successfully!" });
} catch (error) {
res.json({ error: "Cannot remove category from database!" });
}
};
exports.getAllCategory = async (req, res) => {
try {
const [data, fields] = await db.execute(`SELECT * FROM Category`);

View File

@@ -345,3 +345,62 @@ exports.getProductById = async (req, res) => {
});
}
};
exports.getProductWithPagination = async (req, res) => {
const limit = +req.query.limit;
const page = +req.query.page;
const offset = (page - 1) * limit;
try {
const [data, fields] = await db.execute(
`
SELECT
P.ProductID,
P.Name AS ProductName,
P.Price,
P.Date AS DateUploaded,
U.Name AS SellerName,
MIN(I.URL) AS ProductImage,
C.Name AS Category
FROM Product P
LEFT JOIN Image_URL I ON P.ProductID = I.ProductID
LEFT JOIN User U ON P.UserID = U.UserID
LEFT JOIN Category C ON P.CategoryID = C.CategoryID
GROUP BY
P.ProductID,
P.Name,
P.Price,
P.Date,
U.Name,
C.Name
ORDER BY P.ProductID ASC
LIMIT ? OFFSET ?
`,
[limit.toString(), offset.toString()],
);
const [result] = await db.execute(
`SELECT COUNT(*) AS totalProd FROM Product`,
);
const { totalProd } = result[0];
return res.json({ totalProd, products: data });
} catch (error) {
res.json({ error: "Error fetching products!" });
}
};
exports.removeAnyProduct = async (req, res) => {
const { id } = req.params;
console.log(id);
try {
const [result] = await db.execute(
`DELETE FROM Product WHERE ProductID = ?`,
[id],
);
res.json({ message: "Delete product successfully!" });
} catch (error) {
res.json({ error: "Cannot remove product from database!" });
}
};

View File

@@ -0,0 +1,40 @@
const db = require("../utils/database");
exports.getTransactionWithPagination = async (req, res) => {
const limit = +req.query?.limit;
const page = +req.query?.page;
const offset = (page - 1) * limit;
try {
const [data, _] = await db.execute(
`SELECT T.TransactionID, DATE_FORMAT(T.Date, '%b-%d-%Y %h:%i %p') as Date, T.PaymentStatus, U.Name as UserName, P.Name as ProductName
FROM Transaction T
LEFT JOIN User U ON T.UserID = U.UserID
LEFT JOIN Product P ON T.ProductID = P.ProductID
ORDER BY T.TransactionID ASC LIMIT ? OFFSET ?`,
[limit.toString(), offset.toString()]
);
const [result] = await db.execute(
"SELECT COUNT(*) AS count FROM Transaction"
);
const { count: total } = result[0];
return res.json({ data, total });
} catch (error) {
res.json({ error: "Cannot fetch transactions from database!" });
}
};
exports.removeTransation = async (req, res) => {
const { id } = req.params;
try {
const [result] = await db.execute(
"DELETE FROM Transaction WHERE TransactionID = ?;",
[id.toString()]
);
return res.json({ message: "Remove transaction successfully!" });
} catch (error) {
return res
.status(500)
.json({ error: "Cannot remove transactions from database!" });
}
};

View File

@@ -13,13 +13,13 @@ exports.sendVerificationCode = async (req, res) => {
// Generate a random 6-digit code
const verificationCode = crypto.randomInt(100000, 999999).toString();
console.log(
`Generated verification code for ${email}: ${verificationCode}`,
`Generated verification code for ${email}: ${verificationCode}`
);
// Check if email already exists in verification table
const [results, fields] = await db.execute(
"SELECT * FROM AuthVerification WHERE Email = ?",
[email],
[email]
);
if (results.length > 0) {
@@ -27,7 +27,7 @@ exports.sendVerificationCode = async (req, res) => {
const [result] = await db.execute(
`UPDATE AuthVerification SET VerificationCode = ?, Authenticated = FALSE, Date = CURRENT_TIMESTAMP
WHERE Email = ?`,
[verificationCode, email],
[verificationCode, email]
);
// Send email and respond
@@ -37,7 +37,7 @@ exports.sendVerificationCode = async (req, res) => {
// Insert new record
const [result] = await db.execute(
"INSERT INTO AuthVerification (Email, VerificationCode, Authenticated) VALUES (?, ?, FALSE)",
[email, verificationCode],
[email, verificationCode]
);
// Send email and respond
await sendVerificationEmail(email, verificationCode);
@@ -62,7 +62,7 @@ exports.verifyCode = async (req, res) => {
// Check verification code
const [results, fields] = await db.execute(
"SELECT * FROM AuthVerification WHERE Email = ? AND VerificationCode = ? AND Authenticated = 0 AND Date > DATE_SUB(NOW(), INTERVAL 15 MINUTE)",
[email, code],
[email, code]
);
if (results.length === 0) {
console.log(`Invalid or expired verification code for email ${email}`);
@@ -76,7 +76,7 @@ exports.verifyCode = async (req, res) => {
// Mark as authenticated
const [result] = await db.execute(
"UPDATE AuthVerification SET Authenticated = TRUE WHERE Email = ?",
[email],
[email]
);
res.json({
success: true,
@@ -95,7 +95,7 @@ exports.completeSignUp = async (req, res) => {
try {
const [results, fields] = await db.execute(
`SELECT * FROM AuthVerification WHERE Email = ? AND Authenticated = 1;`,
[data.email],
[data.email]
);
if (results.length === 0) {
@@ -105,7 +105,7 @@ exports.completeSignUp = async (req, res) => {
// Create the user
const [createResult] = await db.execute(
`INSERT INTO User (Name, Email, UCID, Password, Phone, Address)
VALUES ('${data.name}', '${data.email}', '${data.UCID}', '${data.password}', '${data.phone}', '${data.address}')`,
VALUES ('${data.name}', '${data.email}', '${data.UCID}', '${data.password}', '${data.phone}', '${data.address}')`
);
// Insert role using the user's ID
@@ -113,12 +113,12 @@ exports.completeSignUp = async (req, res) => {
`INSERT INTO UserRole (UserID, Client, Admin)
VALUES (LAST_INSERT_ID(), ${data.client || true}, ${
data.admin || false
})`,
})`
);
// Delete verification record
const [deleteResult] = await db.execute(
`DELETE FROM AuthVerification WHERE Email = '${data.email}'`,
`DELETE FROM AuthVerification WHERE Email = '${data.email}'`
);
res.json({
@@ -310,7 +310,7 @@ exports.deleteUser = async (req, res) => {
// Delete from UserRole first (assuming foreign key constraint)
const [result1] = await db.execute(
"DELETE FROM UserRole WHERE UserID = ?",
[userId],
[userId]
);
// Then delete from User table
@@ -328,3 +328,38 @@ exports.deleteUser = async (req, res) => {
return res.status(500).json({ error: "Could not delete user!" });
}
};
exports.getUsersWithPagination = async (req, res) => {
const limit = +req.query.limit;
const page = +req.query.page;
const offset = (page - 1) * limit;
try {
const [users, fields] = await db.execute(
"SELECT * FROM User LIMIT ? OFFSET ?",
[limit.toString(), offset.toString()]
);
const [result] = await db.execute("SELECT COUNT(*) AS count FROM User");
const { count: total } = result[0];
res.json({ users, total });
} catch (error) {
console.error("Errors: ", error);
return res.status(500).json({ error: "\nCould not fetch users!" });
}
};
exports.isAdmin = async (req, res) => {
const { id } = req.params;
try {
const [result] = await db.execute(
"SELECT R.Admin FROM marketplace.userrole R WHERE R.UserID = ?",
[id]
);
const { Admin } = result[0];
res.json({ isAdmin: Admin });
} catch (error) {
res.json({ error: "Cannot verify admin status!" });
}
};

View File

@@ -9,7 +9,8 @@ const searchRouter = require("./routes/search");
const recommendedRouter = require("./routes/recommendation");
const history = require("./routes/history");
const review = require("./routes/review");
const category = require("./routes/category");
const categoryRouter = require("./routes/category");
const transactionRouter = require("./routes/transaction");
const { generateEmailTransporter } = require("./utils/mail");
const {
@@ -43,7 +44,9 @@ app.use("/api/search", searchRouter);
app.use("/api/engine", recommendedRouter);
app.use("/api/history", history);
app.use("/api/review", review);
app.use("/api/category", category);
app.use("/api/category", categoryRouter);
app.use("/api/transaction", transactionRouter);
app.use("/api/category", categoryRouter);
// Set up a scheduler to run cleanup every hour
clean_up_time = 30 * 60 * 1000;

View File

@@ -1,7 +1,16 @@
const express = require("express");
const { getAllCategory } = require("../controllers/category");
const {
getAllCategoriesWithPagination,
addCategory,
removeCategory,
getAllCategory,
} = require("../controllers/category");
const router = express.Router();
router.get("/getCategories", getAllCategoriesWithPagination);
router.post("/addCategory", addCategory);
router.delete("/:id", removeCategory);
router.get("/", getAllCategory);
module.exports = router;

View File

@@ -7,8 +7,10 @@ const {
getAllProducts,
getProductById,
addProduct,
myProduct,
removeProduct,
removeAnyProduct,
getProductWithPagination,
myProduct,
updateProduct,
} = require("../controllers/product");
const router = express.Router();
@@ -27,6 +29,12 @@ router.post("/delProduct", removeProduct);
router.post("/myProduct", myProduct);
router.post("/addProduct", addProduct);
router.get("/getProduct", getAllProducts);
//Remove product
router.delete("/any/:id", removeAnyProduct);
//Get products with pagination
router.get("/getProductWithPagination", getProductWithPagination);
router.get("/:id", getProductById); // Simplified route
router.put("/update/:productId", updateProduct);

View File

@@ -0,0 +1,12 @@
const express = require("express");
const {
getTransactionWithPagination,
removeTransation,
} = require("../controllers/transaction");
const router = express.Router();
router.get("/getTransactions", getTransactionWithPagination);
router.delete("/:id", removeTransation);
module.exports = router;

View File

@@ -8,6 +8,8 @@ const {
updateUser,
deleteUser,
doLogin,
isAdmin,
getUsersWithPagination,
} = require("../controllers/user");
const router = express.Router();
@@ -36,4 +38,10 @@ router.post("/update", updateUser);
//Delete A uses Data:
router.post("/delete", deleteUser);
//Check admin status
router.get("/isAdmin/:id", isAdmin);
//Fetch user with pagination
router.get("/getUserWithPagination", getUsersWithPagination);
module.exports = router;

View File

@@ -13,6 +13,7 @@
"lucide-react": "^0.477.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-icons": "^5.5.0",
"react-router-dom": "^7.2.0"
},
"devDependencies": {
@@ -4369,6 +4370,15 @@
"react": "^19.0.0"
}
},
"node_modules/react-icons": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
"integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
"license": "MIT",
"peerDependencies": {
"react": "*"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",

View File

@@ -15,6 +15,7 @@
"lucide-react": "^0.477.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-icons": "^5.5.0",
"react-router-dom": "^7.2.0"
},
"devDependencies": {

View File

@@ -4,6 +4,7 @@ import {
Routes,
Route,
Navigate,
useLocation,
} from "react-router-dom";
import Navbar from "./components/Navbar";
import Home from "./pages/Home";
@@ -13,6 +14,9 @@ import Transactions from "./pages/Transactions";
import Favorites from "./pages/Favorites";
import ProductDetail from "./pages/ProductDetail";
import SearchPage from "./pages/SearchPage";
import Dashboard from "./pages/Dashboard"; // The single consolidated dashboard component
import DashboardNav from "./components/DashboardNav";
import { verifyIsAdmin } from "./api/admin";
function App() {
// Authentication state - initialize from localStorage if available
@@ -35,6 +39,18 @@ function App() {
useState(false);
const [recommendations, setRecommendations] = useState([]);
// Admin state
const [isAdmin, setIsAdmin] = useState(false);
const [showAdminDashboard, setShowAdminDashboard] = useState(false);
// Check URL to determine if we're in admin mode
useEffect(() => {
// If URL contains /admin, set showAdminDashboard to true
if (window.location.pathname.includes("/admin")) {
setShowAdminDashboard(true);
}
}, []);
// New verification states
const [verificationStep, setVerificationStep] = useState("initial"); // 'initial', 'code-sent', 'verifying'
const [tempUserData, setTempUserData] = useState(null);
@@ -120,6 +136,28 @@ function App() {
}
};
useEffect(() => {
const userInfo = sessionStorage.getItem("user")
? JSON.parse(sessionStorage.getItem("user"))
: "";
const id = userInfo?.ID;
verifyIsAdmin(id).then((data) => {
setIsAdmin(data.isAdmin);
});
}, [user]);
const handleShowAdminDashboard = () => {
setShowAdminDashboard(true);
// Update URL without reloading page
window.history.pushState({}, "", "/admin");
};
const handleCloseAdminDashboard = () => {
setShowAdminDashboard(false);
// Update URL without reloading page
window.history.pushState({}, "", "/");
};
// Send verification code
const sendVerificationCode = async (userData) => {
try {
@@ -409,6 +447,7 @@ function App() {
setVerificationStep("initial");
setTempUserData(null);
setRecommendations([]);
setShowAdminDashboard(false);
// Clear localStorage
sessionStorage.removeItem("user");
@@ -749,19 +788,39 @@ function App() {
return (
<Router>
{/* If admin dashboard should be shown */}
{showAdminDashboard ? (
<div className="flex">
<DashboardNav handleCloseAdminDashboard={handleCloseAdminDashboard} />
<Routes>
{/* Single admin route for consolidated dashboard */}
<Route path="/admin/*" element={<Dashboard />} />
{/* Any other path in admin mode should go to dashboard */}
<Route path="*" element={<Navigate to="/admin" />} />
</Routes>
</div>
) : (
/* Normal user interface */
<div className="min-h-screen bg-gray-50">
{/* Show loading overlay when generating recommendations */}
{isGeneratingRecommendations && <LoadingOverlay />}
{/* Only show navbar when authenticated */}
{isAuthenticated && (
<Navbar onLogout={handleLogout} userName={user?.name} />
<Navbar
isAdmin={isAdmin}
onLogout={handleLogout}
userName={user?.name}
handleShowAdminDashboard={handleShowAdminDashboard}
/>
)}
<Routes>
{/* Public routes */}
<Route
path="/login"
element={isAuthenticated ? <Navigate to="/" /> : <LoginComponent />}
element={
isAuthenticated ? <Navigate to="/" /> : <LoginComponent />
}
/>
{/* Protected routes */}
<Route
@@ -839,6 +898,7 @@ function App() {
/>
</Routes>
</div>
)}
</Router>
);
}

119
frontend/src/api/admin.js Normal file
View File

@@ -0,0 +1,119 @@
// api.js
import axios from "axios";
const client = axios.create({
baseURL: "http://localhost:3030/api",
});
// Users
export const getUsers = async (page, limit = 10) => {
try {
const { data } = await client.get(
`/user/getUserWithPagination?page=${page}&limit=${limit}`,
);
return { users: data.users, total: data.total };
} catch (error) {
return handleError(error);
}
};
export const removeUser = async (id) => {
try {
const { data } = await client.post(`/user/delete`, { userId: id });
return { message: data.message };
} catch (error) {
return handleError(error);
}
};
export const verifyIsAdmin = async (id) => {
try {
const { data } = await client.get(`/user/isAdmin/${id}`);
return { isAdmin: data.isAdmin };
} catch (error) {
return handleError(error);
}
};
// Products
export const getProducts = async (page, limit = 10) => {
try {
const { data } = await client.get(
`/product/getProductWithPagination?limit=${limit}&page=${page}`,
);
return { products: data.products, total: data.totalProd };
} catch (error) {
return handleError(error);
}
};
export const removeProduct = async (id) => {
try {
const { data } = await client.delete(`/product/any/${id}`);
return { message: data.message };
} catch (error) {
return handleError(error);
}
};
// Categories
export const getCategories = async (page, limit = 10) => {
try {
const { data } = await client.get(
`/category/getCategories?page=${page}&limit=${limit}`,
);
return { data: data.data, total: data.total };
} catch (error) {
return handleError(error);
}
};
export const addCategory = async (name) => {
try {
const { data } = await client.post(`/category/addCategory`, { name });
return { message: data.message };
} catch (error) {
return handleError(error);
}
};
export const removeCategory = async (id) => {
try {
const { data } = await client.delete(`/category/${id}`);
return { message: data.message };
} catch (error) {
return handleError(error);
}
};
// Transactions
export const getTransactions = async (page, limit = 10) => {
try {
const { data } = await client.get(
`/transaction/getTransactions?limit=${limit}&page=${page}`,
);
return { transactions: data.data, total: data.total };
} catch (error) {
return handleError(error);
}
};
export const removeTransaction = async (id) => {
try {
const { data } = await client.delete(`/transaction/${id}`);
return { message: data.message };
} catch (error) {
return handleError(error);
}
};
// Shared Error Handler
const handleError = (error) => {
const { response } = error;
if (response?.data) return response.data;
return { error: error.message || error };
};
// Optional: export client if you want to use it elsewhere
export default client;

View File

@@ -0,0 +1,65 @@
import { useState } from "react";
import { MdAddBox } from "react-icons/md";
import { addCategory } from "../api/admin";
export default function CategoryForm({ visible, onAddCategory }) {
const [category, setCategory] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
if (!category.trim()) {
document.getElementById("noti").innerHTML = "Category name is missing!";
document
.getElementById("noti")
.classList.add("bg-red-200", "text-red-500");
document.getElementById("noti").classList.remove("opacity-0");
return;
}
addCategory(category)
.then((message) => {
document
.getElementById("noti")
.classList.remove("opacity-0", "bg-red-200", "text-red-500");
document
.getElementById("noti")
.classList.add("bg-green-200", "text-green-800");
document.getElementById("noti").innerHTML = `${message.message}`;
setCategory("");
onAddCategory();
})
.catch((err) => {
console.log(err);
});
};
const handleChange = ({ target }) => {
setCategory(target.value);
if (target.value.trim())
document.getElementById("noti").classList.add("opacity-0");
};
if (!visible) return;
return (
<form onSubmit={handleSubmit} action="" className="flex p-2 items-center">
<label htmlFor="category" className="text-green-700">
Category:
</label>
<input
type="text"
className="border border-green-700 ml-2 rounded-sm focus:bg-green-100 text-green-900"
name="category"
id="category"
onChange={handleChange}
value={category}
/>
<button type="submit" className="text-2xl pl-1 text-green-700">
<MdAddBox className="text-3xl" />
</button>
<p
id="noti"
className="text-red-500 bg-red-200 px-2 rounded-sm opacity-0 mx-2"
></p>
</form>
);
}

View File

@@ -0,0 +1,15 @@
import { FaArrowLeft } from "react-icons/fa";
export default function DashboardNav({ handleCloseAdminDashboard }) {
return (
<div className="w-48 min-w-[12rem] bg-gray-100 text-emerald-600 flex flex-col p-4 shadow-md">
<button
onClick={handleCloseAdminDashboard}
className="flex items-center gap-2 text-sm font-medium hover:text-emerald-700 underline underline-offset-4 transition"
>
<FaArrowLeft className="text-xs mt-[1px]" />
Back to User Page
</button>
</div>
);
}

View File

@@ -3,7 +3,7 @@ import { Link, useNavigate } from "react-router-dom";
import UserDropdown from "./UserDropdown";
import { Search, Heart } from "lucide-react";
const Navbar = ({ onLogout, userName }) => {
const Navbar = ({ onLogout, userName, isAdmin, handleShowAdminDashboard }) => {
const [searchQuery, setSearchQuery] = useState("");
const navigate = useNavigate();
@@ -76,7 +76,12 @@ const Navbar = ({ onLogout, userName }) => {
</Link>
{/* User Profile */}
<UserDropdown onLogout={onLogout} userName={userName} />
<UserDropdown
isAdmin={isAdmin}
onLogout={onLogout}
userName={userName}
handleShowAdminDashboard={handleShowAdminDashboard}
/>
</div>
</div>
</div>

View File

@@ -0,0 +1,99 @@
import { useState } from "react";
import { NavLink } from "react-router-dom";
export default function Pagination({ pageNum, onChange }) {
const [currentPage, setCurrentPage] = useState(1);
const pages = [];
for (let i = 1; i <= pageNum; i++) {
pages.push(i);
}
const handleClick = (page) => {
setCurrentPage(page);
onChange(page);
};
const handleTogglePage = (type) => {
let current = currentPage;
if (type == "next")
current = current + 1 <= pageNum ? current + 1 : current;
else current = current - 1 >= 1 ? current - 1 : current;
setCurrentPage(current);
onChange(current);
};
return (
<>
<nav aria-label="Page navigation" className="flex justify-end">
<ul className="flex items-center -space-x-px h-8 text-sm mt-4 pr-0 font-bold">
<li>
<NavLink
onClick={() => {
handleTogglePage("previous");
}}
className=" flex items-center justify-center px-3 h-8 ms-0 leading-tight border-e-0 border-gray-300 rounded-s-lg hover:bg-gray-100 text-white bg-green-700 border border-gray-300 hover:bg-green-600 hover:text-white"
>
<span className="sr-only">Previous</span>
<svg
className="w-2.5 h-2.5 rtl:rotate-180"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 6 10"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M5 1 1 5l4 4"
/>
</svg>
</NavLink>
</li>
{pages.map((page) => (
<li key={page}>
<NavLink
className={`${
currentPage == page ? "bg-green-600" : "bg-green-700"
} +
" flex items-center justify-center px-3 h-8 leading-tight text-white border border-gray-300 hover:bg-green-600 hover:text-white"`}
onClick={() => {
handleClick(page);
}}
>
{page}
</NavLink>
</li>
))}
<li>
<NavLink
onClick={() => {
handleTogglePage("next");
}}
className="flex items-center justify-center px-3 h-8 leading-tight border border-gray-300 rounded-e-lg text-white bg-green-700 border border-gray-300 hover:bg-green-600 hover:text-white"
>
<span className="sr-only">Next</span>
<svg
className="w-2.5 h-2.5 rtl:rotate-180"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 6 10"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="m1 9 4-4-4-4"
/>
</svg>
</NavLink>
</li>
</ul>
</nav>
</>
);
}

View File

@@ -1,8 +1,14 @@
import { useState, useRef, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
import { User, Settings, ShoppingBag, DollarSign, LogOut } from "lucide-react";
import { RiAdminLine } from "react-icons/ri";
const UserDropdown = ({ onLogout, userName }) => {
const UserDropdown = ({
onLogout,
userName,
isAdmin,
handleShowAdminDashboard,
}) => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
const navigate = useNavigate();
@@ -89,6 +95,20 @@ const UserDropdown = ({ onLogout, userName }) => {
Settings
</Link>
{isAdmin ? (
<Link
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
onClick={() => {
handleShowAdminDashboard();
}}
>
<RiAdminLine className="h-4 w-4 mr-2 text-gray-500" />
Admin
</Link>
) : (
<></>
)}
<button
className="flex w-full items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
onClick={handleLogout}

View File

@@ -0,0 +1,362 @@
import { useEffect, useState, useCallback } from "react";
import {
getUsers,
removeUser,
getTransactions,
removeTransaction,
getProducts,
removeProduct,
getCategories,
removeCategory,
} from "../api/admin";
import { MdDelete } from "react-icons/md";
import { IoAddCircleSharp } from "react-icons/io5";
import { FaHome } from "react-icons/fa";
import Pagination from "../components/Pagination";
import CategoryForm from "../components/CategoryForm";
import DashboardNav from "../components/DashboardNav";
import { useNavigate } from "react-router-dom";
// Spinner Component
const Spinner = () => (
<div className="flex justify-center items-center h-40 w-full">
<div className="w-12 h-12 border-4 border-green-500 border-dashed rounded-full animate-spin"></div>
</div>
);
// Empty State Component
const EmptyState = () => (
<div className="flex flex-col items-center justify-center bg-gray-50 py-10 rounded-lg w-full">
<svg
className="w-16 h-16 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
></path>
</svg>
<p className="mt-4 text-lg font-medium text-gray-600">No data found</p>
<p className="mt-1 text-sm text-gray-500">
No records are currently available.
</p>
</div>
);
// Generic Dashboard Component
const Dashboard = ({
fetchDataFn,
deleteFn,
columns,
idKey,
refreshKey = 0,
headerAction = null,
}) => {
const navigate = useNavigate();
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const [loading, setLoading] = useState(true);
const pageLimit = 10;
const fetchItems = useCallback(
(page = 1, limit = 10) => {
setLoading(true);
fetchDataFn(page, limit)
.then((res) => {
const data =
res.users || res.products || res.transactions || res.data || [];
setItems(data);
setTotal(res.total);
})
.catch((error) => {
console.error("Error fetching data:", error);
setItems([]);
setTotal(0);
})
.finally(() => setLoading(false));
},
[fetchDataFn],
);
const handleRemove = (id) => {
if (window.confirm("Are you sure you want to delete this item?")) {
setLoading(true);
deleteFn(id)
.then(() => fetchItems(currentPage))
.catch((error) => {
console.error("Error removing item:", error);
setLoading(false);
});
}
};
useEffect(() => {
fetchItems(currentPage);
}, [fetchItems, currentPage, refreshKey]);
const onChangePage = (page) => {
setCurrentPage(page);
};
if (loading) return <Spinner />;
return (
<div className="w-full mt-6">
<div className="flex flex-col md:flex-row justify-between items-center mb-4 w-full">
<h2 className="text-xl font-semibold text-gray-700 mb-2 md:mb-0">
Total: <span className="text-green-600">{total}</span>
</h2>
{headerAction && <div>{headerAction}</div>}
</div>
{items.length > 0 ? (
<div className="w-full overflow-x-auto bg-white rounded-lg shadow">
<table className="min-w-full divide-y divide-gray-200 table-fixed">
<thead className="bg-green-600">
<tr>
{columns.map((col) => (
<th
key={col.label}
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-white uppercase tracking-wider"
>
{col.label}
</th>
))}
<th
scope="col"
className="px-6 py-3 text-center text-xs font-medium text-white uppercase tracking-wider w-20"
>
Action
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{items.map((item) => (
<tr
key={item[idKey]}
className="hover:bg-gray-50 transition-colors"
>
{columns.map((col) => (
<td
key={`${item[idKey]}-${col.key}`}
className="px-6 py-4 whitespace-nowrap text-sm text-gray-700 overflow-hidden text-ellipsis"
>
{item[col.key] || "—"}
</td>
))}
<td className="px-6 py-4 whitespace-nowrap text-center text-sm font-medium">
<button
onClick={() => handleRemove(item[idKey])}
className="text-red-500 hover:text-red-700 transition-colors"
title="Delete"
>
<MdDelete size={20} />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<EmptyState />
)}
{total > pageLimit && (
<div className="mt-4 w-full">
<Pagination
pageNum={Math.ceil(total / pageLimit)}
onChange={onChangePage}
/>
</div>
)}
</div>
);
};
// Main Admin Tabs
export default function AdminDashboardTabs() {
const [activeTab, setActiveTab] = useState(0);
const [tabData, setTabData] = useState({
users: { loaded: false, data: null },
products: { loaded: false, data: null },
transactions: { loaded: false, data: null },
categories: { loaded: false, data: null },
});
const [visible, setVisible] = useState(false);
const [categoryRefreshKey, setCategoryRefreshKey] = useState(0);
const toggleForm = () => setVisible((v) => !v);
// Preload all tab data
useEffect(() => {
const tabKeys = ["users", "products", "transactions", "categories"];
const loadTabData = async (index) => {
if (index === tabKeys.length) return;
setTabData((prev) => ({
...prev,
[tabKeys[index]]: { ...prev[tabKeys[index]], loaded: true },
}));
// Load next tab after a short delay
setTimeout(() => loadTabData(index + 1), 100);
};
loadTabData(0);
}, []);
const tabs = [
{
title: "Users",
icon: "👥",
key: "users",
component: () => (
<Dashboard
fetchDataFn={getUsers}
deleteFn={removeUser}
idKey="UserID"
columns={[
{ label: "ID", key: "UserID" },
{ label: "UCID", key: "UCID" },
{ label: "Name", key: "Name" },
{ label: "Email", key: "Email" },
{ label: "Phone", key: "Phone" },
{ label: "Address", key: "Address" },
]}
/>
),
},
{
title: "Products",
icon: "📦",
key: "products",
component: () => (
<Dashboard
fetchDataFn={getProducts}
deleteFn={removeProduct}
idKey="ProductID"
columns={[
{ label: "ID", key: "ProductID" },
{ label: "Name", key: "ProductName" },
{ label: "Price", key: "Price" },
{ label: "Category", key: "Category" },
{ label: "Seller", key: "SellerName" },
]}
/>
),
},
{
title: "Transactions",
icon: "💰",
key: "transactions",
component: () => (
<Dashboard
fetchDataFn={getTransactions}
deleteFn={removeTransaction}
idKey="TransactionID"
columns={[
{ label: "ID", key: "TransactionID" },
{ label: "User", key: "UserName" },
{ label: "Product", key: "ProductName" },
{ label: "Date", key: "Date" },
{ label: "Status", key: "PaymentStatus" },
]}
/>
),
},
{
title: "Categories",
icon: "🏷️",
key: "categories",
component: () => (
<>
<Dashboard
fetchDataFn={getCategories}
deleteFn={removeCategory}
idKey="CategoryID"
columns={[
{ label: "ID", key: "CategoryID" },
{ label: "Name", key: "Name" },
]}
refreshKey={categoryRefreshKey}
headerAction={
<button
onClick={toggleForm}
className="flex items-center bg-green-500 rounded-md px-4 py-2 text-white text-sm hover:bg-green-600 transition-colors font-medium shadow"
>
<IoAddCircleSharp className="mr-1" size={18} />
Add Category
</button>
}
/>
<CategoryForm
visible={visible}
onAddCategory={() => {
setCategoryRefreshKey((prev) => prev + 1);
toggleForm();
}}
/>
</>
),
},
];
return (
<div className="w-full min-h-screen bg-gray-50">
<div className="w-full px-4 py-8">
<div className="mb-8 flex justify-between items-center w-full">
<h1 className="text-2xl md:text-3xl font-bold text-gray-800">
Admin Dashboard
</h1>
</div>
{/* Mobile Tabs */}
<div className="md:hidden w-full mb-4">
<select
className="w-full rounded-md border-gray-300 shadow-sm focus:border-green-500 focus:ring focus:ring-green-500 focus:ring-opacity-50 p-2"
value={activeTab}
onChange={(e) => setActiveTab(parseInt(e.target.value))}
>
{tabs.map((tab, index) => (
<option key={tab.key} value={index}>
{tab.icon} {tab.title}
</option>
))}
</select>
</div>
{/* Desktop Tabs */}
<div className="hidden md:flex space-x-1 border-b border-gray-200 w-full overflow-x-auto">
{tabs.map((tab, index) => (
<button
key={tab.key}
className={`px-6 py-3 font-medium text-sm rounded-t-lg transition-colors ${
index === activeTab
? "text-green-700 bg-white border-l border-t border-r border-gray-200 border-b-0"
: "text-gray-600 hover:text-green-700 bg-gray-50"
}`}
onClick={() => setActiveTab(index)}
>
<span className="inline-block mr-2">{tab.icon}</span>
{tab.title}
</button>
))}
</div>
<div className="bg-white p-4 md:p-6 rounded-lg shadow-sm border border-gray-200 mt-4 w-full">
<div className="w-full">
{tabData[tabs[activeTab].key].loaded && tabs[activeTab].component()}
</div>
</div>
</div>
</div>
);
}

124
frontend/src/schema.sql Normal file
View File

@@ -0,0 +1,124 @@
-- MySql Version 9.2.0
CREATE DATABASE Marketplace;
USE Marketplace;
-- User Entity
CREATE TABLE User (
UserID INT AUTO_INCREMENT PRIMARY KEY,
Name VARCHAR(100) NOT NULL,
Email VARCHAR(100) UNIQUE NOT NULL,
UCID VARCHAR(20) UNIQUE NOT NULL,
Password VARCHAR(255) NOT NULL,
Phone VARCHAR(20),
Address VARCHAR(255)
);
CREATE TABLE UserRole (
UserID INT,
Client BOOLEAN DEFAULT True,
Admin BOOLEAN DEFAULT FALSE,
PRIMARY KEY (UserID),
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE
);
-- Category Entity (must be created before Product or else error)
CREATE TABLE Category (
CategoryID INT AUTO_INCREMENT PRIMARY KEY,
Name VARCHAR(255) NOT NULL
);
-- Product Entity
CREATE TABLE Product (
ProductID INT AUTO_INCREMENT PRIMARY KEY,
Name VARCHAR(255) NOT NULL,
Price DECIMAL(10, 2) NOT NULL,
StockQuantity INT,
UserID INT,
Description TEXT,
CategoryID INT NOT NULL,
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID) ON DELETE SET NULL
);
-- Fixed Image_URL table
CREATE TABLE Image_URL (
URL VARCHAR(255),
ProductID INT,
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE
);
-- Fixed Review Entity (Many-to-One with User, Many-to-One with Product)
CREATE TABLE Review (
ReviewID INT AUTO_INCREMENT PRIMARY KEY,
UserID INT,
ProductID INT,
Comment TEXT,
Rating INT CHECK (
Rating >= 1
AND Rating <= 5
),
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE
);
-- Transaction Entity (Many-to-One with User, Many-to-One with Product)
CREATE TABLE Transaction (
TransactionID INT PRIMARY KEY,
UserID INT,
ProductID INT,
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
PaymentStatus VARCHAR(50),
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE
);
-- Recommendation Entity (Many-to-One with User, Many-to-One with Product)
CREATE TABLE Recommendation (
RecommendationID_PK INT AUTO_INCREMENT PRIMARY KEY,
UserID INT,
RecommendedProductID INT,
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
FOREIGN KEY (RecommendedProductID) REFERENCES Product (ProductID) ON DELETE CASCADE
);
-- History Entity (Many-to-One with User, Many-to-One with Product)
CREATE TABLE History (
HistoryID INT AUTO_INCREMENT PRIMARY KEY,
UserID INT,
ProductID INT,
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE
);
-- Favorites Entity (Many-to-One with User, Many-to-One with Product)
CREATE TABLE Favorites (
FavoriteID INT AUTO_INCREMENT PRIMARY KEY,
UserID INT,
ProductID INT,
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE,
UNIQUE (UserID, ProductID)
);
-- Product-Category Junction Table (Many-to-Many)
CREATE TABLE Product_Category (
ProductID INT,
CategoryID INT,
PRIMARY KEY (ProductID, CategoryID),
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE,
FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID) ON DELETE CASCADE
);
-- Login Authentication table
CREATE TABLE AuthVerification (
UserID INT AUTO_INCREMENT PRIMARY KEY,
Email VARCHAR(100) UNIQUE NOT NULL,
VerificationCode VARCHAR(6) NOT NULL,
Authenticated BOOLEAN DEFAULT FALSE,
Date DATETIME DEFAULT CURRENT_TIMESTAMP
);

View File

@@ -67,40 +67,42 @@ VALUES
(1, TRUE, TRUE),
(2, TRUE, FALSE);
-- Insert Categories
-- Insert Categories
INSERT INTO
Category (CategoryID, Name)
Category (Name)
VALUES
(1, 'Textbooks'),
(2, 'Electronics'),
(3, 'Furniture'),
(4, 'Clothing'),
(5, 'Sports Equipment'),
(6, 'Musical Instruments'),
(7, 'Art Supplies'),
(8, 'Kitchen Appliances'),
(9, 'Gaming'),
(10, 'Bicycles'),
(11, 'Computer Accessories'),
(12, 'Stationery'),
(13, 'Fitness Equipment'),
(14, 'Winter Sports'),
(15, 'Lab Equipment'),
(16, 'Camping Gear'),
(17, 'School Supplies'),
(18, 'Office Furniture'),
(19, 'Books (Non-textbook)'),
(20, 'Math & Science Resources'),
(21, 'Engineering Tools'),
(22, 'Backpacks & Bags'),
(23, 'Audio Equipment'),
(24, 'Dorm Essentials'),
(25, 'Smartphones & Tablets'),
(26, 'Winter Clothing'),
(27, 'Photography Equipment'),
(28, 'Event Tickets'),
(29, 'Software Licenses'),
(30, 'Transportation (Car Pool)');
('Textbooks'),
('Electronics'),
('Furniture'),
('Clothing'),
('Sports Equipment'),
('Musical Instruments'),
('Art Supplies'),
('Kitchen Appliances'),
('Gaming'),
('Bicycles'),
('Computer Accessories'),
('Stationery'),
('Fitness Equipment'),
('Winter Sports'),
('Lab Equipment'),
('Camping Gear'),
('School Supplies'),
('Office Furniture'),
('Books (-textbook)'),
('Math & Science Resources'),
('Engineering Tools'),
('Backpacks & Bags'),
('Audio Equipment'),
('Dorm Essentials'),
('Smartphones & Tablets'),
('Winter Clothing'),
('Photography Equipment'),
('Event Tickets'),
('Software Licenses'),
('Transportation (Car Pool)'),
('Other');
-- Insert Products
INSERT INTO

View File

@@ -24,11 +24,10 @@ CREATE TABLE UserRole (
-- Category Entity (must be created before Product or else error)
CREATE TABLE Category (
CategoryID INT PRIMARY KEY,
CategoryID INT AUTO_INCREMENT PRIMARY KEY,
Name VARCHAR(255) NOT NULL
);
-- Product Entity
CREATE TABLE Product (
ProductID INT AUTO_INCREMENT PRIMARY KEY,
Name VARCHAR(255) NOT NULL,
@@ -42,11 +41,25 @@ CREATE TABLE Product (
FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID)
);
-- Product Entity
CREATE TABLE Product (
ProductID INT AUTO_INCREMENT PRIMARY KEY,
Name VARCHAR(255) NOT NULL,
Price DECIMAL(10, 2) NOT NULL,
StockQuantity INT,
UserID INT,
Description TEXT,
CategoryID INT,
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE SET NULL,
FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID)
);
-- Fixed Image_URL table
CREATE TABLE Image_URL (
URL VARCHAR(255),
ProductID INT,
FOREIGN KEY (ProductID) REFERENCES Product (ProductID)
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE
);
-- Fixed Review Entity (Many-to-One with User, Many-to-One with Product)
@@ -60,8 +73,8 @@ CREATE TABLE Review (
AND Rating <= 5
),
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID),
FOREIGN KEY (ProductID) REFERENCES Product (ProductID)
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE SET NULL,
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE
);
-- Transaction Entity (Many-to-One with User, Many-to-One with Product)
@@ -71,8 +84,8 @@ CREATE TABLE Transaction (
ProductID INT,
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
PaymentStatus VARCHAR(50),
FOREIGN KEY (UserID) REFERENCES User (UserID),
FOREIGN KEY (ProductID) REFERENCES Product (ProductID)
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE SET NULL
);
-- Recommendation Entity (Many-to-One with User, Many-to-One with Product)
@@ -81,8 +94,8 @@ CREATE TABLE Recommendation (
UserID INT,
RecommendedProductID INT,
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID),
FOREIGN KEY (RecommendedProductID) REFERENCES Product (ProductID)
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
FOREIGN KEY (RecommendedProductID) REFERENCES Product (ProductID) ON DELETE CASCADE
);
-- History Entity (Many-to-One with User, Many-to-One with Product)
@@ -91,8 +104,8 @@ CREATE TABLE History (
UserID INT,
ProductID INT,
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID),
FOREIGN KEY (ProductID) REFERENCES Product (ProductID)
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE
);
-- Favorites Entity (Many-to-One with User, Many-to-One with Product)
@@ -100,8 +113,8 @@ CREATE TABLE Favorites (
FavoriteID INT AUTO_INCREMENT PRIMARY KEY,
UserID INT,
ProductID INT,
FOREIGN KEY (UserID) REFERENCES User (UserID),
FOREIGN KEY (ProductID) REFERENCES Product (ProductID),
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE,
UNIQUE (UserID, ProductID)
);
@@ -110,8 +123,8 @@ CREATE TABLE Product_Category (
ProductID INT,
CategoryID INT,
PRIMARY KEY (ProductID, CategoryID),
FOREIGN KEY (ProductID) REFERENCES Product (ProductID),
FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID)
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE,
FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID) ON DELETE CASCADE
);
-- Login Authentication table