7 Commits

Author SHA1 Message Date
estherdev03
15eddb8d13 Fix clearn up expired code 2025-04-20 22:14:04 -06:00
estherdev03
2e98a88db9 Merge branch 'esther2' of https://github.com/MannPatel0/Campus-Plug into esther2 2025-04-20 22:08:38 -06:00
Esther Tran
bcb912b6ce Merge pull request #5 from MannPatel0/main
merge
2025-04-20 22:08:03 -06:00
estherdev03
e83b3640a5 Merge branch 'esther2' of https://github.com/MannPatel0/Campus-Plug into esther2 2025-04-20 22:05:42 -06:00
estherdev03
bd69bed934 Merge branch 'main' of https://github.com/MannPatel0/Campus-Plug into esther2 2025-04-20 22:03:32 -06:00
estherdev03
51dffdae30 fix clean up expired code 2025-04-20 22:00:19 -06:00
Esther Tran
8dcaff3d6d Merge pull request #4 from MannPatel0/main
Merge pull request #3 from MannPatel0/esther2
2025-04-20 21:01:54 -06:00
34 changed files with 1121 additions and 1366 deletions

View File

@@ -25,13 +25,12 @@
1. python3 server.py #Start The Server
```
---
### Recommendation system
1. Install the dependencies
```Bash
pip install mysql.connector
```
---
### Database
1. MySql Version 9.2.0

View File

@@ -7,7 +7,7 @@ exports.getAllCategoriesWithPagination = async (req, res) => {
try {
const [data, _] = await db.execute(
"SELECT * FROM Category C ORDER BY C.CategoryID ASC LIMIT ? OFFSET ?",
[limit.toString(), offset.toString()]
[limit.toString(), offset.toString()],
);
const [result] = await db.execute("SELECT COUNT(*) AS count FROM Category");
@@ -24,7 +24,7 @@ exports.addCategory = async (req, res) => {
try {
const [result] = await db.execute(
"INSERT INTO Category (Name) VALUES (?)",
[name]
[name],
);
res.json({ message: "Adding new category successfully!" });
} catch (error) {
@@ -34,23 +34,15 @@ exports.addCategory = async (req, res) => {
exports.removeCategory = async (req, res) => {
const { id } = req.params;
try {
if (id == "1") throw Error("You're not allowed to delete this category!");
const [updateResult] = await db.execute(
"UPDATE Product SET CategoryID = 1 WHERE CategoryID = ?",
[id]
);
try {
const [result] = await db.execute(
`DELETE FROM Category WHERE CategoryID = ?`,
[id]
[id],
);
res.json({ message: "Delete category successfully!" });
} catch (error) {
res.json({
error: error.message || "Cannot remove category from database!",
});
res.json({ error: "Cannot remove category from database!" });
}
};

View File

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

View File

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

View File

@@ -1,41 +1,11 @@
// 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

@@ -4,6 +4,7 @@ const pool = mysql.createPool({
host: "localhost",
user: "root",
database: "Marketplace",
password: "12345678",
});
// const pool = mysql.createPool(

View File

@@ -1,4 +1,5 @@
const { generateEmailTransporter } = require("./mail");
const db = require("../utils/database");
// Helper function to send email
async function sendVerificationEmail(email, verificationCode) {
@@ -17,18 +18,17 @@ async function sendVerificationEmail(email, verificationCode) {
}
// Clean up expired verification codes (run this periodically)
function cleanupExpiredCodes() {
db.query(
"DELETE FROM AuthVerification WHERE Date < DATE_SUB(NOW(), INTERVAL 15 MINUTE) AND Authenticated = 0",
(err, result) => {
if (err) {
console.error("Error cleaning up expired codes:", err);
} else {
console.log(`Cleaned up ${result} expired verification codes`);
}
}
);
}
const cleanupExpiredCodes = () => {
db.execute(
"DELETE FROM AuthVerification WHERE Date < DATE_SUB(NOW(), INTERVAL 15 MINUTE) AND Authenticated = 0"
)
.then((res) => {
console.log(`Cleaned up expired verification codes`);
})
.catch((err) => {
console.error("Error cleaning up expired codes:", err);
});
};
const checkDatabaseConnection = async (db) => {
try {

View File

@@ -4,7 +4,6 @@ import {
Routes,
Route,
Navigate,
useLocation,
} from "react-router-dom";
import Navbar from "./components/Navbar";
import Home from "./pages/Home";
@@ -13,10 +12,14 @@ 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";
import Dashboard from "./pages/Dashboard"; // The single consolidated dashboard component
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";
import TransactionDashboard from "./pages/TransactionDashboard";
function App() {
// Authentication state - initialize from localStorage if available
@@ -39,18 +42,6 @@ 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);
@@ -136,6 +127,9 @@ function App() {
}
};
const [isAdmin, setIsAdmin] = useState(false);
const [showAdminDashboard, setShowAdminDashboard] = useState(false);
useEffect(() => {
const userInfo = sessionStorage.getItem("user")
? JSON.parse(sessionStorage.getItem("user"))
@@ -148,14 +142,9 @@ 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
@@ -292,8 +281,7 @@ function App() {
// Set authenticated user
setUser(newUser);
setIsSignUp(false);
//setIsAuthenticated(true);
setIsAuthenticated(true);
// Save to localStorage to persist across refreshes
sessionStorage.setItem("isAuthenticated", "true");
@@ -339,11 +327,12 @@ function App() {
setError("Email and password are required");
setIsLoading(false);
return;
} else if (!formValues.email.endsWith("@ucalgary.ca")) {
setError("Please use your UCalgary email address (@ucalgary.ca)");
setIsLoading(false);
return;
}
// else if (!formValues.email.endsWith("@ucalgary.ca")) {
// setError("Please use your UCalgary email address (@ucalgary.ca)");
// setIsLoading(false);
// return;
// }
try {
if (isSignUp) {
// Handle Sign Up with verification
@@ -447,7 +436,6 @@ function App() {
setVerificationStep("initial");
setTempUserData(null);
setRecommendations([]);
setShowAdminDashboard(false);
// Clear localStorage
sessionStorage.removeItem("user");
@@ -517,7 +505,7 @@ function App() {
// Loading overlay component
const LoadingOverlay = () => (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="animate-spin rounded-full h-12 w-12 border-4 border-emerald-600 border-t-transparent"></div>
<div className="animate-spin rounded-full h-12 w-12 border-4 border-green-500 border-t-transparent"></div>
</div>
);
@@ -584,7 +572,7 @@ function App() {
id="name"
name="name"
placeholder="Enter your name"
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-emerald-600 focus:ring-1 focus:ring-emerald-600"
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-green-500 focus:ring-1 focus:ring-green-500"
required={isSignUp}
/>
</div>
@@ -603,7 +591,7 @@ function App() {
id="ucid"
name="ucid"
placeholder="1234567"
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-emerald-600 focus:ring-1 focus:ring-emerald-600"
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-green-500 focus:ring-1 focus:ring-green-500"
required={isSignUp}
/>
</div>
@@ -621,7 +609,7 @@ function App() {
id="email"
name="email"
placeholder="your.email@ucalgary.ca"
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-emerald-600 focus:ring-1 focus:ring-emerald-600"
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-green-500 focus:ring-1 focus:ring-green-500"
required
/>
</div>
@@ -639,7 +627,7 @@ function App() {
id="phone"
name="phone"
placeholder="+1(123)456 7890"
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-emerald-600 focus:ring-1 focus:ring-emerald-600"
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-green-500 focus:ring-1 focus:ring-green-500"
required={isSignUp}
/>
</div>
@@ -658,7 +646,7 @@ function App() {
id="address"
name="address"
placeholder="Your address"
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-emerald-600 focus:ring-1 focus:ring-emerald-600"
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-green-500 focus:ring-1 focus:ring-green-500"
required={isSignUp}
/>
</div>
@@ -680,7 +668,7 @@ function App() {
? "Create a secure password"
: "Enter your password"
}
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-emerald-600 focus:ring-1 focus:ring-emerald-600"
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-green-500 focus:ring-1 focus:ring-green-500"
required
/>
</div>
@@ -689,7 +677,7 @@ function App() {
<button
type="submit"
disabled={isLoading}
className="w-full px-6 py-2 text-base font-medium text-white bg-emerald-600 hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-emerald-400 focus:ring-offset-2 transition-colors disabled:bg-emerald-300"
className="w-full px-6 py-2 text-base font-medium text-white bg-green-500 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-400 focus:ring-offset-2 transition-colors disabled:bg-green-300"
>
{isLoading
? "Please wait..."
@@ -716,7 +704,7 @@ function App() {
id="verificationCode"
name="verificationCode"
placeholder="Enter the 6-digit code"
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-emerald-600 focus:ring-1 focus:ring-emerald-600"
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-green-500 focus:ring-1 focus:ring-green-500"
required
/>
<p className="mt-1 text-xs text-gray-500">
@@ -728,7 +716,7 @@ function App() {
<button
type="submit"
disabled={isLoading}
className="w-full px-6 py-2 text-base font-medium text-white bg-emerald-600 hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-emerald-400 focus:ring-offset-2 transition-colors disabled:bg-emerald-300"
className="w-full px-6 py-2 text-base font-medium text-white bg-green-500 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-400 focus:ring-offset-2 transition-colors disabled:bg-green-300"
>
{isLoading ? "Please wait..." : "Verify Code"}
</button>
@@ -746,7 +734,7 @@ function App() {
type="button"
onClick={handleResendCode}
disabled={isLoading}
className="text-sm text-emerald-600 hover:text-emerald-700"
className="text-sm text-green-500 hover:text-green-700"
>
Resend code
</button>
@@ -764,7 +752,7 @@ function App() {
<button
onClick={toggleAuthMode}
type="button"
className="text-emerald-600 font-medium hover:text-emerald-700"
className="text-green-500 font-medium hover:text-green-700"
>
{isSignUp ? "Sign in" : "Sign up"}
</button>
@@ -786,119 +774,126 @@ function App() {
return children;
};
return (
<Router>
{/* If admin dashboard should be shown */}
{showAdminDashboard ? (
// If user is admin, show admin naviagtion
if (showAdminDashboard) {
return (
<Router>
<div className="flex">
<DashboardNav handleCloseAdminDashboard={handleCloseAdminDashboard} />
<Routes>
{/* 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" />} />
{/* 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 />} />
</Routes>
</div>
) : (
/* Normal user interface */
<div className="min-h-screen bg-gray-50">
{/* Show loading overlay when generating recommendations */}
{isGeneratingRecommendations && <LoadingOverlay />}
</Router>
);
}
{/* 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>
)}
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>
</Router>
);
}

View File

@@ -1,20 +1,77 @@
// api.js
import client from "./client";
import axios from "axios";
const client = axios.create({
baseURL: "http://localhost:3030/api",
});
// Users
export const getUsers = async (page = 1, limit = 10) => {
export const getUsers = async (page, limit = 10) => {
try {
const { data } = await client.get(
`/user/getUserWithPagination?page=${page}&limit=${limit}`
);
return { users: data.users, total: data.total };
} catch (error) {
return handleError(error);
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 };
}
};
@@ -23,7 +80,20 @@ export const removeUser = async (id) => {
const { data } = await client.post(`/user/delete`, { userId: id });
return { message: data.message };
} catch (error) {
return handleError(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 };
}
};
@@ -32,71 +102,9 @@ export const verifyIsAdmin = async (id) => {
const { data } = await client.get(`/user/isAdmin/${id}`);
return { isAdmin: data.isAdmin };
} catch (error) {
return handleError(error);
}
};
// Products
export const getProducts = async (page = 1, 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}`);
if (data.error) throw Error(data.error);
return { message: data.message };
} catch (error) {
return handleError(error);
}
};
// Transactions
export const getTransactions = async (page = 1, 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);
const { response } = error;
if (response?.data) return response.data;
return { error: error.message || error };
}
};
@@ -105,16 +113,8 @@ export const removeTransaction = async (id) => {
const { data } = await client.delete(`/transaction/${id}`);
return { message: data.message };
} catch (error) {
return handleError(error);
const { response } = error;
if (response?.data) return response.data;
return { error: error.message || error };
}
};
// Shared Error Handler
const handleError = (error) => {
const { response } = error;
if (response?.data) return response.data;
return alert(error.message || error);
};
// Optional: export client if you want to use it elsewhere
export default client;

View File

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

View File

@@ -22,7 +22,7 @@ export default function CategoryForm({ visible, onAddCategory }) {
.classList.remove("opacity-0", "bg-red-200", "text-red-500");
document
.getElementById("noti")
.classList.add("bg-emerald-200", "text-emerald-800");
.classList.add("bg-green-200", "text-green-800");
document.getElementById("noti").innerHTML = `${message.message}`;
setCategory("");
onAddCategory();
@@ -42,18 +42,18 @@ export default function CategoryForm({ visible, onAddCategory }) {
return (
<form onSubmit={handleSubmit} action="" className="flex p-2 items-center">
<label htmlFor="category" className="text-emerald-700">
<label htmlFor="category" className="text-green-700">
Category:
</label>
<input
type="text"
className="border border-emerald-700 ml-2 rounded-sm focus:bg-emerald-100 text-emerald-900"
className="border border-green-700 ml-2 rounded-sm focus:bg-green-100 text-green-900"
name="category"
id="category"
onChange={handleChange}
value={category}
/>
<button type="submit" className="text-2xl pl-1 text-emerald-700">
<button type="submit" className="text-2xl pl-1 text-green-700">
<MdAddBox className="text-3xl" />
</button>
<p

View File

@@ -1,16 +1,98 @@
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 className="w-48 min-w-[12rem] bg-gray-100 text-emerald-600 flex flex-col p-4 shadow-md">
<h2 className="text-lg font-semibold mb-4">Admin Dashboard</h2>
<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>
<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>
);
}

View File

@@ -11,7 +11,7 @@ const FloatingAlert = ({ message, onClose, duration = 3000 }) => {
}, [onClose, duration]);
return (
<div className="fixed top-4 left-1/2 transform -translate-x-1/2 bg-emerald-600 text-white px-4 py-2 rounded-xl shadow-lg z-50 text-center">
<div className="fixed top-4 left-1/2 transform -translate-x-1/2 bg-emerald-500 text-white px-4 py-2 rounded-xl shadow-lg z-50 text-center">
{message}
</div>
);

View File

@@ -35,7 +35,7 @@ const Navbar = ({ onLogout, userName, isAdmin, handleShowAdminDashboard }) => {
alt="Campus Plug"
className="h-8 px-2"
/>
<span className="hidden md:block text-emerald-700 font-bold text-xl">
<span className="hidden md:block text-emerald-600 font-bold text-xl">
Campus Plug
</span>
</Link>

View File

@@ -32,7 +32,7 @@ export default function Pagination({ pageNum, onChange }) {
onClick={() => {
handleTogglePage("previous");
}}
className=" flex items-center justify-center px-3 h-8 ms-0 leading-tight border-e-0 border-gray-300 rounded-s-lg hover:bg-gray-100 text-white bg-emerald-700 border border-gray-300 hover:bg-emerald-700 hover:text-white"
className=" flex items-center justify-center px-3 h-8 ms-0 leading-tight border-e-0 border-gray-300 rounded-s-lg hover:bg-gray-100 text-white bg-green-700 border border-gray-300 hover:bg-green-600 hover:text-white"
>
<span className="sr-only">Previous</span>
<svg
@@ -56,9 +56,9 @@ export default function Pagination({ pageNum, onChange }) {
<li key={page}>
<NavLink
className={`${
currentPage == page ? "bg-emerald-700" : "bg-emerald-700"
currentPage == page ? "bg-green-600" : "bg-green-700"
} +
" flex items-center justify-center px-3 h-8 leading-tight text-white border border-gray-300 hover:bg-emerald-700 hover:text-white"`}
" flex items-center justify-center px-3 h-8 leading-tight text-white border border-gray-300 hover:bg-green-600 hover:text-white"`}
onClick={() => {
handleClick(page);
}}
@@ -72,7 +72,7 @@ export default function Pagination({ pageNum, onChange }) {
onClick={() => {
handleTogglePage("next");
}}
className="flex items-center justify-center px-3 h-8 leading-tight border border-gray-300 rounded-e-lg text-white bg-emerald-700 border border-gray-300 hover:bg-emerald-700 hover:text-white"
className="flex items-center justify-center px-3 h-8 leading-tight border border-gray-300 rounded-e-lg text-white bg-green-700 border border-gray-300 hover:bg-green-600 hover:text-white"
>
<span className="sr-only">Next</span>
<svg

View File

@@ -55,7 +55,7 @@ const UserDropdown = ({
onClick={toggleDropdown}
>
<div className="h-8 w-8 rounded-full bg-emerald-100 flex items-center justify-center">
<User className="h-5 w-5 text-emerald-700" />
<User className="h-5 w-5 text-emerald-600" />
</div>
</button>

View File

@@ -0,0 +1,105 @@
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,366 +1,7 @@
import { useEffect, useState, useCallback, useRef } 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 Pagination from "../components/Pagination";
import CategoryForm from "../components/CategoryForm";
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-emerald-600 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 currentTab = useRef();
//Reset the current page to 1 whenever we switch between tab
useEffect(() => {
if (currentTab.current != idKey) setCurrentPage(1);
currentTab.current = idKey;
}, [idKey]);
const fetchItems = useCallback(
(page = 1, limit = 10) => {
fetchDataFn(page, limit)
.then((res) => {
console.log(res);
const data =
res.users || res.products || res.transactions || res.data || [];
setItems(data);
setTotal(res.total);
})
.catch((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 />;
export default function Dashboard() {
return (
<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-emerald-700">{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-emerald-700">
<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-emerald-600 rounded-md px-4 py-2 text-white text-sm hover:bg-emerald-700 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">
{/* Mobile Tabs */}
<div className="md:hidden w-full mb-4">
<select
className="w-full rounded-md border-gray-300 shadow-sm focus:border-emerald-600 focus:ring focus:ring-emerald-600 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-emerald-700 bg-white border-l border-t border-r border-gray-200 border-b-0"
: "text-gray-600 hover:text-emerald-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 className="text-3xl font-bold p-3 text-green-800">
Welcome to admin dashboard
</div>
);
}

View File

@@ -119,7 +119,7 @@ const Favorites = () => {
</p>
<Link
to="/"
className="inline-block bg-emerald-600 hover:bg-emerald-700 text-white font-medium py-2 px-4"
className="inline-block bg-emerald-500 hover:bg-emerald-600 text-white font-medium py-2 px-4"
>
Browse Listings
</Link>
@@ -161,7 +161,7 @@ const Favorites = () => {
</button>
</div>
<p className="text-emerald-700 font-bold mt-1">
<p className="text-emerald-600 font-bold mt-1">
${product.price.toFixed(2)}
</p>
@@ -200,8 +200,56 @@ const Favorites = () => {
)}
<footer className="bg-gray-800 text-white py-6 mt-12">
<div className="border-t border-gray-700 text-center text-sm text-gray-400">
<p>© 2025 Campus Marketplace. All rights reserved.</p>
<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>

View File

@@ -219,7 +219,7 @@ const Home = () => {
// Loading indicator component
const LoadingSection = () => (
<div className="flex justify-center items-center h-48">
<Loader className="animate-spin text-emerald-700 h-8 w-8" />
<Loader className="animate-spin text-emerald-600 h-8 w-8" />
</div>
);
@@ -243,7 +243,7 @@ const Home = () => {
e.preventDefault();
toggleFavorite(product.id);
}}
className="absolute top-0 right-0 p-2 rounded-bl-md bg-emerald-700 hover:bg-emerald-600 transition shadow-sm"
className="absolute top-0 right-0 p-2 rounded-bl-md bg-emerald-600 hover:bg-emerald-500 transition shadow-sm"
>
<Bookmark className="text-white w-5 h-5" />
</button>
@@ -253,7 +253,7 @@ const Home = () => {
<h3 className="text-lg font-medium text-gray-800 leading-tight">
{product.title}
</h3>
<span className="font-semibold text-emerald-700 block mt-1">
<span className="font-semibold text-emerald-600 block mt-1">
${product.price}
</span>
@@ -353,7 +353,7 @@ const Home = () => {
</p>
<button
onClick={handleSelling}
className="bg-emerald-700 hover:bg-emerald-700 text-white font-medium py-2 px-6 focus:outline-none focus:ring-2 focus:ring-emerald-400 transition-colors"
className="bg-emerald-600 hover:bg-emerald-700 text-white font-medium py-2 px-6 focus:outline-none focus:ring-2 focus:ring-emerald-400 transition-colors"
>
Post an Item
</button>
@@ -405,9 +405,59 @@ const Home = () => {
</ScrollableProductList>
)}
</div>
{/* Footer */}
<footer className="bg-gray-800 text-white py-6 mt-12">
<div className="border-t border-gray-700 text-center text-sm text-gray-400">
<p>© 2025 Campus Marketplace. All rights reserved.</p>
<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>

View File

@@ -0,0 +1,91 @@
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,8 +30,6 @@ 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) => {
@@ -247,7 +245,7 @@ const ProductDetail = () => {
if (loading.product) {
return (
<div className="flex justify-center items-center h-screen">
<div className="animate-spin h-32 w-32 border-t-2 border-emerald-700"></div>
<div className="animate-spin h-32 w-32 border-t-2 border-emerald-600"></div>
</div>
);
}
@@ -261,7 +259,7 @@ const ProductDetail = () => {
<p className="text-gray-600">{error.product}</p>
<Link
to="/"
className="mt-4 inline-block bg-emerald-700 text-white px-4 py-2 hover:bg-emerald-700"
className="mt-4 inline-block bg-emerald-600 text-white px-4 py-2 hover:bg-emerald-700"
>
Back to Listings
</Link>
@@ -278,7 +276,7 @@ const ProductDetail = () => {
<h2 className="text-2xl text-red-500 mb-4">Product Not Found</h2>
<Link
to="/"
className="mt-4 inline-block bg-emerald-700 text-white px-4 py-2 hover:bg-emerald-700"
className="mt-4 inline-block bg-emerald-600 text-white px-4 py-2 hover:bg-emerald-700"
>
Back to Listings
</Link>
@@ -305,12 +303,6 @@ 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">
@@ -356,7 +348,7 @@ const ProductDetail = () => {
{product.images.map((image, index) => (
<div
key={index}
className={`bg-white border ${currentImage === index ? "border-emerald-700 border-2" : "border-gray-200"} min-w-[100px] cursor-pointer`}
className={`bg-white border ${currentImage === index ? "border-emerald-600 border-2" : "border-gray-200"} min-w-[100px] cursor-pointer`}
onClick={() => selectImage(index)}
>
<img
@@ -386,7 +378,7 @@ const ProductDetail = () => {
e.preventDefault();
toggleFavorite(product.ProductID);
}}
className="top-0 p-2 rounded-bl-md bg-emerald-700 hover:bg-emerald-700 transition shadow-sm"
className="top-0 p-2 rounded-bl-md bg-emerald-700 hover:bg-emerald-600 transition shadow-sm"
>
<Bookmark className="text-white w-5 h-5" />
</button>
@@ -422,40 +414,8 @@ const ProductDetail = () => {
<div className="relative">
<button
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-emerald-600 hover:bg-emerald-700 text-white font-medium py-3 px-4 mb-3"
onClick={() => setShowContactOptions(!showContactOptions)}
className="w-full bg-emerald-700 hover:bg-emerald-700 text-white font-medium py-3 px-4 mb-3"
>
Contact Seller
</button>
@@ -467,7 +427,7 @@ const ProductDetail = () => {
href={`tel:${product.SellerPhone}`}
className="flex items-center gap-2 p-3 hover:bg-gray-50 border-b border-gray-100"
>
<Phone className="h-5 w-5 text-emerald-700" />
<Phone className="h-5 w-5 text-emerald-600" />
<span>Call Seller</span>
</a>
)}
@@ -477,7 +437,7 @@ const ProductDetail = () => {
href={`mailto:${product.SellerEmail}`}
className="flex items-center gap-2 p-3 hover:bg-gray-50"
>
<Mail className="h-5 w-5 text-emerald-700" />
<Mail className="h-5 w-5 text-emerald-600" />
<span>Email Seller</span>
</a>
)}
@@ -498,10 +458,7 @@ const ProductDetail = () => {
{product.SellerName || "Unknown Seller"}
</h3>
<p className="text-sm text-gray-500">
Product listed since{" "}
{product.Date
? new Date(product.Date).toLocaleDateString()
: "N/A"}
Member since {product.SellerJoinDate || "N/A"}
</p>
</div>
</div>
@@ -517,7 +474,7 @@ const ProductDetail = () => {
<div className="bg-white border border-gray-200 p-6">
{loading.reviews ? (
<div className="flex justify-center py-8">
<div className="animate-spin h-8 w-8 border-t-2 border-emerald-700"></div>
<div className="animate-spin h-8 w-8 border-t-2 border-emerald-600"></div>
</div>
) : error.reviews ? (
<div className="text-red-500 mb-4">
@@ -564,7 +521,7 @@ const ProductDetail = () => {
<div className="mt-4">
<button
onClick={() => setShowReviewForm(true)}
className="bg-emerald-700 hover:bg-emerald-700 text-white font-medium py-2 px-4"
className="bg-emerald-600 hover:bg-emerald-700 text-white font-medium py-2 px-4"
>
Write a Review
</button>
@@ -622,7 +579,7 @@ const ProductDetail = () => {
id="comment"
value={reviewForm.comment}
onChange={handleReviewInputChange}
className="w-full p-3 border border-gray-300 focus:outline-none focus:border-emerald-700"
className="w-full p-3 border border-gray-300 focus:outline-none focus:border-emerald-600"
rows="4"
required
></textarea>
@@ -638,7 +595,7 @@ const ProductDetail = () => {
</button>
<button
type="submit"
className="px-4 py-2 bg-emerald-700 text-white hover:bg-emerald-700"
className="px-4 py-2 bg-emerald-600 text-white hover:bg-emerald-700"
disabled={loading.submitting}
>
{loading.submitting ? "Submitting..." : "Submit Review"}

View File

@@ -146,7 +146,7 @@ const SearchPage = () => {
<div className="flex space-x-2">
<button
onClick={applyFilters}
className="w-full bg-emerald-600 text-white p-3 hover:bg-emerald-700 transition-colors"
className="w-full bg-emerald-500 text-white p-3 hover:bg-emerald-600 transition-colors"
>
Apply Filters
</button>
@@ -187,7 +187,7 @@ const SearchPage = () => {
<h3 className="text-lg font-medium text-gray-800">
{listing.title}
</h3>
<p className="text-emerald-700 font-semibold">
<p className="text-emerald-600 font-semibold">
${Number(listing.price).toFixed(2)}
</p>
</div>
@@ -196,9 +196,58 @@ const SearchPage = () => {
</div>
</div>
</div>
{/* Footer - Added here */}
<footer className="bg-gray-800 text-white py-6 mt-12">
<div className="border-t border-gray-700 text-center text-sm text-gray-400">
<p>© 2025 Campus Marketplace. All rights reserved.</p>
<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>

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from "react";
import { useLocation, Link } from "react-router-dom";
import { X, ChevronLeft, Trash2 } from "lucide-react";
import { X, ChevronLeft, Plus, Trash2 } from "lucide-react";
const Selling = () => {
const [products, setProducts] = useState([]);
@@ -8,13 +8,14 @@ const Selling = () => {
const storedUser = JSON.parse(sessionStorage.getItem("user"));
const [categories, setCategories] = useState([]);
const [categoryMapping, setCategoryMapping] = useState({});
const [selectedCategory, setSelectedCategory] = useState("");
const [originalProduct, setOriginalProduct] = useState(null);
const [editingProduct, setEditingProduct] = useState({
name: "",
price: "",
description: "",
category: "",
categories: [],
images: [],
});
@@ -58,7 +59,7 @@ const Selling = () => {
fetchCategories();
}, []);
// Fetch products from API/database on component mount
// Simulate fetching products from API/database on component mount
useEffect(() => {
const fetchProducts = async () => {
try {
@@ -88,87 +89,61 @@ const Selling = () => {
};
fetchProducts();
});
}, []); // Add userId to dependency array if it might change
// When editing a product, save the original product properly
const handleEditProduct = (product) => {
// Save the original product completely
setOriginalProduct(product);
// Convert category ID to category name if needed
const categoryName = getCategoryNameById(product.CategoryID);
setEditingProduct({
...product,
category: categoryName || "", // Single category string
images: product.images || [],
categories: categoryName ? [categoryName] : [],
images: product.images || [], // Ensure images array exists
});
setShowForm(true);
};
// Upload images to server and get their paths
const uploadImages = async (images) => {
console.log(images);
const uploadedImagePaths = [];
// Filter out only File objects (new images to upload)
const filesToUpload = images.filter((img) => img instanceof File);
for (const file of filesToUpload) {
// Create a FormData object to send the file
const formData = new FormData();
formData.append("image", file);
try {
// Send the file to your upload endpoint
const response = await fetch("http://localhost:3030/api/upload", {
method: "POST",
body: formData,
});
if (!response.ok) {
throw new Error(`Failed to upload image: ${file.name}`);
}
const result = await response.json();
// Assuming the server returns the path where the file was saved
uploadedImagePaths.push(`/public/uploads/${file.name}`);
} catch (error) {
console.error("Error uploading image:", error);
// If upload fails, still add the expected path (this is a fallback)
uploadedImagePaths.push(`/public/uploads/${file.name}`);
}
}
// Also include any existing image URLs that are strings, not File objects
const existingImages = images.filter((img) => typeof img === "string");
if (existingImages.length > 0) {
uploadedImagePaths.push(...existingImages);
}
return uploadedImagePaths;
};
// Handle saving product with updated image logic
// Then update the handleSaveProduct function to properly merge values
const handleSaveProduct = async () => {
if (!editingProduct.category) {
alert("Please select a category");
if (!(editingProduct.categories || []).length) {
alert("Please select at least one category");
return;
}
try {
let imagePaths = [];
const imagePaths = [];
// Handle image uploads and get their paths
// Handle images properly
if (editingProduct.images && editingProduct.images.length > 0) {
imagePaths = await uploadImages(editingProduct.images);
// If there are new images uploaded (File objects)
const newImages = editingProduct.images.filter(
(img) => img instanceof File,
);
newImages.forEach((file) => {
const simulatedPath = `/public/uploads/${file.name}`;
imagePaths.push(simulatedPath);
});
// Also include any existing image URLs that are strings, not File objects
const existingImages = editingProduct.images.filter(
(img) => typeof img === "string",
);
if (existingImages.length > 0) {
imagePaths.push(...existingImages);
}
} else if (originalProduct?.image_url) {
// If no new images but there was an original image URL
imagePaths = [originalProduct.image_url];
imagePaths.push(originalProduct.image_url);
}
const categoryName = (editingProduct.categories || [])[0];
const categoryID =
categoryMapping[editingProduct.category] ||
originalProduct?.CategoryID ||
1;
categoryMapping[categoryName] || originalProduct?.CategoryID || 1;
// Create payload with proper fallback to original values
const payload = {
@@ -191,7 +166,12 @@ const Selling = () => {
originalProduct?.Description ||
"",
category: categoryID,
images: imagePaths.length > 0 ? imagePaths : [],
images:
imagePaths.length > 0
? imagePaths
: originalProduct?.image_url
? [originalProduct.image_url]
: [],
};
console.log("Sending payload:", payload);
@@ -226,7 +206,7 @@ const Selling = () => {
name: "",
price: "",
description: "",
category: "",
categories: [],
images: [],
});
@@ -263,7 +243,7 @@ const Selling = () => {
throw new Error("Network response was not ok");
}
} catch (error) {
console.error("Error deleting product:", error);
console.error("Error fetching products:", error);
// You might want to set an error state here
}
};
@@ -287,18 +267,53 @@ const Selling = () => {
name: "",
price: "",
description: "",
category: "",
categories: [],
images: [],
});
setShowForm(true);
};
// Handle category change
const handleCategoryChange = (e) => {
setEditingProduct({
...editingProduct,
category: e.target.value,
});
const addCategory = () => {
if (
selectedCategory &&
!(editingProduct.categories || []).includes(selectedCategory)
) {
setEditingProduct((prev) => ({
...prev,
categories: [...(prev.categories || []), selectedCategory],
}));
setSelectedCategory("");
}
};
const removeCategory = (categoryToRemove) => {
setEditingProduct((prev) => ({
...prev,
categories: (prev.categories || []).filter(
(cat) => cat !== categoryToRemove,
),
}));
};
const markAsSold = async () => {
// This would call an API to move the product to the transaction table
try {
// API call would go here
console.log(
"Moving product to transaction table:",
editingProduct.ProductID,
);
// Toggle the sold status in the UI
setEditingProduct((prev) => ({
...prev,
isSold: !prev.isSold,
}));
// You would add your API call here to update the backend
} catch (error) {
console.error("Error marking product as sold:", error);
}
};
return (
@@ -308,7 +323,7 @@ const Selling = () => {
{!showForm && (
<button
onClick={handleAddProduct}
className="bg-emerald-700 text-white px-4 py-2 hover:bg-emerald-700"
className="bg-emerald-600 text-white px-4 py-2 hover:bg-emerald-700"
>
+ Add New Product
</button>
@@ -320,7 +335,7 @@ const Selling = () => {
{/* Back Button */}
<button
onClick={() => setShowForm(false)}
className="mb-4 text-emerald-700 hover:text-emerald-800 flex items-center gap-1"
className="mb-4 text-emerald-600 hover:text-emerald-800 flex items-center gap-1"
>
<ChevronLeft size={16} />
<span>Back to Listings</span>
@@ -348,7 +363,7 @@ const Selling = () => {
name: e.target.value,
})
}
className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-600 focus:outline-none"
className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-500 focus:outline-none"
/>
</div>
@@ -367,33 +382,78 @@ const Selling = () => {
price: e.target.value,
})
}
className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-600 focus:outline-none"
className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-500 focus:outline-none"
/>
</div>
{/* Category - Single Selection Dropdown */}
{/* Sold Status */}
<div className="md:col-span-2">
<div className="flex items-center mt-2">
{editingProduct.isSold && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">
Sold
</span>
)}
</div>
</div>
{/* Categories */}
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Category
Categories
</label>
<select
value={editingProduct.category || ""}
onChange={handleCategoryChange}
className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-600 focus:outline-none"
required
>
<option value="" disabled>
Select a category
</option>
{categories.map((category, index) => (
<option key={index} value={category}>
{category}
<div className="flex gap-2">
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="flex-1 px-3 py-2 border border-gray-300 focus:border-emerald-500 focus:outline-none"
>
<option value="" disabled>
Select a category
</option>
))}
</select>
{!editingProduct.category && (
{categories
.filter(
(cat) => !(editingProduct.categories || []).includes(cat),
)
.map((category, index) => (
<option key={index} value={category}>
{category}
</option>
))}
</select>
<button
type="button"
onClick={addCategory}
disabled={!selectedCategory}
className="px-3 py-2 bg-emerald-600 text-white hover:bg-emerald-700 disabled:bg-gray-300 disabled:cursor-not-allowed flex items-center gap-1"
>
<Plus size={16} />
<span>Add</span>
</button>
</div>
{/* Selected Categories */}
{(editingProduct.categories || []).length > 0 ? (
<div className="mt-2 flex flex-wrap gap-2">
{(editingProduct.categories || []).map((category, index) => (
<span
key={index}
className="inline-flex items-center px-2 py-1 bg-emerald-100 text-emerald-800"
>
{category}
<button
type="button"
onClick={() => removeCategory(category)}
className="ml-1 text-emerald-600 hover:text-emerald-800"
>
<X size={14} />
</button>
</span>
))}
</div>
) : (
<p className="text-xs text-gray-500 mt-1">
Please select a category
Please select at least one category
</p>
)}
</div>
@@ -415,7 +475,7 @@ const Selling = () => {
})
}
rows="4"
className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-600 focus:outline-none"
className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-500 focus:outline-none"
placeholder="Describe your product in detail..."
></textarea>
</div>
@@ -444,8 +504,8 @@ const Selling = () => {
htmlFor="image-upload"
className="block w-full p-3 border border-gray-300 bg-gray-50 text-center cursor-pointer hover:bg-gray-100"
>
<span className="text-emerald-700 font-medium">
Click to upload images (will be saved to /public/uploads)
<span className="text-emerald-600 font-medium">
Click to upload images
</span>
</label>
@@ -475,11 +535,7 @@ const Selling = () => {
className="relative w-20 h-20 border border-gray-200 overflow-hidden"
>
<img
src={
typeof img === "string"
? img
: URL.createObjectURL(img)
}
src={URL.createObjectURL(img)}
alt={`Product ${idx + 1}`}
className="w-full h-full object-cover"
/>
@@ -503,19 +559,18 @@ const Selling = () => {
)}
{/* Show current image if editing */}
{editingProduct.image_url &&
!(editingProduct.images || []).length && (
<div className="mt-3">
<p className="text-sm text-gray-600 mb-2">Current image:</p>
<div className="relative w-20 h-20 border border-gray-200 overflow-hidden">
<img
src={editingProduct.image_url}
alt="Current product"
className="w-full h-full object-cover"
/>
</div>
{editingProduct.image_url && (
<div className="mt-3">
<p className="text-sm text-gray-600 mb-2">Current image:</p>
<div className="relative w-20 h-20 border border-gray-200 overflow-hidden">
<img
src={editingProduct.image_url}
alt="Current product"
className="w-full h-full object-cover"
/>
</div>
)}
</div>
)}
</div>
</div>
@@ -528,9 +583,22 @@ const Selling = () => {
Cancel
</button>
{editingProduct.ProductID && (
<button
onClick={markAsSold}
className={`px-4 py-2 rounded-md transition-colors ${
editingProduct.isSold
? "bg-green-600 text-white hover:bg-green-700"
: "bg-red-600 text-white hover:bg-red-700"
}`}
>
Mark as {editingProduct.isSold ? "Available" : "Sold"}
</button>
)}
<button
onClick={handleSaveProduct}
className="bg-emerald-700 text-white px-6 py-2 hover:bg-emerald-700 rounded-md"
className="bg-emerald-600 text-white px-6 py-2 hover:bg-emerald-700 rounded-md"
>
{editingProduct.ProductID ? "Update Product" : "Add Product"}
</button>
@@ -545,7 +613,7 @@ const Selling = () => {
</p>
<button
onClick={handleAddProduct}
className="bg-emerald-700 text-white px-4 py-2 hover:bg-emerald-700"
className="bg-emerald-600 text-white px-4 py-2 hover:bg-emerald-700"
>
Create Your First Listing
</button>
@@ -577,7 +645,7 @@ const Selling = () => {
</h3>
</div>
<p className="text-emerald-700 font-bold mt-1">
<p className="text-emerald-600 font-bold mt-1">
${product.Price}
</p>
@@ -611,7 +679,7 @@ const Selling = () => {
e.preventDefault();
handleEditProduct(product);
}}
className="text-emerald-700 hover:text-emerald-800 font-medium"
className="text-emerald-600 hover:text-emerald-800 font-medium"
>
Edit
</button>
@@ -624,11 +692,6 @@ const Selling = () => {
)}
</>
)}
<footer className="bg-gray-800 text-white py-6 mt-12">
<div className="border-t border-gray-700 text-center text-sm text-gray-400">
<p>© 2025 Campus Marketplace. All rights reserved.</p>
</div>
</footer>
</div>
);
};

View File

@@ -190,7 +190,7 @@ const Settings = () => {
if (isLoading) {
return (
<div className="flex justify-center items-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-emerald-600"></div>
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-emerald-500"></div>
</div>
);
}
@@ -234,7 +234,7 @@ const Settings = () => {
id="name"
value={userData.name}
onChange={handleInputChange}
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-600"
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-500"
required
/>
</div>
@@ -251,7 +251,7 @@ const Settings = () => {
id="email"
value={userData.email}
onChange={handleInputChange}
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-600"
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-500"
required
readOnly // Email is often used as identifier and not changeable
/>
@@ -269,7 +269,7 @@ const Settings = () => {
id="phone"
value={userData.phone}
onChange={handleInputChange}
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-600"
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-500"
/>
</div>
@@ -285,7 +285,7 @@ const Settings = () => {
id="UCID"
value={userData.UCID}
onChange={handleInputChange}
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-600"
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-500"
required
/>
</div>
@@ -302,7 +302,7 @@ const Settings = () => {
id="address"
value={userData.address}
onChange={handleInputChange}
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-600"
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-500"
/>
</div>
<div>
@@ -317,14 +317,14 @@ const Settings = () => {
id="password"
value={userData.password}
onChange={handleInputChange}
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-600"
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-500"
/>
</div>
</div>
<button
type="submit"
className="bg-emerald-600 hover:bg-emerald-700 text-white font-medium py-2 px-4"
className="bg-emerald-500 hover:bg-emerald-600 text-white font-medium py-2 px-4"
>
Update Profile
</button>
@@ -395,11 +395,6 @@ const Settings = () => {
</div>
</div>
</div>
<footer className="bg-gray-800 text-white py-6 mt-12">
<div className="border-t border-gray-700 text-center text-sm text-gray-400">
<p>© 2025 Campus Marketplace. All rights reserved.</p>
</div>
</footer>
</div>
);
};

View File

@@ -0,0 +1,91 @@
import { useEffect, useState } from "react";
import { getTransactions, removeTransaction } from "../api/admin";
import { MdDelete } from "react-icons/md";
import Pagination from "../components/Pagination";
export default function TransactionDashboard() {
const [transactions, setTransactions] = useState([]);
const [total, setTotal] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
let pageLimit = 10;
const onChangePage = (page, limit = 10) => {
setCurrentPage(page);
fetchTransactions(page, limit);
};
const fetchTransactions = (page = 1, limit = 10) => {
getTransactions(page, limit).then(({ transactions, total }) => {
setTotal(total);
setTransactions(transactions);
});
};
const handleRemoveTransaction = (id) => {
removeTransaction(id)
.then(() => {
fetchTransactions(currentPage);
})
.catch((err) => {
console.log(err);
});
};
//Get user when initialize the component
useEffect(fetchTransactions, []);
return (
<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,163 +1,8 @@
import { useState, useEffect } from "react";
import { useState } from "react";
import { Link } from "react-router-dom";
import { Calendar, CreditCard, Trash2 } from "lucide-react";
const Transactions = () => {
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-600 hover:bg-emerald-700 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-700 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="border-t border-gray-700 text-center text-sm text-gray-400">
<p>© 2025 Campus Marketplace. All rights reserved.</p>
</div>
</footer>
</div>
);
return <div></div>;
};
export default Transactions;

View File

@@ -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 (
<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>
);
}

View File

@@ -1,124 +0,0 @@
-- 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

@@ -44,7 +44,7 @@ VALUES
(
1,
'John Doe',
'john.doe@ucalgary.ca',
'john.doe@example.com',
'U123456',
'hashedpassword1',
'555-123-4567',
@@ -53,7 +53,7 @@ VALUES
(
2,
'Jane Smith',
'jane.smith@ucalgary.ca',
'jane.smith@example.com',
'U234567',
'hashedpassword2',
'555-234-5678',
@@ -72,7 +72,6 @@ VALUES
INSERT INTO
Category (Name)
VALUES
('Other'),
('Textbooks'),
('Electronics'),
('Furniture'),
@@ -101,7 +100,8 @@ VALUES
('Winter Clothing'),
('Photography Equipment'),
('Event Tickets'),
('Software Licenses');
('Software Licenses'),
('Transportation (Car Pool)');
-- Insert Products
INSERT INTO
@@ -453,5 +453,4 @@ VALUES
1,
'This is a great fake product! Totally recommend it.',
5,
'2024-10-02 16:00:00'
)
);

View File

@@ -36,10 +36,10 @@ CREATE TABLE Product (
StockQuantity INT,
UserID INT,
Description TEXT,
CategoryID INT,
CategoryID INT NOT NULL,
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
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
@@ -60,19 +60,19 @@ CREATE TABLE Review (
AND Rating <= 5
),
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
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)
CREATE TABLE Transaction (
TransactionID INT AUTO_INCREMENT PRIMARY KEY,
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
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE SET NULL
);
-- Recommendation Entity (Many-to-One with User, Many-to-One with Product)