20 Commits

Author SHA1 Message Date
Mann Patel
505f6cd134 updating alerts ui/ux 2025-04-21 01:19:39 -06:00
Mann Patel
53686bd71d Merge branch 'aaqil-Branch' into mann-Branch 2025-04-21 01:08:26 -06:00
Mann Patel
635ba76ed4 Merge branch 'main' into mann-Branch 2025-04-21 01:04:03 -06:00
Mann Patel
5228bf73c9 refactor for redundant code 2025-04-21 01:01:58 -06:00
aruhani
6d2f736541 Updated transaction route file 2025-04-20 23:52:28 -06:00
aruhani
7670bb2b99 Pushing new delete icon 2025-04-20 23:46:22 -06:00
noahnghg
3c7a1a876a fix duplicate products 2025-04-20 23:42:22 -06:00
noahnghg
46bd77025f fix the routers and UI 2025-04-20 23:36:58 -06:00
aruhani
eff9d9d91b Updating Image URL 2025-04-20 23:22:15 -06:00
aruhani
89f5032212 Added a delete function 2025-04-20 23:07:01 -06:00
noahnghg
691980bf7c fix the controller again to show every transaction 2025-04-20 23:03:04 -06:00
noahnghg
3ea45b5400 fix the transaction UI 2025-04-20 23:01:16 -06:00
aruhani
644db7707c Update Frontend Transaction 2025-04-20 22:33:12 -06:00
Mann Patel
f82e111e39 uploads image folder 2025-04-20 22:07:02 -06:00
aruhani
d381ef7973 Transaction frontend update
Included a default message when no items are in the table
2025-04-20 22:03:04 -06:00
noahnghg
25f2c3d8af adding stuff to the onClick 2025-04-20 21:18:43 -06:00
aruhani
f7e4b49ac8 Backend for Transactions
Added in 3 files, for the backend
2025-04-20 10:42:52 -06:00
aruhani
90789b6fd4 Merge branch 'main' into aaqil 2025-04-19 00:00:44 -06:00
Mann Patel
d78b0c32e0 Merge branch 'mannBranch' into aaqil 2025-03-24 23:12:43 -06:00
aruhani
c2e13d56f2 Removing Password Temporarily 2025-03-24 22:43:06 -06:00
21 changed files with 1200 additions and 704 deletions

View File

@@ -391,9 +391,9 @@ exports.getProductWithPagination = async (req, res) => {
}
};
exports.removeProduct = async (req, res) => {
exports.removeAnyProduct = async (req, res) => {
const { id } = req.params;
console.log(id);
try {
const [result] = await db.execute(
`DELETE FROM Product WHERE ProductID = ?`,

View File

@@ -6,16 +6,16 @@ exports.getTransactionWithPagination = async (req, res) => {
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
`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()]
[limit.toString(), offset.toString()],
);
const [result] = await db.execute(
"SELECT COUNT(*) AS count FROM Transaction"
"SELECT COUNT(*) AS count FROM Transaction",
);
const { count: total } = result[0];
return res.json({ data, total });
@@ -29,7 +29,7 @@ exports.removeTransation = async (req, res) => {
try {
const [result] = await db.execute(
"DELETE FROM Transaction WHERE TransactionID = ?;",
[id.toString()]
[id.toString()],
);
return res.json({ message: "Remove transaction successfully!" });
} catch (error) {
@@ -38,3 +38,188 @@ exports.removeTransation = async (req, res) => {
.json({ error: "Cannot remove transactions from database!" });
}
};
// Create a new transaction
exports.createTransaction = async (req, res) => {
const { userID, productID, date, paymentStatus } = req.body;
try {
// Check if the transaction already exists for the same user and product
const [existingTransaction] = await db.execute(
`SELECT TransactionID FROM Transaction WHERE UserID = ? AND ProductID = ?`,
[userID, productID],
);
if (existingTransaction.length > 0) {
return res.status(400).json({
success: false,
message: "Transaction already exists for this user and product",
});
}
// Format the date
const formattedDate = new Date(date)
.toISOString()
.slice(0, 19)
.replace("T", " ");
// Insert the new transaction
const [result] = await db.execute(
`INSERT INTO Transaction (UserID, ProductID, Date, PaymentStatus)
VALUES (?, ?, ?, ?)`,
[userID, productID, formattedDate, paymentStatus],
);
res.json({
success: true,
message: "Transaction created successfully",
transactionID: result.insertId,
});
} catch (error) {
console.error("Error creating transaction:", error);
res.status(500).json({ error: "Could not create transaction" });
}
};
// Get all transactions for a given product
exports.getTransactionsByProduct = async (req, res) => {
const { productID } = req.params;
try {
const [transactions] = await db.execute(
`SELECT
T.TransactionID,
T.UserID,
T.ProductID,
T.Date,
T.PaymentStatus,
P.Name AS ProductName,
MIN(I.URL) AS Image_URL
FROM Transaction T
JOIN Product P ON T.ProductID = P.ProductID
LEFT JOIN Image_URL I ON P.ProductID = I.ProductID
GROUP BY T.TransactionID, T.UserID, T.ProductID, T.Date, T.PaymentStatus, P.Name`,
);
res.json({
success: true,
transactions,
});
} catch (error) {
console.error("Error fetching transactions by product:", error);
res.status(500).json({ error: "Could not retrieve transactions" });
}
};
// Get all transactions for a given user
exports.getTransactionsByUser = async (req, res) => {
const { userID } = req.body;
try {
const [transactions] = await db.execute(
`SELECT
T.TransactionID,
T.UserID,
T.ProductID,
T.Date,
T.PaymentStatus,
P.Name AS ProductName,
I.URL AS Image_URL
FROM Transaction T
JOIN Product P ON T.ProductID = P.ProductID
LEFT JOIN Image_URL I ON P.ProductID = I.ProductID
WHERE T.UserID = ?`,
[userID],
);
res.json({
success: true,
transactions,
});
} catch (error) {
console.error("Error fetching transactions by user:", error);
res.status(500).json({ error: "Could not retrieve transactions" });
}
};
// Get all transactions in the system
exports.getAllTransactions = async (req, res) => {
try {
const [transactions] = await db.execute(
`SELECT
T.TransactionID,
T.UserID,
T.ProductID,
T.Date,
T.PaymentStatus,
P.Name AS ProductName,
MIN(I.URL) AS Image_URL
FROM Transaction T
JOIN Product P ON T.ProductID = P.ProductID
LEFT JOIN Image_URL I ON P.ProductID = I.ProductID
GROUP BY T.TransactionID, T.UserID, T.ProductID, T.Date, T.PaymentStatus, P.Name`,
);
res.json({
success: true,
transactions,
});
} catch (error) {
console.error("Error fetching all transactions:", error);
res.status(500).json({ error: "Could not retrieve transactions" });
}
};
// Update the payment status of a transaction
exports.updatePaymentStatus = async (req, res) => {
const { transactionID, paymentStatus } = req.body;
try {
const [result] = await db.execute(
`UPDATE Transaction
SET PaymentStatus = ?
WHERE TransactionID = ?`,
[paymentStatus, transactionID],
);
if (result.affectedRows === 0) {
return res
.status(404)
.json({ success: false, message: "Transaction not found" });
}
res.json({
success: true,
message: "Payment status updated successfully",
});
} catch (error) {
console.error("Error updating payment status:", error);
res.status(500).json({ error: "Could not update payment status" });
}
};
// Delete a transaction
exports.deleteTransaction = async (req, res) => {
const { transactionID } = req.body;
try {
const [result] = await db.execute(
`DELETE FROM Transaction
WHERE TransactionID = ?`,
[transactionID],
);
if (result.affectedRows === 0) {
return res
.status(404)
.json({ success: false, message: "Transaction not found" });
}
res.json({
success: true,
message: "Transaction deleted successfully",
});
} catch (error) {
console.error("Error deleting transaction:", error);
res.status(500).json({ error: "Could not delete transaction" });
}
};

View File

@@ -9,7 +9,9 @@ const searchRouter = require("./routes/search");
const recommendedRouter = require("./routes/recommendation");
const history = require("./routes/history");
const review = require("./routes/review");
const categoryRouter = require("./routes/category");
const transactionRouter = require("./routes/transaction");
const { generateEmailTransporter } = require("./utils/mail");
@@ -44,9 +46,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", categoryRouter);
app.use("/api/transaction", transactionRouter);
app.use("/api/category", categoryRouter);
app.use("/api/transaction", transactionRouter);
// Set up a scheduler to run cleanup every hour
clean_up_time = 30 * 60 * 1000;

View File

@@ -8,6 +8,7 @@ const {
getProductById,
addProduct,
removeProduct,
removeAnyProduct,
getProductWithPagination,
myProduct,
updateProduct,
@@ -30,7 +31,7 @@ router.post("/addProduct", addProduct);
router.get("/getProduct", getAllProducts);
//Remove product
router.delete("/:id", removeProduct);
router.delete("/any/:id", removeAnyProduct);
//Get products with pagination
router.get("/getProductWithPagination", getProductWithPagination);

View File

@@ -1,11 +1,41 @@
// routes/transaction.js
const express = require("express");
const {
createTransaction,
getTransactionsByProduct,
getTransactionsByUser,
getAllTransactions,
updatePaymentStatus,
deleteTransaction,
getTransactionWithPagination,
removeTransation,
} = require("../controllers/transaction");
const router = express.Router();
// logging middleware
router.use((req, res, next) => {
console.log(`Incoming ${req.method} ${req.originalUrl}`);
next();
});
// Create a new transaction
router.post("/createTransaction", createTransaction);
// Get all transactions for a specific product
router.get("/getTransactionsByProduct/:productID", getTransactionsByProduct);
// Get all transactions for a specific user
router.post("/getTransactionsByUser", getTransactionsByUser);
// Get all transactions in the system
router.post("/getAllTransactions", getAllTransactions);
// Update payment status on a transaction
router.patch("/updatePaymentStatus", updatePaymentStatus);
// Delete a transaction
router.delete("/deleteTransaction", deleteTransaction);
router.get("/getTransactions", getTransactionWithPagination);
router.delete("/:id", removeTransation);

View File

@@ -18,7 +18,7 @@ async function sendVerificationEmail(email, verificationCode) {
// Clean up expired verification codes (run this periodically)
function cleanupExpiredCodes() {
db_con.query(
db.query(
"DELETE FROM AuthVerification WHERE Date < DATE_SUB(NOW(), INTERVAL 15 MINUTE) AND Authenticated = 0",
(err, result) => {
if (err) {

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

@@ -30,6 +30,8 @@ const ProductDetail = () => {
const [reviews, setReviews] = useState([]);
const [showReviewForm, setShowReviewForm] = useState(false);
const [showAlert, setShowAlert] = useState(false);
const [showAlert1, setShowAlert1] = useState(false);
const storedUser = JSON.parse(sessionStorage.getItem("user"));
const toggleFavorite = async (id) => {
@@ -303,6 +305,12 @@ const ProductDetail = () => {
onClose={() => setShowAlert(false)}
/>
)}
{showAlert1 && (
<FloatingAlert
message="Product added to transaction!"
onClose={() => setShowAlert1(false)}
/>
)}
<div className="flex flex-col md:flex-row gap-8">
<div className="md:w-3/5">
@@ -414,8 +422,40 @@ const ProductDetail = () => {
<div className="relative">
<button
onClick={() => setShowContactOptions(!showContactOptions)}
className="w-full bg-emerald-700 hover:bg-emerald-700 text-white font-medium py-3 px-4 mb-3"
onClick={async () => {
try {
// Create a transaction record
const transactionData = {
userID: storedUser.ID, // User ID from session storage
productID: product.ProductID, // Product ID from the product details
date: new Date().toISOString(), // Current date in ISO format
paymentStatus: "Pending", // Default payment status
};
const response = await fetch(
"http://localhost:3030/api/transaction/createTransaction",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(transactionData),
},
);
const result = await response.json();
if (result.success) {
setShowAlert1(true);
}
// Toggle contact options visibility
setShowContactOptions(!showContactOptions);
} catch (error) {
console.error("Error creating transaction:", error);
alert(`Error: ${error.message}`);
}
}}
className="w-full bg-green-500 hover:bg-green-600 text-white font-medium py-3 px-4 mb-3"
>
Contact Seller
</button>

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,8 +1,213 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { Calendar, CreditCard, Trash2 } from "lucide-react";
const Transactions = () => {
return <div></div>;
const [transactions, setTransactions] = useState([]);
useEffect(() => {
const fetchTransactions = async () => {
try {
const response = await fetch(
"http://localhost:3030/api/transaction/getAllTransactions",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userID: 1 }), // replace with actual userID
}
);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const { transactions: txData } = await response.json();
if (!Array.isArray(txData)) return;
setTransactions(
txData.map((tx) => ({
id: tx.TransactionID,
productId: tx.ProductID,
name: tx.ProductName || "Unnamed Product",
price: tx.Price != null ? parseFloat(tx.Price) : null,
image: tx.Image_URL || "/default-image.jpg",
date: tx.Date,
status: tx.PaymentStatus,
}))
);
} catch (error) {
console.error("Failed to fetch transactions:", error);
}
};
fetchTransactions();
}, []);
const deleteTransaction = async (id) => {
try {
const res = await fetch(
"http://localhost:3030/api/transaction/deleteTransaction",
{
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ transactionID: id }),
}
);
const data = await res.json();
if (data.success) {
setTransactions((prev) => prev.filter((tx) => tx.id !== id));
} else {
console.error("Delete failed:", data.message);
}
} catch (err) {
console.error("Error deleting transaction:", err);
}
};
const formatDate = (dateString) => {
const d = new Date(dateString);
return d.toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
});
};
return (
<div className="max-w-6xl mx-auto">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold text-gray-800">My Transactions</h1>
</div>
{transactions.length === 0 ? (
<div className="bg-white border border-gray-200 p-8 text-center">
<CreditCard className="h-12 w-12 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-medium text-gray-700 mb-2">
No transactions found
</h3>
<p className="text-gray-500 mb-4">
Once transactions are created, theyll appear here.
</p>
<Link
to="/"
className="inline-block bg-emerald-500 hover:bg-emerald-600 text-white font-medium py-2 px-4"
>
Browse Listings
</Link>
</div>
) : (
<>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{transactions.map((tx) => (
<div
key={tx.id}
className="relative border-2 border-gray-200 overflow-hidden hover:shadow-md transition-shadow"
>
{/* Delete Button */}
<button
onClick={(e) => {
e.preventDefault();
deleteTransaction(tx.id);
}}
className="absolute bottom-2 right-2 text-red-500 hover:text-red-600 z-10"
>
<Trash2 size={20} />
</button>
<Link to={`/product/${tx.productId}`}>
<div className="h-48 bg-gray-200 flex items-center justify-center">
{tx.image ? (
<img
src={tx.image}
alt={tx.name}
className="w-full h-full object-cover"
/>
) : (
<div className="text-gray-400">No image</div>
)}
</div>
<div className="p-4">
<h3 className="text-lg font-semibold text-gray-800">
{tx.name}
</h3>
{tx.price !== null && (
<p className="text-emerald-600 font-bold mt-1">
${tx.price.toFixed(2)}
</p>
)}
<div className="flex items-center text-gray-500 text-sm mt-2">
<Calendar className="mr-1" size={16} />
{formatDate(tx.date)}
</div>
<p className="text-gray-600 text-sm mt-1">
Status: <span className="font-medium">{tx.status}</span>
</p>
</div>
</Link>
</div>
))}
</div>
<div className="mt-6 text-sm text-gray-500">
Showing {transactions.length}{" "}
{transactions.length === 1 ? "transaction" : "transactions"}
</div>
</>
)}
<footer className="bg-gray-800 text-white py-6 mt-12">
<div className="container mx-auto px-4">
<div className="flex flex-col md:flex-row justify-between items-center">
<div className="mb-4 md:mb-0">
<h3 className="text-lg font-semibold mb-2">
Campus Marketplace
</h3>
<p className="text-gray-400 text-sm">
Your trusted university trading platform
</p>
</div>
<div className="flex space-x-6">
<div>
<h4 className="font-medium mb-2">Quick Links</h4>
<ul className="text-sm text-gray-400">
<li className="mb-1">
<Link to="/" className="hover:text-white transition">
Home
</Link>
</li>
<li className="mb-1">
<Link
to="/selling"
className="hover:text-white transition"
>
Sell an Item
</Link>
</li>
<li className="mb-1">
<Link
to="/favorites"
className="hover:text-white transition"
>
My Favorites
</Link>
</li>
</ul>
</div>
<div>
<h4 className="font-medium mb-2">Contact</h4>
<ul className="text-sm text-gray-400">
<li className="mb-1">support@campusmarket.com</li>
<li className="mb-1">University of Calgary</li>
</ul>
</div>
</div>
</div>
<div className="border-t border-gray-700 mt-6 pt-6 text-center text-sm text-gray-400">
<p>
© {new Date().getFullYear()} Campus Marketplace. All rights
reserved.
</p>
</div>
</div>
</footer>
</div>
);
};
export default Transactions;

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

View File

@@ -101,7 +101,8 @@ VALUES
('Photography Equipment'),
('Event Tickets'),
('Software Licenses'),
('Transportation (Car Pool)');
('Transportation (Car Pool)'),
('Other');
-- Insert Products
INSERT INTO

View File

@@ -28,7 +28,6 @@ CREATE TABLE Category (
Name VARCHAR(255) NOT NULL
);
-- Product Entity
CREATE TABLE Product (
ProductID INT AUTO_INCREMENT PRIMARY KEY,
Name VARCHAR(255) NOT NULL,
@@ -38,8 +37,22 @@ CREATE TABLE Product (
Description TEXT,
CategoryID INT NOT NULL,
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID),
FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID)
);
-- Product Entity
CREATE TABLE Product (
ProductID INT AUTO_INCREMENT PRIMARY KEY,
Name VARCHAR(255) NOT NULL,
Price DECIMAL(10, 2) NOT NULL,
StockQuantity INT,
UserID INT,
Description TEXT,
CategoryID INT,
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE SET NULL,
FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID) ON DELETE SET NULL
FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID)
);
-- Fixed Image_URL table
@@ -66,13 +79,13 @@ CREATE TABLE Review (
-- Transaction Entity (Many-to-One with User, Many-to-One with Product)
CREATE TABLE Transaction (
TransactionID INT PRIMARY KEY,
TransactionID INT AUTO_INCREMENT 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 SET NULL
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE SET NULL
);
-- Recommendation Entity (Many-to-One with User, Many-to-One with Product)