diff --git a/README.md b/README.md
index 160b52d..b506b12 100644
--- a/README.md
+++ b/README.md
@@ -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
```
diff --git a/backend/controllers/category.js b/backend/controllers/category.js
index 31776f7..4764dbf 100644
--- a/backend/controllers/category.js
+++ b/backend/controllers/category.js
@@ -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`);
diff --git a/backend/controllers/product.js b/backend/controllers/product.js
index f385270..31ca2ec 100644
--- a/backend/controllers/product.js
+++ b/backend/controllers/product.js
@@ -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!" });
+ }
+};
diff --git a/backend/controllers/transaction.js b/backend/controllers/transaction.js
new file mode 100644
index 0000000..ace31bb
--- /dev/null
+++ b/backend/controllers/transaction.js
@@ -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!" });
+ }
+};
diff --git a/backend/controllers/user.js b/backend/controllers/user.js
index 32aef17..37c5b06 100644
--- a/backend/controllers/user.js
+++ b/backend/controllers/user.js
@@ -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,20 +105,20 @@ 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
const [insertResult] = await db.execute(
`INSERT INTO UserRole (UserID, Client, Admin)
VALUES (LAST_INSERT_ID(), ${data.client || true}, ${
- data.admin || false
- })`,
+ 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!" });
+ }
+};
diff --git a/backend/index.js b/backend/index.js
index 3080f33..b06f1fc 100644
--- a/backend/index.js
+++ b/backend/index.js
@@ -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;
diff --git a/backend/routes/category.js b/backend/routes/category.js
index 425adc6..49ebdcf 100644
--- a/backend/routes/category.js
+++ b/backend/routes/category.js
@@ -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;
diff --git a/backend/routes/product.js b/backend/routes/product.js
index a3c75ba..5862209 100644
--- a/backend/routes/product.js
+++ b/backend/routes/product.js
@@ -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);
diff --git a/backend/routes/transaction.js b/backend/routes/transaction.js
new file mode 100644
index 0000000..0788b96
--- /dev/null
+++ b/backend/routes/transaction.js
@@ -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;
diff --git a/backend/routes/user.js b/backend/routes/user.js
index 1ccbc88..657b793 100644
--- a/backend/routes/user.js
+++ b/backend/routes/user.js
@@ -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;
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 7b44684..3add1d8 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -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",
diff --git a/frontend/package.json b/frontend/package.json
index 8a323b6..35ececa 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -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": {
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index e87b52d..3cd6a94 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -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,96 +788,117 @@ function App() {
return (
-
- {/* Show loading overlay when generating recommendations */}
- {isGeneratingRecommendations &&
}
+ {/* If admin dashboard should be shown */}
+ {showAdminDashboard ? (
+
+
+
+ {/* Single admin route for consolidated dashboard */}
+ } />
+ {/* Any other path in admin mode should go to dashboard */}
+ } />
+
+
+ ) : (
+ /* Normal user interface */
+
+ {/* Show loading overlay when generating recommendations */}
+ {isGeneratingRecommendations &&
}
- {/* Only show navbar when authenticated */}
- {isAuthenticated && (
-
- )}
-
- {/* Public routes */}
- : }
- />
- {/* Protected routes */}
-
-
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
-
-
- }
- />
-
-
-
-
-
- }
- />
-
-
-
-
-
- }
- />
-
-
-
-
-
- }
- />
-
-
-
-
-
- }
- />
- {/* Redirect to login for any unmatched routes */}
- }
- />
-
-
+ {/* Only show navbar when authenticated */}
+ {isAuthenticated && (
+
+ )}
+
+ {/* Public routes */}
+ :
+ }
+ />
+ {/* Protected routes */}
+
+
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+
+
+ }
+ />
+
+
+
+
+
+ }
+ />
+
+
+
+
+
+ }
+ />
+
+
+
+
+
+ }
+ />
+
+
+
+
+
+ }
+ />
+ {/* Redirect to login for any unmatched routes */}
+ }
+ />
+
+
+ )}
);
}
diff --git a/frontend/src/api/admin.js b/frontend/src/api/admin.js
new file mode 100644
index 0000000..7aaa648
--- /dev/null
+++ b/frontend/src/api/admin.js
@@ -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;
diff --git a/frontend/src/components/CategoryForm.jsx b/frontend/src/components/CategoryForm.jsx
new file mode 100644
index 0000000..08d7e86
--- /dev/null
+++ b/frontend/src/components/CategoryForm.jsx
@@ -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 (
+
+ );
+}
diff --git a/frontend/src/components/DashboardNav.jsx b/frontend/src/components/DashboardNav.jsx
new file mode 100644
index 0000000..2013901
--- /dev/null
+++ b/frontend/src/components/DashboardNav.jsx
@@ -0,0 +1,15 @@
+import { FaArrowLeft } from "react-icons/fa";
+
+export default function DashboardNav({ handleCloseAdminDashboard }) {
+ return (
+
+
+
+ );
+}
diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx
index aeceb2c..1c40ad8 100644
--- a/frontend/src/components/Navbar.jsx
+++ b/frontend/src/components/Navbar.jsx
@@ -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 }) => {
{/* User Profile */}
-
+
diff --git a/frontend/src/components/Pagination.jsx b/frontend/src/components/Pagination.jsx
new file mode 100644
index 0000000..ca14f76
--- /dev/null
+++ b/frontend/src/components/Pagination.jsx
@@ -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 (
+ <>
+
+ >
+ );
+}
diff --git a/frontend/src/components/UserDropdown.jsx b/frontend/src/components/UserDropdown.jsx
index e8eed00..cf3cb1f 100644
--- a/frontend/src/components/UserDropdown.jsx
+++ b/frontend/src/components/UserDropdown.jsx
@@ -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
+ {isAdmin ? (
+ {
+ handleShowAdminDashboard();
+ }}
+ >
+
+ Admin
+
+ ) : (
+ <>>
+ )}
+
+ }
+ />
+ {
+ setCategoryRefreshKey((prev) => prev + 1);
+ toggleForm();
+ }}
+ />
+ >
+ ),
+ },
+ ];
+
+ return (
+
+
+
+
+ Admin Dashboard
+
+
+
+ {/* Mobile Tabs */}
+
+
+
+
+ {/* Desktop Tabs */}
+
+ {tabs.map((tab, index) => (
+ setActiveTab(index)}
+ >
+ {tab.icon}
+ {tab.title}
+
+ ))}
+
+
+
+
+ {tabData[tabs[activeTab].key].loaded && tabs[activeTab].component()}
+
+
+
+
+ );
+}
diff --git a/frontend/src/schema.sql b/frontend/src/schema.sql
new file mode 100644
index 0000000..8dafcf0
--- /dev/null
+++ b/frontend/src/schema.sql
@@ -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
+);
diff --git a/mysql-code/Init-Data.sql b/mysql-code/Init-Data.sql
index 20bbb15..3c7dae5 100644
--- a/mysql-code/Init-Data.sql
+++ b/mysql-code/Init-Data.sql
@@ -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
diff --git a/mysql-code/Schema.sql b/mysql-code/Schema.sql
index cadb8b5..e65a325 100644
--- a/mysql-code/Schema.sql
+++ b/mysql-code/Schema.sql
@@ -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