refactor for redundant code

This commit is contained in:
Mann Patel
2025-04-21 01:01:58 -06:00
parent 8347689f6c
commit 5228bf73c9
14 changed files with 726 additions and 692 deletions

View File

@@ -4,6 +4,7 @@ import {
Routes,
Route,
Navigate,
useLocation,
} from "react-router-dom";
import Navbar from "./components/Navbar";
import Home from "./pages/Home";
@@ -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 (
<Router>
return (
<Router>
{/* If admin dashboard should be shown */}
{showAdminDashboard ? (
<div className="flex">
<DashboardNav handleCloseAdminDashboard={handleCloseAdminDashboard} />
<Routes>
{/* Admin routes */}
<Route path="/admin" element={<Dashboard />} />
<Route path="/admin/user" element={<UserDashboard />} />
<Route path="/admin/product" element={<ProductDashboard />} />
<Route path="/admin/category" element={<CategoryDashboard />} />
<Route
path="/admin/transaction"
element={<TransactionDashboard />}
/>
<Route path="*" element={<Dashboard />} />
{/* Single admin route for consolidated dashboard */}
<Route path="/admin/*" element={<Dashboard />} />
{/* Any other path in admin mode should go to dashboard */}
<Route path="*" element={<Navigate to="/admin" />} />
</Routes>
</div>
</Router>
);
}
) : (
/* Normal user interface */
<div className="min-h-screen bg-gray-50">
{/* Show loading overlay when generating recommendations */}
{isGeneratingRecommendations && <LoadingOverlay />}
return (
<Router>
<div className="min-h-screen bg-gray-50">
{/* Show loading overlay when generating recommendations */}
{isGeneratingRecommendations && <LoadingOverlay />}
{/* Only show navbar when authenticated */}
{isAuthenticated && (
<Navbar
isAdmin={isAdmin}
onLogout={handleLogout}
userName={user?.name}
handleShowAdminDashboard={handleShowAdminDashboard}
/>
)}
<Routes>
{/* Public routes */}
<Route
path="/login"
element={isAuthenticated ? <Navigate to="/" /> : <LoginComponent />}
/>
{/* Protected routes */}
<Route
path="/"
element={
<ProtectedRoute>
<div className="container mx-auto px-4 py-6">
<Home recommendations={recommendations} />
</div>
</ProtectedRoute>
}
/>
<Route
path="/product/:id"
element={
<ProtectedRoute>
<ProductDetail />
</ProtectedRoute>
}
/>
<Route
path="/search"
element={
<ProtectedRoute>
<div className="container mx-auto px-4 py-6">
<SearchPage />
</div>
</ProtectedRoute>
}
/>
<Route
path="/settings"
element={
<ProtectedRoute>
<div className="container mx-auto px-4 py-6">
<Settings />
</div>
</ProtectedRoute>
}
/>
<Route
path="/selling"
element={
<ProtectedRoute>
<div className="container mx-auto px-4 py-6">
<Selling />
</div>
</ProtectedRoute>
}
/>
<Route
path="/transactions"
element={
<ProtectedRoute>
<div className="container mx-auto px-4 py-6">
<Transactions />
</div>
</ProtectedRoute>
}
/>
<Route
path="/favorites"
element={
<ProtectedRoute>
<div className="container mx-auto px-4 py-6">
<Favorites />
</div>
</ProtectedRoute>
}
/>
{/* Redirect to login for any unmatched routes */}
<Route
path="*"
element={<Navigate to={isAuthenticated ? "/" : "/login"} />}
/>
</Routes>
</div>
{/* Only show navbar when authenticated */}
{isAuthenticated && (
<Navbar
isAdmin={isAdmin}
onLogout={handleLogout}
userName={user?.name}
handleShowAdminDashboard={handleShowAdminDashboard}
/>
)}
<Routes>
{/* Public routes */}
<Route
path="/login"
element={
isAuthenticated ? <Navigate to="/" /> : <LoginComponent />
}
/>
{/* Protected routes */}
<Route
path="/"
element={
<ProtectedRoute>
<div className="container mx-auto px-4 py-6">
<Home recommendations={recommendations} />
</div>
</ProtectedRoute>
}
/>
<Route
path="/product/:id"
element={
<ProtectedRoute>
<ProductDetail />
</ProtectedRoute>
}
/>
<Route
path="/search"
element={
<ProtectedRoute>
<div className="container mx-auto px-4 py-6">
<SearchPage />
</div>
</ProtectedRoute>
}
/>
<Route
path="/settings"
element={
<ProtectedRoute>
<div className="container mx-auto px-4 py-6">
<Settings />
</div>
</ProtectedRoute>
}
/>
<Route
path="/selling"
element={
<ProtectedRoute>
<div className="container mx-auto px-4 py-6">
<Selling />
</div>
</ProtectedRoute>
}
/>
<Route
path="/transactions"
element={
<ProtectedRoute>
<div className="container mx-auto px-4 py-6">
<Transactions />
</div>
</ProtectedRoute>
}
/>
<Route
path="/favorites"
element={
<ProtectedRoute>
<div className="container mx-auto px-4 py-6">
<Favorites />
</div>
</ProtectedRoute>
}
/>
{/* Redirect to login for any unmatched routes */}
<Route
path="*"
element={<Navigate to={isAuthenticated ? "/" : "/login"} />}
/>
</Routes>
</div>
)}
</Router>
);
}

View File

@@ -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;

View File

@@ -1,3 +0,0 @@
import axios from "axios";
const client = axios.create({ baseURL: "http://localhost:3030/api" });
export default client;

View File

@@ -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 (
<div>
<div className="w-3xs h-screen bg-green-700 border border-green-700">
<ul>
<li>
<div className="flex-shrink-0 p-6 bg-green-200">
<Link to="/admin" className="flex items-center">
<img
src="/icon/icon-512.png"
alt="Campus Plug"
className="h-8 px-2 "
/>
<span className="hidden md:block text-green-700 font-bold text-2xl">
Campus Plug
</span>
</Link>
</div>
</li>
<li className="w-fit pl-10">
<NavLink
to="/admin/user"
className={({ isActive }) =>
(isActive
? " text-green-400"
: "text-white transition-all hover:text-green-200") +
" flex items-center px-5 text-lg pt-5 "
}
>
<FaUserTag />
<span className="pl-3">Users</span>
</NavLink>
</li>
<li className="w-fit pl-10">
<NavLink
to="/admin/product"
className={({ isActive }) =>
(isActive
? "text-green-400"
: "text-white transition-all hover:text-green-200") +
" flex items-center px-5 text-lg pt-5"
}
>
<FaBoxArchive />
<span className="pl-3">Products</span>
</NavLink>
</li>
<li className="w-fit pl-10">
<NavLink
to="/admin/category"
className={({ isActive }) =>
(isActive
? "text-green-400"
: "text-white transition-all hover:text-green-200") +
" flex items-center px-5 text-lg pt-5"
}
>
<MdOutlineCategory />
<span className="pl-3">Categories</span>
</NavLink>
</li>
<li className="w-fit pl-10">
<NavLink
to="/admin/transaction"
className={({ isActive }) =>
(isActive
? "text-green-400"
: "text-white transition-all hover:text-green-200") +
" flex items-center px-5 text-lg pt-5"
}
>
<FaMoneyBillTransfer />
<span className="pl-3">Transaction</span>
</NavLink>
</li>
</ul>
<div
onClick={handleClick}
className=" text-center my-8 underline text-white underline-offset-4 flex justify-center items-center hover:cursor-pointer h-fit w-fit mx-auto hover:text-green-200 transition"
>
<FaArrowLeft className="text-sm mx-2 mt-1" />
<span>Go back to user page</span>
</div>
</div>
<div className="w-48 min-w-[12rem] bg-gray-100 text-emerald-600 flex flex-col p-4 shadow-md">
<button
onClick={handleCloseAdminDashboard}
className="flex items-center gap-2 text-sm font-medium hover:text-emerald-700 underline underline-offset-4 transition"
>
<FaArrowLeft className="text-xs mt-[1px]" />
Back to User Page
</button>
</div>
);
}

View File

@@ -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 (
<div className="pt-10 p-20 w-full">
<h1 className="text-4xl pb-3 font-bold text-green-800 underline">
CATEGORIES
</h1>
<button
onClick={handleToggleForm}
className="flex justify-end items-center bg-blue-500 rounded-md px-2 text-white text-sm p-1 mb-1 ml-auto hover:cursor-pointer hover:bg-blue-600 transtion"
>
<span className="pr-1 text-xs">Add</span>
<IoAddCircleSharp />
</button>
<CategoryForm onAddCategory={notiChange} visible={visible} />
{categories.length > 0 ? (
<>
<table className="table-fixed w-full text-center border border-green-600">
<thead className="bg-green-600 h-10">
<tr>
<th>CategoryID</th>
<th>Name</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{categories.map((category) => (
<tr
key={category.UserID}
className="border border-green-600 h-10"
>
<td>{category.CategoryID}</td>
<td>{category.Name}</td>
<td className="flex justify-center pt-2">
<MdDelete
onClick={() => {
handleRemove(category.CategoryID);
}}
className="hover:text-red-600 cursor-pointer transition-all text-xl"
/>
</td>
</tr>
))}
</tbody>
</table>
<Pagination
pageNum={Math.ceil(total / pageLimit)}
onChange={onChangePage}
/>
</>
) : (
<p className="text-red-700 text-xl bg-red-200 px-3 rounded-md py-1 w-fit">
No category exists!
</p>
)}
</div>
);
}

View File

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

View File

@@ -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 (
<div className="pt-10 p-20">
<h1 className="text-4xl pb-3 font-bold text-green-800 underline">
PRODUCTS
</h1>
{products.length > 0 ? (
<>
<table className="table-fixed w-full text-center border border-green-600">
<thead className="bg-green-600 h-10">
<tr>
<th>ProductID</th>
<th>Name</th>
<th>Price</th>
<th>Category</th>
<th>Seller</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{products.map((product) => (
<tr
key={product.ProductID}
className="border border-green-600 h-10"
>
<td>{product.ProductID}</td>
<td>{product.ProductName}</td>
<td>{product.Price}</td>
<td>{product.Category ? product.Category : "N/A"}</td>
<td>{product.SellerName ? product.SellerName : "N/A"}</td>
<td className="flex justify-center pt-2">
<MdDelete
onClick={() => {
handleRemoveProduct(product.ProductID);
}}
className="hover:text-red-600 cursor-pointer transition-all text-xl"
/>
</td>
</tr>
))}
</tbody>
</table>
<Pagination
onChange={onChangePage}
pageNum={Math.ceil(total / pageLimit)}
/>
</>
) : (
<p className="text-red-700 text-xl bg-red-200 px-3 rounded-md py-1 w-fit">
No product exists!
</p>
)}
</div>
);
}

View File

@@ -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 (
<div className="pt-10 p-20">
<h1 className="text-4xl pb-3 font-bold text-green-800 underline">
TRANSACTIONS
</h1>
{transactions.length > 0 ? (
<>
<table className="table-fixed w-full text-center border border-green-600">
<thead className="bg-green-600 h-10">
<tr>
<th>TransactionID</th>
<th>User</th>
<th>Product</th>
<th>Date</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{transactions.map((t) => (
<tr
key={t.TransactionID}
className="border border-green-600 h-10"
>
<td>{t.TransactionID}</td>
<td>{t.UserName ? t.UserName : "N/A"}</td>
<td>{t.ProductName ? t.ProductName : "N/A"}</td>
<td>{t.Date}</td>
<td>{t.PaymentStatus}</td>
<td className="flex justify-center pt-2">
<MdDelete
onClick={() => {
handleRemoveTransaction(t.TransactionID);
}}
className="hover:text-red-600 cursor-pointer transition-all text-xl"
/>
</td>
</tr>
))}
</tbody>
</table>
<Pagination
onChange={onChangePage}
pageNum={Math.ceil(total / pageLimit)}
/>
</>
) : (
<p className="text-red-700 text-xl bg-red-200 px-3 rounded-md py-1 w-fit">
No transaction exists!
</p>
)}
</div>
);
}

View File

@@ -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 (
<div className="pt-10 p-20">
<h1 className="text-4xl pb-3 font-bold text-green-800 underline">
USERS
</h1>
{users.length > 0 ? (
<>
{" "}
<table className="table-fixed w-full text-center border border-green-600">
<thead className="bg-green-600 h-10">
<tr>
<th>UserID</th>
<th>UCID</th>
<th>Name</th>
<th>Email</th>
<th>Phone</th>
<th>Address</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr key={user.UserID} className="border border-green-600 h-10">
<td>{user.UserID}</td>
<td>{user.UCID}</td>
<td>{user.Name}</td>
<td>{user.Email}</td>
<td>{user.Phone}</td>
<td>{user.Address}</td>
<td className="flex justify-center pt-2">
<MdDelete
onClick={() => {
handleRemoveUser(user.UserID);
}}
className="hover:text-red-600 cursor-pointer transition-all text-xl"
/>
</td>
</tr>
))}
</tbody>
</table>
<Pagination
pageNum={Math.ceil(total / pageLimit)}
onChange={onChangePage}
/>
</>
) : (
<p className="text-red-700 text-xl bg-red-200 px-3 rounded-md py-1">
No user exists!
</p>
)}
</div>
);
}

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

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