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