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) => ( + + ))} +
+ +
+
+ {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