From 5b4332a8478f158721b9322a6386217f899f38f3 Mon Sep 17 00:00:00 2001 From: Mann Patel <130435633+MannPatel0@users.noreply.github.com> Date: Fri, 18 Apr 2025 11:29:06 -0600 Subject: [PATCH 1/8] Update README.md --- README.md | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 55e9604..33fd9ff 100644 --- a/README.md +++ b/README.md @@ -6,20 +6,16 @@ ### 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 ``` --- @@ -28,7 +24,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 ``` From fa43c91cc547c094e053a2ea3b5d4458af8bd34b Mon Sep 17 00:00:00 2001 From: Mann Patel <130435633+MannPatel0@users.noreply.github.com> Date: Fri, 18 Apr 2025 11:29:39 -0600 Subject: [PATCH 2/8] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 33fd9ff..235b707 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ### 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 From 47786b04f45a39c2baad2ead8d511fa0c5446629 Mon Sep 17 00:00:00 2001 From: Mann Patel <130435633+MannPatel0@users.noreply.github.com> Date: Fri, 18 Apr 2025 11:30:40 -0600 Subject: [PATCH 3/8] Update README.md --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 235b707..adaa015 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,13 @@ ``` --- +### Recommendation +1. `cd recommendation-engine` into the dir and then type command +```Bash + 1. python3 server.py #Start The Server +``` +--- + ### Database 1. MySql Version 9.2.0 2. To Create the DataBase use the command bellow: From 7a2250369e721684850eae453e3586ed7a086907 Mon Sep 17 00:00:00 2001 From: estherdev03 Date: Sun, 20 Apr 2025 07:48:20 -0600 Subject: [PATCH 4/8] Finish admin dashboard and update sql code --- backend/controllers/category.js | 47 ++++++++++++++++++++++ backend/controllers/product.js | 71 ++++++++++++++++++++++++++++++--- backend/controllers/user.js | 59 +++++++++++++++++++++------ backend/index.js | 5 ++- backend/routes/category.js | 14 +++++++ backend/routes/product.js | 8 ++++ backend/routes/user.js | 8 ++++ backend/utils/database.js | 1 + 8 files changed, 193 insertions(+), 20 deletions(-) create mode 100644 backend/controllers/category.js create mode 100644 backend/routes/category.js diff --git a/backend/controllers/category.js b/backend/controllers/category.js new file mode 100644 index 0000000..90cbd94 --- /dev/null +++ b/backend/controllers/category.js @@ -0,0 +1,47 @@ +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!" }); + } +}; diff --git a/backend/controllers/product.js b/backend/controllers/product.js index 569e4c6..a606fa2 100644 --- a/backend/controllers/product.js +++ b/backend/controllers/product.js @@ -6,7 +6,7 @@ exports.addProduct = async (req, res) => { try { const [result] = await db.execute( `INSERT INTO Product (Name, Price, StockQuantity, UserID, Description, CategoryID) VALUES (?, ?, ?, ?, ?, ?)`, - [name, price, qty, userID, description, category], + [name, price, qty, userID, description, category] ); const productID = result.insertId; @@ -15,7 +15,7 @@ exports.addProduct = async (req, res) => { db.execute(`INSERT INTO Image_URL (URL, ProductID) VALUES (?, ?)`, [ imagePath, productID, - ]), + ]) ); await Promise.all(imageInsertPromises); //perallel @@ -39,7 +39,7 @@ exports.addFavorite = async (req, res) => { // Use parameterized query to prevent SQL injection const [result] = await db.execute( `INSERT INTO Favorites (UserID, ProductID) VALUES (?, ?)`, - [userID, productID], + [userID, productID] ); res.json({ @@ -59,7 +59,7 @@ exports.removeFavorite = async (req, res) => { // Use parameterized query to prevent SQL injection const [result] = await db.execute( `DELETE FROM Favorites WHERE UserID = ? AND ProductID = ?`, - [userID, productID], + [userID, productID] ); res.json({ @@ -103,7 +103,7 @@ exports.getFavorites = async (req, res) => { p.Date, u.Name; `, - [userID], + [userID] ); res.json({ @@ -168,7 +168,7 @@ exports.getProductById = async (req, res) => { JOIN User U ON p.UserID = U.UserID WHERE p.ProductID = ? `, - [id], + [id] ); // Log raw data for debugging @@ -211,6 +211,65 @@ 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.removeProduct = async (req, res) => { + const { id } = req.params; + + 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!" }); + } +}; + // db_con.query( // "SELECT ProductID FROM product WHERE ProductID = ?", // [productID], 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 7afa3fe..5ba3b27 100644 --- a/backend/index.js +++ b/backend/index.js @@ -9,6 +9,7 @@ const searchRouter = require("./routes/search"); const recommendedRouter = require("./routes/recommendation"); const history = require("./routes/history"); const review = require("./routes/review"); +const categoryRouter = require("./routes/category"); const { generateEmailTransporter } = require("./utils/mail"); const { @@ -42,10 +43,10 @@ app.use("/api/search", searchRouter); app.use("/api/engine", recommendedRouter); app.use("/api/history", history); app.use("/api/review", review); - +app.use("/api/category", categoryRouter); // Set up a scheduler to run cleanup every hour -clean_up_time = 30*60*1000; +clean_up_time = 30 * 60 * 1000; setInterval(cleanupExpiredCodes, clean_up_time); app.listen(3030, () => { diff --git a/backend/routes/category.js b/backend/routes/category.js new file mode 100644 index 0000000..9a04a08 --- /dev/null +++ b/backend/routes/category.js @@ -0,0 +1,14 @@ +const express = require("express"); +const { + getAllCategoriesWithPagination, + addCategory, + removeCategory, +} = require("../controllers/category"); + +const router = express.Router(); + +router.get("/getCategories", getAllCategoriesWithPagination); +router.post("/addCategory", addCategory); +router.delete("/:id", removeCategory); + +module.exports = router; diff --git a/backend/routes/product.js b/backend/routes/product.js index 944e63b..4b59e25 100644 --- a/backend/routes/product.js +++ b/backend/routes/product.js @@ -7,6 +7,8 @@ const { getAllProducts, getProductById, addProduct, + removeProduct, + getProductWithPagination, } = require("../controllers/product"); const router = express.Router(); @@ -22,6 +24,12 @@ router.post("/delFavorite", removeFavorite); router.post("/addProduct", addProduct); router.get("/getProduct", getAllProducts); + +//Remove product +router.delete("/:id", removeProduct); +//Get products with pagination +router.get("/getProductWithPagination", getProductWithPagination); + router.get("/:id", getProductById); // Simplified route 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/backend/utils/database.js b/backend/utils/database.js index 6e75c3a..643b1f9 100644 --- a/backend/utils/database.js +++ b/backend/utils/database.js @@ -4,6 +4,7 @@ const pool = mysql.createPool({ host: "localhost", user: "root", database: "Marketplace", + password: "12345678", }); module.exports = pool.promise(); From 26cd50ab6fcece1726dafe41967af83c58efc94b Mon Sep 17 00:00:00 2001 From: estherdev03 Date: Sun, 20 Apr 2025 07:50:57 -0600 Subject: [PATCH 5/8] Finish admin dashboard and update sql code --- frontend/package-lock.json | 10 +++ frontend/package.json | 1 + frontend/src/App.jsx | 64 +++++++++++++-- frontend/src/api/admin.js | 96 +++++++++++++++++++++++ frontend/src/api/client.js | 3 + frontend/src/components/CategoryForm.jsx | 65 ++++++++++++++++ frontend/src/components/DashboardNav.jsx | 83 ++++++++++++++++++++ frontend/src/components/Navbar.jsx | 9 ++- frontend/src/components/Pagination.jsx | 99 ++++++++++++++++++++++++ frontend/src/components/UserDropdown.jsx | 22 +++++- frontend/src/pages/CategoryDashboard.jsx | 94 ++++++++++++++++++++++ frontend/src/pages/Dashboard.jsx | 7 ++ frontend/src/pages/ProductDashboard.jsx | 83 ++++++++++++++++++++ frontend/src/pages/UserDashboard.jsx | 91 ++++++++++++++++++++++ mysql-code/Init-Data.sql | 64 +++++++-------- mysql-code/Schema.sql | 32 ++++---- 16 files changed, 766 insertions(+), 57 deletions(-) create mode 100644 frontend/src/api/admin.js create mode 100644 frontend/src/api/client.js create mode 100644 frontend/src/components/CategoryForm.jsx create mode 100644 frontend/src/components/DashboardNav.jsx create mode 100644 frontend/src/components/Pagination.jsx create mode 100644 frontend/src/pages/CategoryDashboard.jsx create mode 100644 frontend/src/pages/Dashboard.jsx create mode 100644 frontend/src/pages/ProductDashboard.jsx create mode 100644 frontend/src/pages/UserDashboard.jsx 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 4b88b70..eefdb63 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -13,6 +13,12 @@ import Transactions from "./pages/Transactions"; import Favorites from "./pages/Favorites"; import ProductDetail from "./pages/ProductDetail"; import SearchPage from "./pages/SearchPage"; // Make sure to import the SearchPage +import Dashboard from "./pages/Dashboard"; +import UserDashboard from "./pages/UserDashboard"; +import ProductDashboard from "./pages/ProductDashboard"; +import DashboardNav from "./components/DashboardNav"; +import CategoryDashboard from "./pages/CategoryDashboard"; +import { verifyIsAdmin } from "./api/admin"; function App() { // Authentication state - initialize from localStorage if available @@ -56,6 +62,26 @@ function App() { sendSessionDataToServer(); }, []); + const [isAdmin, setIsAdmin] = useState(false); + const [showAdminDashboard, setShowAdminDashboard] = useState(false); + + 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); + }; + const handleCloseAdminDashboard = () => { + setShowAdminDashboard(false); + }; + // Send verification code const sendVerificationCode = async (userData) => { try { @@ -76,7 +102,7 @@ function App() { email: userData.email, // Add any other required fields }), - }, + } ); if (!response.ok) { @@ -125,7 +151,7 @@ function App() { email: tempUserData.email, code: code, }), - }, + } ); if (!response.ok) { @@ -169,7 +195,7 @@ function App() { "Content-Type": "application/json", }, body: JSON.stringify(userData), - }, + } ); if (!response.ok) { @@ -275,7 +301,7 @@ function App() { email: formValues.email, password: formValues.password, }), - }, + } ); if (!response.ok) { @@ -580,8 +606,8 @@ function App() { {isLoading ? "Please wait..." : isSignUp - ? "Create Account" - : "Sign In"} + ? "Create Account" + : "Sign In"} @@ -672,12 +698,36 @@ function App() { return children; }; + // If user is admin, show admin naviagtion + if (showAdminDashboard) { + return ( + +
+ + + {/* Admin routes */} + } /> + } /> + } /> + } /> + } /> + +
+
+ ); + } + return (
{/* Only show navbar when authenticated */} {isAuthenticated && ( - + )} {/* Public routes */} diff --git a/frontend/src/api/admin.js b/frontend/src/api/admin.js new file mode 100644 index 0000000..f2728c9 --- /dev/null +++ b/frontend/src/api/admin.js @@ -0,0 +1,96 @@ +import client from "./client"; + +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) { + const { response } = error; + if (response?.data) return response.data; + return { error: error.message || error }; + } +}; + +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) { + const { response } = error; + if (response?.data) return response.data; + return { error: error.message || error }; + } +}; + +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) { + const { response } = error; + if (response?.data) return response.data; + return { error: error.message || error }; + } +}; + +export const addCategory = async (name) => { + try { + const { data } = await client.post(`/category/addCategory`, { name: name }); + return { message: data.message }; + } catch (error) { + const { response } = error; + if (response?.data) return response.data; + return { error: error.message || error }; + } +}; + +export const removeCategory = async (id) => { + try { + const { data } = await client.delete(`/category/${id}`); + return { message: data.message }; + } catch (error) { + const { response } = error; + if (response?.data) return response.data; + return { error: error.message || error }; + } +}; + +export const removeUser = async (id) => { + try { + const { data } = await client.post(`/user/delete`, { userId: id }); + return { message: data.message }; + } catch (error) { + const { response } = error; + if (response?.data) return response.data; + return { error: error.message || error }; + } +}; + +export const removeProduct = async (id) => { + try { + const { data } = await client.delete(`/product/${id}`); + return { message: data.message }; + } catch (error) { + const { response } = error; + if (response?.data) return response.data; + return { error: error.message || error }; + } +}; + +export const verifyIsAdmin = async (id) => { + try { + const { data } = await client.get(`/user/isAdmin/${id}`); + return { isAdmin: data.isAdmin }; + } catch (error) { + const { response } = error; + if (response?.data) return response.data; + return { error: error.message || error }; + } +}; diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js new file mode 100644 index 0000000..bcb4831 --- /dev/null +++ b/frontend/src/api/client.js @@ -0,0 +1,3 @@ +import axios from "axios"; +const client = axios.create({ baseURL: "http://localhost:3030/api" }); +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..75e287c --- /dev/null +++ b/frontend/src/components/DashboardNav.jsx @@ -0,0 +1,83 @@ +import { Link, NavLink } from "react-router-dom"; +import { FaUserTag } from "react-icons/fa"; +import { FaBoxArchive } from "react-icons/fa6"; +import { MdOutlineCategory } from "react-icons/md"; +import { FaArrowLeft } from "react-icons/fa"; + +export default function DashboardNav({ handleCloseAdminDashboard }) { + const handleClick = () => { + handleCloseAdminDashboard(); + }; + + return ( +
+
+
    +
  • +
    + + Campus Plug + + Campus Plug + + +
    +
  • +
  • + + (isActive + ? " text-green-400" + : "text-white transition-all hover:text-green-200") + + " flex items-center px-5 text-lg pt-5 " + } + > + + Users + +
  • +
  • + + (isActive + ? "text-green-400" + : "text-white transition-all hover:text-green-200") + + " flex items-center px-5 text-lg pt-5" + } + > + + Products + +
  • +
  • + + (isActive + ? "text-green-400" + : "text-white transition-all hover:text-green-200") + + " flex items-center px-5 text-lg pt-5" + } + > + + Categories + +
  • +
+
+ + Go back to user page +
+
+
+ ); +} 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 + + ) : ( + <> + )} + + + + + + + + + + + + {categories.map((category) => ( + + + + + + ))} + +
CategoryIDNameAction
{category.CategoryID}{category.Name} + { + handleRemove(category.CategoryID); + }} + className="hover:text-red-600 cursor-pointer transition-all text-xl" + /> +
+ + + ); +} diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx new file mode 100644 index 0000000..dbcc762 --- /dev/null +++ b/frontend/src/pages/Dashboard.jsx @@ -0,0 +1,7 @@ +export default function Dashboard() { + return ( +
+ Welcome to admin dashboard +
+ ); +} diff --git a/frontend/src/pages/ProductDashboard.jsx b/frontend/src/pages/ProductDashboard.jsx new file mode 100644 index 0000000..c3ef4d2 --- /dev/null +++ b/frontend/src/pages/ProductDashboard.jsx @@ -0,0 +1,83 @@ +import { useEffect, useState } from "react"; +import { getProducts, removeProduct } from "../api/admin"; +import { MdDelete } from "react-icons/md"; +import Pagination from "../components/Pagination"; + +export default function ProductDashboard() { + const [products, setProducts] = useState([]); + const [total, setTotal] = useState(0); + const [currentPage, setCurrentPage] = useState(1); + + let pageLimit = 10; + + const onChangePage = (page, limit = 10) => { + setCurrentPage(page); + fetchProducts(page, limit); + }; + + const fetchProducts = (page = 1, limit = 10) => { + getProducts(page, limit).then(({ products, total }) => { + setTotal(total); + setProducts(products); + }); + }; + + const handleRemoveProduct = (id) => { + removeProduct(id) + .then((res) => { + fetchProducts(currentPage); + }) + .catch((err) => { + console.log(err); + }); + }; + + //Get user when initialize the component + useEffect(fetchProducts, []); + + return ( +
+

+ PRODUCTS +

+ + + + + + + + + + + + + {products.map((product) => ( + + + + + + + + + ))} + +
ProductIDNamePriceCategorySellerAction
{product.ProductID}{product.ProductName}{product.Price}{product.Category ? product.Category : "N/A"}{product.SellerName ? product.SellerName : "N/A"} + { + handleRemoveProduct(product.ProductID); + }} + className="hover:text-red-600 cursor-pointer transition-all text-xl" + /> +
+ +
+ ); +} diff --git a/frontend/src/pages/UserDashboard.jsx b/frontend/src/pages/UserDashboard.jsx new file mode 100644 index 0000000..9cab96d --- /dev/null +++ b/frontend/src/pages/UserDashboard.jsx @@ -0,0 +1,91 @@ +import { useEffect, useState } from "react"; +import { getUsers, removeUser } from "../api/admin"; +import { MdDelete } from "react-icons/md"; +import Pagination from "../components/Pagination"; + +export default function UserDashboard() { + const [users, setUsers] = useState([]); + const [total, setTotal] = useState(0); + const [currentPage, setCurrentPage] = useState(1); + + let pageLimit = 10; + + const onChangePage = (page, limit = 10) => { + setCurrentPage(page); + fetchUsers(page, limit); + }; + + const fetchUsers = (page = 1, limit = 10) => { + getUsers(page, limit).then(({ users, total }) => { + setUsers(users); + setTotal(total); + }); + }; + + const handleRemoveUser = (id) => { + removeUser(id) + .then((res) => { + fetchUsers(currentPage); + }) + .catch((err) => { + console.log(err); + }); + }; + + //Get user when initialize the component + useEffect(fetchUsers, []); + + return ( +
+

+ USERS +

+ {users.length > 0 ? ( + <> + {" "} + + + + + + + + + + + + + + {users.map((user) => ( + + + + + + + + + + ))} + +
UserIDUCIDNameEmailPhoneAddressAction
{user.UserID}{user.UCID}{user.Name}{user.Email}{user.Phone}{user.Address} + { + handleRemoveUser(user.UserID); + }} + className="hover:text-red-600 cursor-pointer transition-all text-xl" + /> +
+ + + ) : ( +

+ No user exists! +

+ )} +
+ ); +} diff --git a/mysql-code/Init-Data.sql b/mysql-code/Init-Data.sql index 638e888..89747ab 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)'); + -- Insert Products INSERT INTO diff --git a/mysql-code/Schema.sql b/mysql-code/Schema.sql index cadb8b5..508dc7d 100644 --- a/mysql-code/Schema.sql +++ b/mysql-code/Schema.sql @@ -24,7 +24,7 @@ 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 ); @@ -38,15 +38,15 @@ CREATE TABLE Product ( Description TEXT, CategoryID INT NOT NULL, Date DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (UserID) REFERENCES User (UserID), - FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID) + FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE SET NULL, + 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) + FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE ); -- Fixed Review Entity (Many-to-One with User, Many-to-One with Product) @@ -60,8 +60,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 +71,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 +81,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 +91,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 +100,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 +110,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 From 444b436983cb62f7a441457a4e237cbf60468afd Mon Sep 17 00:00:00 2001 From: estherdev03 Date: Sun, 20 Apr 2025 20:56:14 -0600 Subject: [PATCH 6/8] Add transaction section to admin dashboard --- backend/controllers/transaction.js | 40 +++++++++ backend/index.js | 2 + backend/routes/transaction.js | 12 +++ frontend/src/App.jsx | 5 ++ frontend/src/api/admin.js | 24 ++++++ frontend/src/components/DashboardNav.jsx | 15 ++++ frontend/src/pages/CategoryDashboard.jsx | 73 ++++++++++------- frontend/src/pages/ProductDashboard.jsx | 84 ++++++++++--------- frontend/src/pages/TransactionDashboard.jsx | 91 +++++++++++++++++++++ 9 files changed, 277 insertions(+), 69 deletions(-) create mode 100644 backend/controllers/transaction.js create mode 100644 backend/routes/transaction.js create mode 100644 frontend/src/pages/TransactionDashboard.jsx 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/index.js b/backend/index.js index 5ba3b27..796860f 100644 --- a/backend/index.js +++ b/backend/index.js @@ -10,6 +10,7 @@ const recommendedRouter = require("./routes/recommendation"); const history = require("./routes/history"); const review = require("./routes/review"); const categoryRouter = require("./routes/category"); +const transactionRouter = require("./routes/transaction"); const { generateEmailTransporter } = require("./utils/mail"); const { @@ -44,6 +45,7 @@ app.use("/api/engine", recommendedRouter); app.use("/api/history", history); app.use("/api/review", review); app.use("/api/category", categoryRouter); +app.use("/api/transaction", transactionRouter); // Set up a scheduler to run cleanup every hour clean_up_time = 30 * 60 * 1000; 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/frontend/src/App.jsx b/frontend/src/App.jsx index eefdb63..ac28ad5 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -19,6 +19,7 @@ import ProductDashboard from "./pages/ProductDashboard"; import DashboardNav from "./components/DashboardNav"; import CategoryDashboard from "./pages/CategoryDashboard"; import { verifyIsAdmin } from "./api/admin"; +import TransactionDashboard from "./pages/TransactionDashboard"; function App() { // Authentication state - initialize from localStorage if available @@ -710,6 +711,10 @@ function App() { } /> } /> } /> + } + /> } /> diff --git a/frontend/src/api/admin.js b/frontend/src/api/admin.js index f2728c9..bea2a15 100644 --- a/frontend/src/api/admin.js +++ b/frontend/src/api/admin.js @@ -40,6 +40,19 @@ export const getCategories = async (page, limit = 10) => { } }; +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) { + const { response } = error; + if (response?.data) return response.data; + return { error: error.message || error }; + } +}; + export const addCategory = async (name) => { try { const { data } = await client.post(`/category/addCategory`, { name: name }); @@ -94,3 +107,14 @@ export const verifyIsAdmin = async (id) => { return { error: error.message || error }; } }; + +export const removeTransaction = async (id) => { + try { + const { data } = await client.delete(`/transaction/${id}`); + return { message: data.message }; + } catch (error) { + const { response } = error; + if (response?.data) return response.data; + return { error: error.message || error }; + } +}; diff --git a/frontend/src/components/DashboardNav.jsx b/frontend/src/components/DashboardNav.jsx index 75e287c..5f19de2 100644 --- a/frontend/src/components/DashboardNav.jsx +++ b/frontend/src/components/DashboardNav.jsx @@ -3,6 +3,7 @@ import { FaUserTag } from "react-icons/fa"; import { FaBoxArchive } from "react-icons/fa6"; import { MdOutlineCategory } from "react-icons/md"; import { FaArrowLeft } from "react-icons/fa"; +import { FaMoneyBillTransfer } from "react-icons/fa6"; export default function DashboardNav({ handleCloseAdminDashboard }) { const handleClick = () => { @@ -69,6 +70,20 @@ export default function DashboardNav({ handleCloseAdminDashboard }) { Categories +
  • + + (isActive + ? "text-green-400" + : "text-white transition-all hover:text-green-200") + + " flex items-center px-5 text-lg pt-5" + } + > + + Transaction + +
  • { removeCategory(id) - .then((res) => { + .then(() => { fetchCategory(currentPage); }) .catch((err) => { @@ -48,7 +48,7 @@ export default function CategoryDashboard() { useEffect(fetchCategory, []); return ( -
    +

    CATEGORIES

    @@ -60,35 +60,46 @@ export default function CategoryDashboard() { - - - - - - - - - - {categories.map((category) => ( - - - - - - ))} - -
    CategoryIDNameAction
    {category.CategoryID}{category.Name} - { - handleRemove(category.CategoryID); - }} - className="hover:text-red-600 cursor-pointer transition-all text-xl" - /> -
    - + {categories.length > 0 ? ( + <> + + + + + + + + + + {categories.map((category) => ( + + + + + + ))} + +
    CategoryIDNameAction
    {category.CategoryID}{category.Name} + { + handleRemove(category.CategoryID); + }} + className="hover:text-red-600 cursor-pointer transition-all text-xl" + /> +
    + + + ) : ( +

    + No category exists! +

    + )}
    ); } diff --git a/frontend/src/pages/ProductDashboard.jsx b/frontend/src/pages/ProductDashboard.jsx index c3ef4d2..35ce13e 100644 --- a/frontend/src/pages/ProductDashboard.jsx +++ b/frontend/src/pages/ProductDashboard.jsx @@ -40,44 +40,52 @@ export default function ProductDashboard() {

    PRODUCTS

    - - - - - - - - - - - - - {products.map((product) => ( - - - - - - - - - ))} - -
    ProductIDNamePriceCategorySellerAction
    {product.ProductID}{product.ProductName}{product.Price}{product.Category ? product.Category : "N/A"}{product.SellerName ? product.SellerName : "N/A"} - { - handleRemoveProduct(product.ProductID); - }} - className="hover:text-red-600 cursor-pointer transition-all text-xl" - /> -
    - + {products.length > 0 ? ( + <> + + + + + + + + + + + + + {products.map((product) => ( + + + + + + + + + ))} + +
    ProductIDNamePriceCategorySellerAction
    {product.ProductID}{product.ProductName}{product.Price}{product.Category ? product.Category : "N/A"}{product.SellerName ? product.SellerName : "N/A"} + { + handleRemoveProduct(product.ProductID); + }} + className="hover:text-red-600 cursor-pointer transition-all text-xl" + /> +
    + + + ) : ( +

    + No product exists! +

    + )}
    ); } diff --git a/frontend/src/pages/TransactionDashboard.jsx b/frontend/src/pages/TransactionDashboard.jsx new file mode 100644 index 0000000..167d5ee --- /dev/null +++ b/frontend/src/pages/TransactionDashboard.jsx @@ -0,0 +1,91 @@ +import { useEffect, useState } from "react"; +import { getTransactions, removeTransaction } from "../api/admin"; +import { MdDelete } from "react-icons/md"; +import Pagination from "../components/Pagination"; + +export default function TransactionDashboard() { + const [transactions, setTransactions] = useState([]); + const [total, setTotal] = useState(0); + const [currentPage, setCurrentPage] = useState(1); + + let pageLimit = 10; + + const onChangePage = (page, limit = 10) => { + setCurrentPage(page); + fetchTransactions(page, limit); + }; + + const fetchTransactions = (page = 1, limit = 10) => { + getTransactions(page, limit).then(({ transactions, total }) => { + setTotal(total); + setTransactions(transactions); + }); + }; + + const handleRemoveTransaction = (id) => { + removeTransaction(id) + .then(() => { + fetchTransactions(currentPage); + }) + .catch((err) => { + console.log(err); + }); + }; + + //Get user when initialize the component + useEffect(fetchTransactions, []); + + return ( +
    +

    + TRANSACTIONS +

    + {transactions.length > 0 ? ( + <> + + + + + + + + + + + + + {transactions.map((t) => ( + + + + + + + + + ))} + +
    TransactionIDUserProductDateStatusAction
    {t.TransactionID}{t.UserName ? t.UserName : "N/A"}{t.ProductName ? t.ProductName : "N/A"}{t.Date}{t.PaymentStatus} + { + handleRemoveTransaction(t.TransactionID); + }} + className="hover:text-red-600 cursor-pointer transition-all text-xl" + /> +
    + + + ) : ( +

    + No transaction exists! +

    + )} +
    + ); +} From 8347689f6c357efa568bb46bf365ea1bbf102eb4 Mon Sep 17 00:00:00 2001 From: Mann Patel <130435633+MannPatel0@users.noreply.github.com> Date: Sun, 20 Apr 2025 22:07:40 -0600 Subject: [PATCH 7/8] uploads --- backend/utils/database.js | 1 - mysql-code/Init-Data.sql | 89 +++++++++--------- .../__pycache__/app.cpython-313.pyc | Bin 11849 -> 11849 bytes 3 files changed, 44 insertions(+), 46 deletions(-) diff --git a/backend/utils/database.js b/backend/utils/database.js index ffc5ef3..379d5df 100644 --- a/backend/utils/database.js +++ b/backend/utils/database.js @@ -4,7 +4,6 @@ const pool = mysql.createPool({ host: "localhost", user: "root", database: "Marketplace", - password: "12345678", }); // const pool = mysql.createPool( diff --git a/mysql-code/Init-Data.sql b/mysql-code/Init-Data.sql index 89747ab..b636c7a 100644 --- a/mysql-code/Init-Data.sql +++ b/mysql-code/Init-Data.sql @@ -81,28 +81,27 @@ VALUES ('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)'); - + ('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)'); -- Insert Products INSERT INTO @@ -321,29 +320,29 @@ VALUES INSERT INTO Image_URL (URL, ProductID) VALUES - ('/Pictures/Dell1.jpg', 1), - ('/Pictures/Dell2.jpg', 1), - ('/Pictures/Dell3.jpg', 1), - ('/Pictures/HP-Laptop1.jpg', 2), - ('/Pictures/HP-Laptop1.jpg', 2), - ('/Pictures/Dorm-Desk.jpg', 3), - ('/Pictures/University-Hoodie.jpg', 4), - ('/Pictures/Basketball.jpg', 5), - ('/Pictures/Acoustic-Guitar.jpg', 6), - ('/Pictures/Physics-Textbook.jpg', 7), - ('/Pictures/Mini-Fridge.jpg', 8), - ('/Pictures/Controller.jpg', 9), - ('/Pictures/Mountain-Bike.jpg', 10), - ('/Pictures/Wireless-Mouse.jpg', 11), - ('/Pictures/Lab-Coat.jpg', 12), - ('/Pictures/Calculator.jpg', 13), - ('/Pictures/Yoga-Mat.jpg', 14), - ('/Pictures/Winter-Jacket.jpg', 15), - ('/Pictures/CS-Textbook.jpg', 16), - ('/Pictures/Desk-Lamp.jpg', 17), - ('/Pictures/HP-Calculator.jpg', 18), - ('/Pictures/Bluetooth-Speaker.jpg', 19), - ('/Pictures/Backpack.jpg', 20); + ('/Uploads/Dell1.jpg', 1), + ('/Uploads/Dell2.jpg', 1), + ('/Uploads/Dell3.jpg', 1), + ('/Uploads/HP-Laptop1.jpg', 2), + ('/Uploads/HP-Laptop1.jpg', 2), + ('/Uploads/Dorm-Desk.jpg', 3), + ('/Uploads/University-Hoodie.jpg', 4), + ('/Uploads/Basketball.jpg', 5), + ('/Uploads/Acoustic-Guitar.jpg', 6), + ('/Uploads/Physics-Textbook.jpg', 7), + ('/Uploads/Mini-Fridge.jpg', 8), + ('/Uploads/Controller.jpg', 9), + ('/Uploads/Mountain-Bike.jpg', 10), + ('/Uploads/Wireless-Mouse.jpg', 11), + ('/Uploads/Lab-Coat.jpg', 12), + ('/Uploads/Calculator.jpg', 13), + ('/Uploads/Yoga-Mat.jpg', 14), + ('/Uploads/Winter-Jacket.jpg', 15), + ('/Uploads/CS-Textbook.jpg', 16), + ('/Uploads/Desk-Lamp.jpg', 17), + ('/Uploads/HP-Calculator.jpg', 18), + ('/Uploads/Bluetooth-Speaker.jpg', 19), + ('/Uploads/Backpack.jpg', 20); -- Insert Product-Category relationships (products with multiple categories) INSERT INTO diff --git a/recommondation-engine/__pycache__/app.cpython-313.pyc b/recommondation-engine/__pycache__/app.cpython-313.pyc index 1a5844b7cbc5caba7202300616523e6986d1fd68..fb4ea10b64b1126bc955c142bc824715311c94fe 100644 GIT binary patch delta 19 ZcmX>Zb25hOGcPX}0}ycR-^gXJ2LL~t1zP|B delta 19 ZcmX>Zb25hOGcPX}0}zz$+{k6G2LM751;YRU From 5228bf73c9c57e626675f6837d24c1ca9c8ce033 Mon Sep 17 00:00:00 2001 From: Mann Patel <130435633+MannPatel0@users.noreply.github.com> Date: Mon, 21 Apr 2025 01:01:58 -0600 Subject: [PATCH 8/8] refactor for redundant code --- backend/controllers/product.js | 4 +- backend/routes/product.js | 3 +- frontend/src/App.jsx | 251 +++++++------- frontend/src/api/admin.js | 173 +++++----- frontend/src/api/client.js | 3 - frontend/src/components/DashboardNav.jsx | 99 +----- frontend/src/pages/CategoryDashboard.jsx | 105 ------ frontend/src/pages/Dashboard.jsx | 361 +++++++++++++++++++- frontend/src/pages/ProductDashboard.jsx | 91 ----- frontend/src/pages/TransactionDashboard.jsx | 91 ----- frontend/src/pages/UserDashboard.jsx | 91 ----- frontend/src/schema.sql | 124 +++++++ mysql-code/Init-Data.sql | 3 +- mysql-code/Schema.sql | 19 +- 14 files changed, 726 insertions(+), 692 deletions(-) delete mode 100644 frontend/src/api/client.js delete mode 100644 frontend/src/pages/CategoryDashboard.jsx delete mode 100644 frontend/src/pages/ProductDashboard.jsx delete mode 100644 frontend/src/pages/TransactionDashboard.jsx delete mode 100644 frontend/src/pages/UserDashboard.jsx create mode 100644 frontend/src/schema.sql diff --git a/backend/controllers/product.js b/backend/controllers/product.js index 44185a5..31ca2ec 100644 --- a/backend/controllers/product.js +++ b/backend/controllers/product.js @@ -391,9 +391,9 @@ exports.getProductWithPagination = async (req, res) => { } }; -exports.removeProduct = async (req, res) => { +exports.removeAnyProduct = async (req, res) => { const { id } = req.params; - + console.log(id); try { const [result] = await db.execute( `DELETE FROM Product WHERE ProductID = ?`, diff --git a/backend/routes/product.js b/backend/routes/product.js index 09000cd..5862209 100644 --- a/backend/routes/product.js +++ b/backend/routes/product.js @@ -8,6 +8,7 @@ const { getProductById, addProduct, removeProduct, + removeAnyProduct, getProductWithPagination, myProduct, updateProduct, @@ -30,7 +31,7 @@ router.post("/addProduct", addProduct); router.get("/getProduct", getAllProducts); //Remove product -router.delete("/:id", removeProduct); +router.delete("/any/:id", removeAnyProduct); //Get products with pagination router.get("/getProductWithPagination", getProductWithPagination); diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 38d0009..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"; @@ -12,14 +13,10 @@ import Selling from "./pages/Selling"; import Transactions from "./pages/Transactions"; import Favorites from "./pages/Favorites"; import ProductDetail from "./pages/ProductDetail"; -import SearchPage from "./pages/SearchPage"; // Make sure to import the SearchPage -import Dashboard from "./pages/Dashboard"; -import UserDashboard from "./pages/UserDashboard"; -import ProductDashboard from "./pages/ProductDashboard"; +import SearchPage from "./pages/SearchPage"; +import Dashboard from "./pages/Dashboard"; // The single consolidated dashboard component import DashboardNav from "./components/DashboardNav"; -import CategoryDashboard from "./pages/CategoryDashboard"; import { verifyIsAdmin } from "./api/admin"; -import TransactionDashboard from "./pages/TransactionDashboard"; function App() { // Authentication state - initialize from localStorage if available @@ -42,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); @@ -127,9 +136,6 @@ function App() { } }; - const [isAdmin, setIsAdmin] = useState(false); - const [showAdminDashboard, setShowAdminDashboard] = useState(false); - useEffect(() => { const userInfo = sessionStorage.getItem("user") ? JSON.parse(sessionStorage.getItem("user")) @@ -142,9 +148,14 @@ function App() { 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 @@ -436,6 +447,7 @@ function App() { setVerificationStep("initial"); setTempUserData(null); setRecommendations([]); + setShowAdminDashboard(false); // Clear localStorage sessionStorage.removeItem("user"); @@ -774,126 +786,119 @@ function App() { return children; }; - // If user is admin, show admin naviagtion - if (showAdminDashboard) { - return ( - + return ( + + {/* If admin dashboard should be shown */} + {showAdminDashboard ? (
    - {/* Admin routes */} - } /> - } /> - } /> - } /> - } - /> - } /> + {/* 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 && } - return ( - -
    - {/* 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 index bea2a15..7aaa648 100644 --- a/frontend/src/api/admin.js +++ b/frontend/src/api/admin.js @@ -1,77 +1,20 @@ -import client from "./client"; +// 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}` + `/user/getUserWithPagination?page=${page}&limit=${limit}`, ); return { users: data.users, total: data.total }; } catch (error) { - const { response } = error; - if (response?.data) return response.data; - return { error: error.message || error }; - } -}; - -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) { - const { response } = error; - if (response?.data) return response.data; - return { error: error.message || error }; - } -}; - -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) { - const { response } = error; - if (response?.data) return response.data; - return { error: error.message || error }; - } -}; - -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) { - const { response } = error; - if (response?.data) return response.data; - return { error: error.message || error }; - } -}; - -export const addCategory = async (name) => { - try { - const { data } = await client.post(`/category/addCategory`, { name: name }); - return { message: data.message }; - } catch (error) { - const { response } = error; - if (response?.data) return response.data; - return { error: error.message || error }; - } -}; - -export const removeCategory = async (id) => { - try { - const { data } = await client.delete(`/category/${id}`); - return { message: data.message }; - } catch (error) { - const { response } = error; - if (response?.data) return response.data; - return { error: error.message || error }; + return handleError(error); } }; @@ -80,20 +23,7 @@ export const removeUser = async (id) => { const { data } = await client.post(`/user/delete`, { userId: id }); return { message: data.message }; } catch (error) { - const { response } = error; - if (response?.data) return response.data; - return { error: error.message || error }; - } -}; - -export const removeProduct = async (id) => { - try { - const { data } = await client.delete(`/product/${id}`); - return { message: data.message }; - } catch (error) { - const { response } = error; - if (response?.data) return response.data; - return { error: error.message || error }; + return handleError(error); } }; @@ -102,9 +32,70 @@ export const verifyIsAdmin = async (id) => { const { data } = await client.get(`/user/isAdmin/${id}`); return { isAdmin: data.isAdmin }; } catch (error) { - const { response } = error; - if (response?.data) return response.data; - return { error: error.message || 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); } }; @@ -113,8 +104,16 @@ export const removeTransaction = async (id) => { const { data } = await client.delete(`/transaction/${id}`); return { message: data.message }; } catch (error) { - const { response } = error; - if (response?.data) return response.data; - return { error: error.message || 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/api/client.js b/frontend/src/api/client.js deleted file mode 100644 index bcb4831..0000000 --- a/frontend/src/api/client.js +++ /dev/null @@ -1,3 +0,0 @@ -import axios from "axios"; -const client = axios.create({ baseURL: "http://localhost:3030/api" }); -export default client; diff --git a/frontend/src/components/DashboardNav.jsx b/frontend/src/components/DashboardNav.jsx index 5f19de2..2013901 100644 --- a/frontend/src/components/DashboardNav.jsx +++ b/frontend/src/components/DashboardNav.jsx @@ -1,98 +1,15 @@ -import { Link, NavLink } from "react-router-dom"; -import { FaUserTag } from "react-icons/fa"; -import { FaBoxArchive } from "react-icons/fa6"; -import { MdOutlineCategory } from "react-icons/md"; import { FaArrowLeft } from "react-icons/fa"; -import { FaMoneyBillTransfer } from "react-icons/fa6"; export default function DashboardNav({ handleCloseAdminDashboard }) { - const handleClick = () => { - handleCloseAdminDashboard(); - }; - return ( -
    -
    -
      -
    • -
      - - Campus Plug - - Campus Plug - - -
      -
    • -
    • - - (isActive - ? " text-green-400" - : "text-white transition-all hover:text-green-200") + - " flex items-center px-5 text-lg pt-5 " - } - > - - Users - -
    • -
    • - - (isActive - ? "text-green-400" - : "text-white transition-all hover:text-green-200") + - " flex items-center px-5 text-lg pt-5" - } - > - - Products - -
    • -
    • - - (isActive - ? "text-green-400" - : "text-white transition-all hover:text-green-200") + - " flex items-center px-5 text-lg pt-5" - } - > - - Categories - -
    • -
    • - - (isActive - ? "text-green-400" - : "text-white transition-all hover:text-green-200") + - " flex items-center px-5 text-lg pt-5" - } - > - - Transaction - -
    • -
    -
    - - Go back to user page -
    -
    +
    +
    ); } diff --git a/frontend/src/pages/CategoryDashboard.jsx b/frontend/src/pages/CategoryDashboard.jsx deleted file mode 100644 index e08c82f..0000000 --- a/frontend/src/pages/CategoryDashboard.jsx +++ /dev/null @@ -1,105 +0,0 @@ -import { useEffect, useState } from "react"; -import { getCategories, removeCategory } from "../api/admin"; -import { MdDelete } from "react-icons/md"; -import Pagination from "../components/Pagination"; -import { IoAddCircleSharp } from "react-icons/io5"; -import CategoryForm from "../components/CategoryForm"; - -export default function CategoryDashboard() { - const [categories, setCategories] = useState([]); - const [total, setTotal] = useState(0); - const [currentPage, setCurrentPage] = useState(1); - - let pageLimit = 10; - - const [visible, setVisible] = useState(false); - - const onChangePage = (page, limit = 10) => { - setCurrentPage(page); - fetchCategory(page, limit); - }; - - const fetchCategory = (page = 1, limit = 10) => { - getCategories(page, limit).then(({ data, total }) => { - setCategories(data); - setTotal(total); - }); - }; - - const notiChange = () => { - fetchCategory(currentPage); - }; - - const handleToggleForm = () => { - setVisible((curr) => !curr); - }; - - const handleRemove = (id) => { - removeCategory(id) - .then(() => { - fetchCategory(currentPage); - }) - .catch((err) => { - console.log(err); - }); - }; - - //Get user when initialize the component - useEffect(fetchCategory, []); - - return ( -
    -

    - CATEGORIES -

    - - - {categories.length > 0 ? ( - <> - - - - - - - - - - {categories.map((category) => ( - - - - - - ))} - -
    CategoryIDNameAction
    {category.CategoryID}{category.Name} - { - handleRemove(category.CategoryID); - }} - className="hover:text-red-600 cursor-pointer transition-all text-xl" - /> -
    - - - ) : ( -

    - No category exists! -

    - )} -
    - ); -} diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index dbcc762..6b991f6 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -1,7 +1,362 @@ -export default function Dashboard() { +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 = () => ( +
    +
    +
    +); + +// Empty State Component +const EmptyState = () => ( +
    + + + +

    No data found

    +

    + No records are currently available. +

    +
    +); + +// 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 ; + return ( -
    - Welcome to admin dashboard +
    +
    +

    + Total: {total} +

    + {headerAction &&
    {headerAction}
    } +
    + + {items.length > 0 ? ( +
    + + + + {columns.map((col) => ( + + ))} + + + + + {items.map((item) => ( + + {columns.map((col) => ( + + ))} + + + ))} + +
    + {col.label} + + Action +
    + {item[col.key] || "—"} + + +
    +
    + ) : ( + + )} + + {total > pageLimit && ( +
    + +
    + )} +
    + ); +}; + +// 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: () => ( + + ), + }, + { + title: "Products", + icon: "📦", + key: "products", + component: () => ( + + ), + }, + { + title: "Transactions", + icon: "💰", + key: "transactions", + component: () => ( + + ), + }, + { + title: "Categories", + icon: "🏷️", + key: "categories", + component: () => ( + <> + + + Add Category + + } + /> + { + setCategoryRefreshKey((prev) => prev + 1); + toggleForm(); + }} + /> + + ), + }, + ]; + + return ( +
    +
    +
    +

    + Admin Dashboard +

    +
    + + {/* Mobile Tabs */} +
    + +
    + + {/* Desktop Tabs */} +
    + {tabs.map((tab, index) => ( + + ))} +
    + +
    +
    + {tabData[tabs[activeTab].key].loaded && tabs[activeTab].component()} +
    +
    +
    ); } diff --git a/frontend/src/pages/ProductDashboard.jsx b/frontend/src/pages/ProductDashboard.jsx deleted file mode 100644 index 35ce13e..0000000 --- a/frontend/src/pages/ProductDashboard.jsx +++ /dev/null @@ -1,91 +0,0 @@ -import { useEffect, useState } from "react"; -import { getProducts, removeProduct } from "../api/admin"; -import { MdDelete } from "react-icons/md"; -import Pagination from "../components/Pagination"; - -export default function ProductDashboard() { - const [products, setProducts] = useState([]); - const [total, setTotal] = useState(0); - const [currentPage, setCurrentPage] = useState(1); - - let pageLimit = 10; - - const onChangePage = (page, limit = 10) => { - setCurrentPage(page); - fetchProducts(page, limit); - }; - - const fetchProducts = (page = 1, limit = 10) => { - getProducts(page, limit).then(({ products, total }) => { - setTotal(total); - setProducts(products); - }); - }; - - const handleRemoveProduct = (id) => { - removeProduct(id) - .then((res) => { - fetchProducts(currentPage); - }) - .catch((err) => { - console.log(err); - }); - }; - - //Get user when initialize the component - useEffect(fetchProducts, []); - - return ( -
    -

    - PRODUCTS -

    - {products.length > 0 ? ( - <> - - - - - - - - - - - - - {products.map((product) => ( - - - - - - - - - ))} - -
    ProductIDNamePriceCategorySellerAction
    {product.ProductID}{product.ProductName}{product.Price}{product.Category ? product.Category : "N/A"}{product.SellerName ? product.SellerName : "N/A"} - { - handleRemoveProduct(product.ProductID); - }} - className="hover:text-red-600 cursor-pointer transition-all text-xl" - /> -
    - - - ) : ( -

    - No product exists! -

    - )} -
    - ); -} diff --git a/frontend/src/pages/TransactionDashboard.jsx b/frontend/src/pages/TransactionDashboard.jsx deleted file mode 100644 index 167d5ee..0000000 --- a/frontend/src/pages/TransactionDashboard.jsx +++ /dev/null @@ -1,91 +0,0 @@ -import { useEffect, useState } from "react"; -import { getTransactions, removeTransaction } from "../api/admin"; -import { MdDelete } from "react-icons/md"; -import Pagination from "../components/Pagination"; - -export default function TransactionDashboard() { - const [transactions, setTransactions] = useState([]); - const [total, setTotal] = useState(0); - const [currentPage, setCurrentPage] = useState(1); - - let pageLimit = 10; - - const onChangePage = (page, limit = 10) => { - setCurrentPage(page); - fetchTransactions(page, limit); - }; - - const fetchTransactions = (page = 1, limit = 10) => { - getTransactions(page, limit).then(({ transactions, total }) => { - setTotal(total); - setTransactions(transactions); - }); - }; - - const handleRemoveTransaction = (id) => { - removeTransaction(id) - .then(() => { - fetchTransactions(currentPage); - }) - .catch((err) => { - console.log(err); - }); - }; - - //Get user when initialize the component - useEffect(fetchTransactions, []); - - return ( -
    -

    - TRANSACTIONS -

    - {transactions.length > 0 ? ( - <> - - - - - - - - - - - - - {transactions.map((t) => ( - - - - - - - - - ))} - -
    TransactionIDUserProductDateStatusAction
    {t.TransactionID}{t.UserName ? t.UserName : "N/A"}{t.ProductName ? t.ProductName : "N/A"}{t.Date}{t.PaymentStatus} - { - handleRemoveTransaction(t.TransactionID); - }} - className="hover:text-red-600 cursor-pointer transition-all text-xl" - /> -
    - - - ) : ( -

    - No transaction exists! -

    - )} -
    - ); -} diff --git a/frontend/src/pages/UserDashboard.jsx b/frontend/src/pages/UserDashboard.jsx deleted file mode 100644 index 9cab96d..0000000 --- a/frontend/src/pages/UserDashboard.jsx +++ /dev/null @@ -1,91 +0,0 @@ -import { useEffect, useState } from "react"; -import { getUsers, removeUser } from "../api/admin"; -import { MdDelete } from "react-icons/md"; -import Pagination from "../components/Pagination"; - -export default function UserDashboard() { - const [users, setUsers] = useState([]); - const [total, setTotal] = useState(0); - const [currentPage, setCurrentPage] = useState(1); - - let pageLimit = 10; - - const onChangePage = (page, limit = 10) => { - setCurrentPage(page); - fetchUsers(page, limit); - }; - - const fetchUsers = (page = 1, limit = 10) => { - getUsers(page, limit).then(({ users, total }) => { - setUsers(users); - setTotal(total); - }); - }; - - const handleRemoveUser = (id) => { - removeUser(id) - .then((res) => { - fetchUsers(currentPage); - }) - .catch((err) => { - console.log(err); - }); - }; - - //Get user when initialize the component - useEffect(fetchUsers, []); - - return ( -
    -

    - USERS -

    - {users.length > 0 ? ( - <> - {" "} - - - - - - - - - - - - - - {users.map((user) => ( - - - - - - - - - - ))} - -
    UserIDUCIDNameEmailPhoneAddressAction
    {user.UserID}{user.UCID}{user.Name}{user.Email}{user.Phone}{user.Address} - { - handleRemoveUser(user.UserID); - }} - className="hover:text-red-600 cursor-pointer transition-all text-xl" - /> -
    - - - ) : ( -

    - No user exists! -

    - )} -
    - ); -} 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 b636c7a..3c7dae5 100644 --- a/mysql-code/Init-Data.sql +++ b/mysql-code/Init-Data.sql @@ -101,7 +101,8 @@ VALUES ('Photography Equipment'), ('Event Tickets'), ('Software Licenses'), - ('Transportation (Car Pool)'); + ('Transportation (Car Pool)'), + ('Other'); -- Insert Products INSERT INTO diff --git a/mysql-code/Schema.sql b/mysql-code/Schema.sql index 508dc7d..e65a325 100644 --- a/mysql-code/Schema.sql +++ b/mysql-code/Schema.sql @@ -28,7 +28,6 @@ CREATE TABLE Category ( Name VARCHAR(255) NOT NULL ); --- Product Entity CREATE TABLE Product ( ProductID INT AUTO_INCREMENT PRIMARY KEY, Name VARCHAR(255) NOT NULL, @@ -38,8 +37,22 @@ CREATE TABLE Product ( Description TEXT, CategoryID INT NOT NULL, Date DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (UserID) REFERENCES User (UserID), + 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) ON DELETE SET NULL + FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID) ); -- Fixed Image_URL table @@ -72,7 +85,7 @@ CREATE TABLE Transaction ( 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 SET NULL + FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE SET NULL ); -- Recommendation Entity (Many-to-One with User, Many-to-One with Product)