30 Commits

Author SHA1 Message Date
Mann Patel
d6b5e8ff1b ref: update readme 2026-02-20 21:58:45 -07:00
Mann Patel
7ffba4c14c Update README.md 2025-04-30 13:46:04 -06:00
Mann Patel
c6d98b6d77 Update App.jsx 2025-04-22 18:47:20 -06:00
Mann Patel
b4ac53a8d0 code update 2025-04-22 18:01:10 -06:00
Mann Patel
b7937018e5 updating the transaction 2025-04-22 16:33:23 -06:00
Mann Patel
9d05adacfb removing unwanted table product_category 2025-04-22 14:27:32 -06:00
Mann Patel
bbddc8566a Enforce @ucalgary.ca emails for registration & require login after account creation 2025-04-22 12:18:10 -06:00
Mann Patel
4ba6dfa7be UI Color update 2025-04-21 22:46:39 -06:00
estherdev03
0a1db869f7 fix delete category 2025-04-21 17:03:09 -06:00
estherdev03
a8745ed94c Fix admin 2025-04-21 16:53:51 -06:00
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
35 changed files with 1425 additions and 1297 deletions

View File

@@ -1,6 +1,9 @@
### Some ground rules ### Ground rules
1. Add both node_modules from Slient and Server to your ```gitignore``` file
2. Make a brach with the following naming conventionp, prefix it with your name ```Your-Name Branch-Name```. <img class='fullimage' alt='Fin-Track Application Screenshot' src='./assets/CampusPlug.png'/>
1. Add both node_modules from Client and Server to your ```gitignore``` file
2. Make a brach with the following naming convention, prefix it with your name ```Your-Name Branch-Name```.
--- ---
### Frontend ### Frontend
@@ -19,24 +22,19 @@
``` ```
--- ---
### Recommendation ### Recommendation system
1. `cd recommendation-engine` into the dir and then type command 1. Install the dependencies `pip install scikit-learn numpy mysql.connector flask flask-cors`
2. `cd recommendation-engine` into the dir and then type command
```Bash ```Bash
1. python3 server.py #Start The Server 1. python3 server.py #Start The Server
``` ```
--- ---
### Recommendation system
1. Install the dependencies
```Bash
pip install mysql.connector
```
### Database ### Database
1. MySql Version 9.2.0 1. MySql Version 9.2.0
2. To Create the DataBase use the command bellow: 2. To Create the DataBase use the command bellow:
```Bash ```Bash
1. mysql -u root 1. mysql -u root
2. \. PathToYour/Schema.sql 2. \. PathTo/Schema.sql
3. \. PathToYour/Init-Data.sql 3. \. PathTo/Init-Data.sql
``` ```

BIN
assets/CampusPlug.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

View File

@@ -7,7 +7,7 @@ exports.getAllCategoriesWithPagination = async (req, res) => {
try { try {
const [data, _] = await db.execute( const [data, _] = await db.execute(
"SELECT * FROM Category C ORDER BY C.CategoryID ASC LIMIT ? OFFSET ?", "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"); const [result] = await db.execute("SELECT COUNT(*) AS count FROM Category");
@@ -24,7 +24,7 @@ exports.addCategory = async (req, res) => {
try { try {
const [result] = await db.execute( const [result] = await db.execute(
"INSERT INTO Category (Name) VALUES (?)", "INSERT INTO Category (Name) VALUES (?)",
[name], [name]
); );
res.json({ message: "Adding new category successfully!" }); res.json({ message: "Adding new category successfully!" });
} catch (error) { } catch (error) {
@@ -34,15 +34,23 @@ exports.addCategory = async (req, res) => {
exports.removeCategory = async (req, res) => { exports.removeCategory = async (req, res) => {
const { id } = req.params; const { id } = req.params;
try { 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]
);
const [result] = await db.execute( const [result] = await db.execute(
`DELETE FROM Category WHERE CategoryID = ?`, `DELETE FROM Category WHERE CategoryID = ?`,
[id], [id]
); );
res.json({ message: "Delete category successfully!" }); res.json({ message: "Delete category successfully!" });
} catch (error) { } catch (error) {
res.json({ error: "Cannot remove category from database!" }); res.json({
error: error.message || "Cannot remove category from database!",
});
} }
}; };

View File

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

View File

@@ -6,16 +6,16 @@ exports.getTransactionWithPagination = async (req, res) => {
const offset = (page - 1) * limit; const offset = (page - 1) * limit;
try { try {
const [data, _] = await db.execute( 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 FROM Transaction T
LEFT JOIN User U ON T.UserID = U.UserID LEFT JOIN User U ON T.UserID = U.UserID
LEFT JOIN Product P ON T.ProductID = P.ProductID LEFT JOIN Product P ON T.ProductID = P.ProductID
ORDER BY T.TransactionID ASC LIMIT ? OFFSET ?`, ORDER BY T.TransactionID ASC LIMIT ? OFFSET ?`,
[limit.toString(), offset.toString()] [limit.toString(), offset.toString()],
); );
const [result] = await db.execute( const [result] = await db.execute(
"SELECT COUNT(*) AS count FROM Transaction" "SELECT COUNT(*) AS count FROM Transaction",
); );
const { count: total } = result[0]; const { count: total } = result[0];
return res.json({ data, total }); return res.json({ data, total });
@@ -29,7 +29,7 @@ exports.removeTransation = async (req, res) => {
try { try {
const [result] = await db.execute( const [result] = await db.execute(
"DELETE FROM Transaction WHERE TransactionID = ?;", "DELETE FROM Transaction WHERE TransactionID = ?;",
[id.toString()] [id.toString()],
); );
return res.json({ message: "Remove transaction successfully!" }); return res.json({ message: "Remove transaction successfully!" });
} catch (error) { } catch (error) {
@@ -38,3 +38,189 @@ exports.removeTransation = async (req, res) => {
.json({ error: "Cannot remove transactions from database!" }); .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
JOIN Image_URL I ON I.ProductID = T.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,
MIN(I.URL) AS Image_URL
FROM Transaction T
JOIN Product P ON T.ProductID = P.ProductID
JOIN Image_URL I ON I.ProductID = T.ProductID
WHERE T.UserID = ?
GROUP BY
T.TransactionID,
T.UserID,
T.ProductID,
T.Date,
T.PaymentStatus,
P.Name;
`,
[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
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" });
}
};
// 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" });
}
};
exports.updateTransactionStatus = async (req, res) => {
const { transactionID } = req.body;
try {
const [result] = await db.execute(
`UPDATE Transaction
SET PaymentStatus = 'completed'
WHERE TransactionID = ?;`,
[transactionID],
);
res.json({
success: true,
message: "Transaction updated 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 recommendedRouter = require("./routes/recommendation");
const history = require("./routes/history"); const history = require("./routes/history");
const review = require("./routes/review"); const review = require("./routes/review");
const categoryRouter = require("./routes/category"); const categoryRouter = require("./routes/category");
const transactionRouter = require("./routes/transaction"); const transactionRouter = require("./routes/transaction");
const { generateEmailTransporter } = require("./utils/mail"); const { generateEmailTransporter } = require("./utils/mail");
@@ -44,9 +46,9 @@ app.use("/api/search", searchRouter);
app.use("/api/engine", recommendedRouter); app.use("/api/engine", recommendedRouter);
app.use("/api/history", history); app.use("/api/history", history);
app.use("/api/review", review); app.use("/api/review", review);
app.use("/api/category", categoryRouter);
app.use("/api/transaction", transactionRouter); app.use("/api/transaction", transactionRouter);
app.use("/api/category", categoryRouter); app.use("/api/category", categoryRouter);
app.use("/api/transaction", transactionRouter);
// Set up a scheduler to run cleanup every hour // Set up a scheduler to run cleanup every hour
clean_up_time = 30 * 60 * 1000; clean_up_time = 30 * 60 * 1000;

View File

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

View File

@@ -1,11 +1,29 @@
// routes/transaction.js
const express = require("express"); const express = require("express");
const { const {
createTransaction,
getTransactionsByProduct,
getTransactionsByUser,
getAllTransactions,
deleteTransaction,
getTransactionWithPagination, getTransactionWithPagination,
removeTransation, removeTransation,
updateTransactionStatus,
} = require("../controllers/transaction"); } = require("../controllers/transaction");
const router = express.Router(); const router = express.Router();
// logging middleware
router.use((req, res, next) => {
console.log(`Incoming ${req.method} ${req.originalUrl}`);
next();
});
router.post("/createTransaction", createTransaction);
router.get("/getTransactionsByProduct/:productID", getTransactionsByProduct);
router.post("/getTransactionsByUser", getTransactionsByUser);
router.post("/updateStatus", updateTransactionStatus);
router.post("/getAllTransactions", getAllTransactions);
router.delete("/deleteTransaction", deleteTransaction);
router.get("/getTransactions", getTransactionWithPagination); router.get("/getTransactions", getTransactionWithPagination);
router.delete("/:id", removeTransation); router.delete("/:id", removeTransation);

View File

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

View File

@@ -4,6 +4,7 @@ import {
Routes, Routes,
Route, Route,
Navigate, Navigate,
useLocation,
} from "react-router-dom"; } from "react-router-dom";
import Navbar from "./components/Navbar"; import Navbar from "./components/Navbar";
import Home from "./pages/Home"; import Home from "./pages/Home";
@@ -12,14 +13,10 @@ import Selling from "./pages/Selling";
import Transactions from "./pages/Transactions"; import Transactions from "./pages/Transactions";
import Favorites from "./pages/Favorites"; import Favorites from "./pages/Favorites";
import ProductDetail from "./pages/ProductDetail"; import ProductDetail from "./pages/ProductDetail";
import SearchPage from "./pages/SearchPage"; // Make sure to import the SearchPage import SearchPage from "./pages/SearchPage";
import Dashboard from "./pages/Dashboard"; import Dashboard from "./pages/Dashboard"; // The single consolidated dashboard component
import UserDashboard from "./pages/UserDashboard";
import ProductDashboard from "./pages/ProductDashboard";
import DashboardNav from "./components/DashboardNav"; import DashboardNav from "./components/DashboardNav";
import CategoryDashboard from "./pages/CategoryDashboard";
import { verifyIsAdmin } from "./api/admin"; import { verifyIsAdmin } from "./api/admin";
import TransactionDashboard from "./pages/TransactionDashboard";
function App() { function App() {
// Authentication state - initialize from localStorage if available // Authentication state - initialize from localStorage if available
@@ -42,6 +39,18 @@ function App() {
useState(false); useState(false);
const [recommendations, setRecommendations] = useState([]); 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 // New verification states
const [verificationStep, setVerificationStep] = useState("initial"); // 'initial', 'code-sent', 'verifying' const [verificationStep, setVerificationStep] = useState("initial"); // 'initial', 'code-sent', 'verifying'
const [tempUserData, setTempUserData] = useState(null); const [tempUserData, setTempUserData] = useState(null);
@@ -77,59 +86,11 @@ function App() {
// Generate product recommendations // Generate product recommendations
const generateProductRecommendations = async () => { const generateProductRecommendations = async () => {
try { setIsGeneratingRecommendations(true);
setIsGeneratingRecommendations(true); await new Promise((resolve) => setTimeout(resolve, 500));
setIsGeneratingRecommendations(false);
// Add a short delay to simulate calculation time
await new Promise((resolve) => setTimeout(resolve, 500));
console.log("Generating product recommendations for user:", user.ID);
// Make API call to get recommendations
const response = await fetch(
"http://localhost:3030/api/recommendations/generate",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userId: user.ID,
}),
},
);
if (!response.ok) {
throw new Error("Failed to generate recommendations");
}
const result = await response.json();
if (result.success) {
console.log(
"Recommendations generated successfully:",
result.recommendations,
);
setRecommendations(result.recommendations);
// Store recommendations in session storage for access across the app
sessionStorage.setItem(
"userRecommendations",
JSON.stringify(result.recommendations),
);
} else {
console.error("Error generating recommendations:", result.message);
}
} catch (err) {
console.error("Error generating product recommendations:", err);
} finally {
setIsGeneratingRecommendations(false);
}
}; };
const [isAdmin, setIsAdmin] = useState(false);
const [showAdminDashboard, setShowAdminDashboard] = useState(false);
useEffect(() => { useEffect(() => {
const userInfo = sessionStorage.getItem("user") const userInfo = sessionStorage.getItem("user")
? JSON.parse(sessionStorage.getItem("user")) ? JSON.parse(sessionStorage.getItem("user"))
@@ -142,9 +103,14 @@ function App() {
const handleShowAdminDashboard = () => { const handleShowAdminDashboard = () => {
setShowAdminDashboard(true); setShowAdminDashboard(true);
// Update URL without reloading page
window.history.pushState({}, "", "/admin");
}; };
const handleCloseAdminDashboard = () => { const handleCloseAdminDashboard = () => {
setShowAdminDashboard(false); setShowAdminDashboard(false);
// Update URL without reloading page
window.history.pushState({}, "", "/");
}; };
// Send verification code // Send verification code
@@ -245,7 +211,6 @@ function App() {
// Complete signup // Complete signup
const completeSignUp = async (userData) => { const completeSignUp = async (userData) => {
try { try {
setIsLoading(true);
setError(""); setError("");
console.log("Completing signup for:", userData.email); console.log("Completing signup for:", userData.email);
@@ -281,7 +246,8 @@ function App() {
// Set authenticated user // Set authenticated user
setUser(newUser); setUser(newUser);
setIsAuthenticated(true); setIsSignUp(false);
//setIsAuthenticated(true);
// Save to localStorage to persist across refreshes // Save to localStorage to persist across refreshes
sessionStorage.setItem("isAuthenticated", "true"); sessionStorage.setItem("isAuthenticated", "true");
@@ -327,12 +293,11 @@ function App() {
setError("Email and password are required"); setError("Email and password are required");
setIsLoading(false); setIsLoading(false);
return; 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 { try {
if (isSignUp) { if (isSignUp) {
// Handle Sign Up with verification // Handle Sign Up with verification
@@ -436,6 +401,7 @@ function App() {
setVerificationStep("initial"); setVerificationStep("initial");
setTempUserData(null); setTempUserData(null);
setRecommendations([]); setRecommendations([]);
setShowAdminDashboard(false);
// Clear localStorage // Clear localStorage
sessionStorage.removeItem("user"); sessionStorage.removeItem("user");
@@ -505,7 +471,7 @@ function App() {
// Loading overlay component // Loading overlay component
const LoadingOverlay = () => ( const LoadingOverlay = () => (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"> <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-green-500 border-t-transparent"></div> <div className="animate-spin rounded-full h-12 w-12 border-4 border-emerald-600 border-t-transparent"></div>
</div> </div>
); );
@@ -572,7 +538,7 @@ function App() {
id="name" id="name"
name="name" name="name"
placeholder="Enter your 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-green-500 focus:ring-1 focus:ring-green-500" 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"
required={isSignUp} required={isSignUp}
/> />
</div> </div>
@@ -591,7 +557,7 @@ function App() {
id="ucid" id="ucid"
name="ucid" name="ucid"
placeholder="1234567" placeholder="1234567"
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" 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"
required={isSignUp} required={isSignUp}
/> />
</div> </div>
@@ -609,7 +575,7 @@ function App() {
id="email" id="email"
name="email" name="email"
placeholder="your.email@ucalgary.ca" 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-green-500 focus:ring-1 focus:ring-green-500" 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"
required required
/> />
</div> </div>
@@ -627,7 +593,7 @@ function App() {
id="phone" id="phone"
name="phone" name="phone"
placeholder="+1(123)456 7890" 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-green-500 focus:ring-1 focus:ring-green-500" 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"
required={isSignUp} required={isSignUp}
/> />
</div> </div>
@@ -646,7 +612,7 @@ function App() {
id="address" id="address"
name="address" name="address"
placeholder="Your 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-green-500 focus:ring-1 focus:ring-green-500" 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"
required={isSignUp} required={isSignUp}
/> />
</div> </div>
@@ -668,7 +634,7 @@ function App() {
? "Create a secure password" ? "Create a secure password"
: "Enter your 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-green-500 focus:ring-1 focus:ring-green-500" 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"
required required
/> />
</div> </div>
@@ -677,7 +643,7 @@ function App() {
<button <button
type="submit" type="submit"
disabled={isLoading} disabled={isLoading}
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" 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"
> >
{isLoading {isLoading
? "Please wait..." ? "Please wait..."
@@ -704,7 +670,7 @@ function App() {
id="verificationCode" id="verificationCode"
name="verificationCode" name="verificationCode"
placeholder="Enter the 6-digit code" 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-green-500 focus:ring-1 focus:ring-green-500" 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"
required required
/> />
<p className="mt-1 text-xs text-gray-500"> <p className="mt-1 text-xs text-gray-500">
@@ -716,7 +682,7 @@ function App() {
<button <button
type="submit" type="submit"
disabled={isLoading} disabled={isLoading}
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" 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"
> >
{isLoading ? "Please wait..." : "Verify Code"} {isLoading ? "Please wait..." : "Verify Code"}
</button> </button>
@@ -734,7 +700,7 @@ function App() {
type="button" type="button"
onClick={handleResendCode} onClick={handleResendCode}
disabled={isLoading} disabled={isLoading}
className="text-sm text-green-500 hover:text-green-700" className="text-sm text-emerald-600 hover:text-emerald-700"
> >
Resend code Resend code
</button> </button>
@@ -752,7 +718,7 @@ function App() {
<button <button
onClick={toggleAuthMode} onClick={toggleAuthMode}
type="button" type="button"
className="text-green-500 font-medium hover:text-green-700" className="text-emerald-600 font-medium hover:text-emerald-700"
> >
{isSignUp ? "Sign in" : "Sign up"} {isSignUp ? "Sign in" : "Sign up"}
</button> </button>
@@ -774,126 +740,119 @@ function App() {
return children; return children;
}; };
// If user is admin, show admin naviagtion return (
if (showAdminDashboard) { <Router>
return ( {/* If admin dashboard should be shown */}
<Router> {showAdminDashboard ? (
<div className="flex"> <div className="flex">
<DashboardNav handleCloseAdminDashboard={handleCloseAdminDashboard} /> <DashboardNav handleCloseAdminDashboard={handleCloseAdminDashboard} />
<Routes> <Routes>
{/* Admin routes */} {/* Single admin route for consolidated dashboard */}
<Route path="/admin" element={<Dashboard />} /> <Route path="/admin/*" element={<Dashboard />} />
<Route path="/admin/user" element={<UserDashboard />} /> {/* Any other path in admin mode should go to dashboard */}
<Route path="/admin/product" element={<ProductDashboard />} /> <Route path="*" element={<Navigate to="/admin" />} />
<Route path="/admin/category" element={<CategoryDashboard />} />
<Route
path="/admin/transaction"
element={<TransactionDashboard />}
/>
<Route path="*" element={<Dashboard />} />
</Routes> </Routes>
</div> </div>
</Router> ) : (
); /* Normal user interface */
} <div className="min-h-screen bg-gray-50">
{/* Show loading overlay when generating recommendations */}
{isGeneratingRecommendations && <LoadingOverlay />}
return ( {/* Only show navbar when authenticated */}
<Router> {isAuthenticated && (
<div className="min-h-screen bg-gray-50"> <Navbar
{/* Show loading overlay when generating recommendations */} isAdmin={isAdmin}
{isGeneratingRecommendations && <LoadingOverlay />} onLogout={handleLogout}
userName={user?.name}
{/* Only show navbar when authenticated */} handleShowAdminDashboard={handleShowAdminDashboard}
{isAuthenticated && ( />
<Navbar )}
isAdmin={isAdmin} <Routes>
onLogout={handleLogout} {/* Public routes */}
userName={user?.name} <Route
handleShowAdminDashboard={handleShowAdminDashboard} path="/login"
/> element={
)} isAuthenticated ? <Navigate to="/" /> : <LoginComponent />
<Routes> }
{/* Public routes */} />
<Route {/* Protected routes */}
path="/login" <Route
element={isAuthenticated ? <Navigate to="/" /> : <LoginComponent />} path="/"
/> element={
{/* Protected routes */} <ProtectedRoute>
<Route <div className="container mx-auto px-4 py-6">
path="/" <Home recommendations={recommendations} />
element={ </div>
<ProtectedRoute> </ProtectedRoute>
<div className="container mx-auto px-4 py-6"> }
<Home recommendations={recommendations} /> />
</div> <Route
</ProtectedRoute> path="/product/:id"
} element={
/> <ProtectedRoute>
<Route <ProductDetail />
path="/product/:id" </ProtectedRoute>
element={ }
<ProtectedRoute> />
<ProductDetail /> <Route
</ProtectedRoute> path="/search"
} element={
/> <ProtectedRoute>
<Route <div className="container mx-auto px-4 py-6">
path="/search" <SearchPage />
element={ </div>
<ProtectedRoute> </ProtectedRoute>
<div className="container mx-auto px-4 py-6"> }
<SearchPage /> />
</div> <Route
</ProtectedRoute> path="/settings"
} element={
/> <ProtectedRoute>
<Route <div className="container mx-auto px-4 py-6">
path="/settings" <Settings />
element={ </div>
<ProtectedRoute> </ProtectedRoute>
<div className="container mx-auto px-4 py-6"> }
<Settings /> />
</div> <Route
</ProtectedRoute> path="/selling"
} element={
/> <ProtectedRoute>
<Route <div className="container mx-auto px-4 py-6">
path="/selling" <Selling />
element={ </div>
<ProtectedRoute> </ProtectedRoute>
<div className="container mx-auto px-4 py-6"> }
<Selling /> />
</div> <Route
</ProtectedRoute> path="/transactions"
} element={
/> <ProtectedRoute>
<Route <div className="container mx-auto px-4 py-6">
path="/transactions" <Transactions />
element={ </div>
<ProtectedRoute> </ProtectedRoute>
<div className="container mx-auto px-4 py-6"> }
<Transactions /> />
</div> <Route
</ProtectedRoute> path="/favorites"
} element={
/> <ProtectedRoute>
<Route <div className="container mx-auto px-4 py-6">
path="/favorites" <Favorites />
element={ </div>
<ProtectedRoute> </ProtectedRoute>
<div className="container mx-auto px-4 py-6"> }
<Favorites /> />
</div> {/* Redirect to login for any unmatched routes */}
</ProtectedRoute> <Route
} path="*"
/> element={<Navigate to={isAuthenticated ? "/" : "/login"} />}
{/* Redirect to login for any unmatched routes */} />
<Route </Routes>
path="*" </div>
element={<Navigate to={isAuthenticated ? "/" : "/login"} />} )}
/>
</Routes>
</div>
</Router> </Router>
); );
} }

View File

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

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

View File

@@ -1,98 +1,16 @@
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 { FaArrowLeft } from "react-icons/fa";
import { FaMoneyBillTransfer } from "react-icons/fa6";
export default function DashboardNav({ handleCloseAdminDashboard }) { export default function DashboardNav({ handleCloseAdminDashboard }) {
const handleClick = () => {
handleCloseAdminDashboard();
};
return ( return (
<div> <div className="w-48 min-w-[12rem] bg-gray-100 text-emerald-600 flex flex-col p-4 shadow-md">
<div className="w-3xs h-screen bg-green-700 border border-green-700"> <h2 className="text-lg font-semibold mb-4">Admin Dashboard</h2>
<ul> <button
<li> onClick={handleCloseAdminDashboard}
<div className="flex-shrink-0 p-6 bg-green-200"> className="flex items-center gap-2 text-sm font-medium hover:text-emerald-700 underline underline-offset-4 transition"
<Link to="/admin" className="flex items-center"> >
<img <FaArrowLeft className="text-xs mt-[1px]" />
src="/icon/icon-512.png" Back to User Page
alt="Campus Plug" </button>
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> </div>
); );
} }

View File

@@ -11,7 +11,7 @@ const FloatingAlert = ({ message, onClose, duration = 3000 }) => {
}, [onClose, duration]); }, [onClose, duration]);
return ( return (
<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"> <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">
{message} {message}
</div> </div>
); );

View File

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

View File

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

View File

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

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,366 @@
export default function Dashboard() { 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 />;
return ( return (
<div className="text-3xl font-bold p-3 text-green-800"> <div className="w-full mt-6">
Welcome to admin dashboard <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> </div>
); );
} }

View File

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

View File

@@ -219,7 +219,7 @@ const Home = () => {
// Loading indicator component // Loading indicator component
const LoadingSection = () => ( const LoadingSection = () => (
<div className="flex justify-center items-center h-48"> <div className="flex justify-center items-center h-48">
<Loader className="animate-spin text-emerald-600 h-8 w-8" /> <Loader className="animate-spin text-emerald-700 h-8 w-8" />
</div> </div>
); );
@@ -243,7 +243,7 @@ const Home = () => {
e.preventDefault(); e.preventDefault();
toggleFavorite(product.id); toggleFavorite(product.id);
}} }}
className="absolute top-0 right-0 p-2 rounded-bl-md bg-emerald-600 hover:bg-emerald-500 transition shadow-sm" className="absolute top-0 right-0 p-2 rounded-bl-md bg-emerald-700 hover:bg-emerald-600 transition shadow-sm"
> >
<Bookmark className="text-white w-5 h-5" /> <Bookmark className="text-white w-5 h-5" />
</button> </button>
@@ -253,7 +253,7 @@ const Home = () => {
<h3 className="text-lg font-medium text-gray-800 leading-tight"> <h3 className="text-lg font-medium text-gray-800 leading-tight">
{product.title} {product.title}
</h3> </h3>
<span className="font-semibold text-emerald-600 block mt-1"> <span className="font-semibold text-emerald-700 block mt-1">
${product.price} ${product.price}
</span> </span>
@@ -353,7 +353,7 @@ const Home = () => {
</p> </p>
<button <button
onClick={handleSelling} onClick={handleSelling}
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" 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"
> >
Post an Item Post an Item
</button> </button>
@@ -405,59 +405,9 @@ const Home = () => {
</ScrollableProductList> </ScrollableProductList>
)} )}
</div> </div>
{/* Footer */}
<footer className="bg-gray-800 text-white py-6 mt-12"> <footer className="bg-gray-800 text-white py-6 mt-12">
<div className="container mx-auto px-4"> <div className="border-t border-gray-700 text-center text-sm text-gray-400">
<div className="flex flex-col md:flex-row justify-between items-center"> <p>© 2025 Campus Marketplace. All rights reserved.</p>
<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> </div>
</footer> </footer>
</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 [reviews, setReviews] = useState([]);
const [showReviewForm, setShowReviewForm] = useState(false); const [showReviewForm, setShowReviewForm] = useState(false);
const [showAlert, setShowAlert] = useState(false); const [showAlert, setShowAlert] = useState(false);
const [showAlert1, setShowAlert1] = useState(false);
const storedUser = JSON.parse(sessionStorage.getItem("user")); const storedUser = JSON.parse(sessionStorage.getItem("user"));
const toggleFavorite = async (id) => { const toggleFavorite = async (id) => {
@@ -245,7 +247,7 @@ const ProductDetail = () => {
if (loading.product) { if (loading.product) {
return ( return (
<div className="flex justify-center items-center h-screen"> <div className="flex justify-center items-center h-screen">
<div className="animate-spin h-32 w-32 border-t-2 border-emerald-600"></div> <div className="animate-spin h-32 w-32 border-t-2 border-emerald-700"></div>
</div> </div>
); );
} }
@@ -259,7 +261,7 @@ const ProductDetail = () => {
<p className="text-gray-600">{error.product}</p> <p className="text-gray-600">{error.product}</p>
<Link <Link
to="/" to="/"
className="mt-4 inline-block bg-emerald-600 text-white px-4 py-2 hover:bg-emerald-700" className="mt-4 inline-block bg-emerald-700 text-white px-4 py-2 hover:bg-emerald-700"
> >
Back to Listings Back to Listings
</Link> </Link>
@@ -276,7 +278,7 @@ const ProductDetail = () => {
<h2 className="text-2xl text-red-500 mb-4">Product Not Found</h2> <h2 className="text-2xl text-red-500 mb-4">Product Not Found</h2>
<Link <Link
to="/" to="/"
className="mt-4 inline-block bg-emerald-600 text-white px-4 py-2 hover:bg-emerald-700" className="mt-4 inline-block bg-emerald-700 text-white px-4 py-2 hover:bg-emerald-700"
> >
Back to Listings Back to Listings
</Link> </Link>
@@ -303,6 +305,12 @@ const ProductDetail = () => {
onClose={() => setShowAlert(false)} 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="flex flex-col md:flex-row gap-8">
<div className="md:w-3/5"> <div className="md:w-3/5">
@@ -348,7 +356,7 @@ const ProductDetail = () => {
{product.images.map((image, index) => ( {product.images.map((image, index) => (
<div <div
key={index} key={index}
className={`bg-white border ${currentImage === index ? "border-emerald-600 border-2" : "border-gray-200"} min-w-[100px] cursor-pointer`} className={`bg-white border ${currentImage === index ? "border-emerald-700 border-2" : "border-gray-200"} min-w-[100px] cursor-pointer`}
onClick={() => selectImage(index)} onClick={() => selectImage(index)}
> >
<img <img
@@ -378,7 +386,7 @@ const ProductDetail = () => {
e.preventDefault(); e.preventDefault();
toggleFavorite(product.ProductID); toggleFavorite(product.ProductID);
}} }}
className="top-0 p-2 rounded-bl-md bg-emerald-700 hover:bg-emerald-600 transition shadow-sm" className="top-0 p-2 rounded-bl-md bg-emerald-700 hover:bg-emerald-700 transition shadow-sm"
> >
<Bookmark className="text-white w-5 h-5" /> <Bookmark className="text-white w-5 h-5" />
</button> </button>
@@ -414,8 +422,40 @@ const ProductDetail = () => {
<div className="relative"> <div className="relative">
<button <button
onClick={() => setShowContactOptions(!showContactOptions)} onClick={async () => {
className="w-full bg-emerald-700 hover:bg-emerald-700 text-white font-medium py-3 px-4 mb-3" 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"
> >
Contact Seller Contact Seller
</button> </button>
@@ -427,7 +467,7 @@ const ProductDetail = () => {
href={`tel:${product.SellerPhone}`} href={`tel:${product.SellerPhone}`}
className="flex items-center gap-2 p-3 hover:bg-gray-50 border-b border-gray-100" 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-600" /> <Phone className="h-5 w-5 text-emerald-700" />
<span>Call Seller</span> <span>Call Seller</span>
</a> </a>
)} )}
@@ -437,7 +477,7 @@ const ProductDetail = () => {
href={`mailto:${product.SellerEmail}`} href={`mailto:${product.SellerEmail}`}
className="flex items-center gap-2 p-3 hover:bg-gray-50" className="flex items-center gap-2 p-3 hover:bg-gray-50"
> >
<Mail className="h-5 w-5 text-emerald-600" /> <Mail className="h-5 w-5 text-emerald-700" />
<span>Email Seller</span> <span>Email Seller</span>
</a> </a>
)} )}
@@ -458,7 +498,10 @@ const ProductDetail = () => {
{product.SellerName || "Unknown Seller"} {product.SellerName || "Unknown Seller"}
</h3> </h3>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
Member since {product.SellerJoinDate || "N/A"} Product listed since{" "}
{product.Date
? new Date(product.Date).toLocaleDateString()
: "N/A"}
</p> </p>
</div> </div>
</div> </div>
@@ -474,7 +517,7 @@ const ProductDetail = () => {
<div className="bg-white border border-gray-200 p-6"> <div className="bg-white border border-gray-200 p-6">
{loading.reviews ? ( {loading.reviews ? (
<div className="flex justify-center py-8"> <div className="flex justify-center py-8">
<div className="animate-spin h-8 w-8 border-t-2 border-emerald-600"></div> <div className="animate-spin h-8 w-8 border-t-2 border-emerald-700"></div>
</div> </div>
) : error.reviews ? ( ) : error.reviews ? (
<div className="text-red-500 mb-4"> <div className="text-red-500 mb-4">
@@ -521,7 +564,7 @@ const ProductDetail = () => {
<div className="mt-4"> <div className="mt-4">
<button <button
onClick={() => setShowReviewForm(true)} onClick={() => setShowReviewForm(true)}
className="bg-emerald-600 hover:bg-emerald-700 text-white font-medium py-2 px-4" className="bg-emerald-700 hover:bg-emerald-700 text-white font-medium py-2 px-4"
> >
Write a Review Write a Review
</button> </button>
@@ -579,7 +622,7 @@ const ProductDetail = () => {
id="comment" id="comment"
value={reviewForm.comment} value={reviewForm.comment}
onChange={handleReviewInputChange} onChange={handleReviewInputChange}
className="w-full p-3 border border-gray-300 focus:outline-none focus:border-emerald-600" className="w-full p-3 border border-gray-300 focus:outline-none focus:border-emerald-700"
rows="4" rows="4"
required required
></textarea> ></textarea>
@@ -595,7 +638,7 @@ const ProductDetail = () => {
</button> </button>
<button <button
type="submit" type="submit"
className="px-4 py-2 bg-emerald-600 text-white hover:bg-emerald-700" className="px-4 py-2 bg-emerald-700 text-white hover:bg-emerald-700"
disabled={loading.submitting} disabled={loading.submitting}
> >
{loading.submitting ? "Submitting..." : "Submit Review"} {loading.submitting ? "Submitting..." : "Submit Review"}

View File

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

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useLocation, Link } from "react-router-dom"; import { useLocation, Link } from "react-router-dom";
import { X, ChevronLeft, Plus, Trash2 } from "lucide-react"; import { X, ChevronLeft, Trash2 } from "lucide-react";
const Selling = () => { const Selling = () => {
const [products, setProducts] = useState([]); const [products, setProducts] = useState([]);
@@ -8,14 +8,13 @@ const Selling = () => {
const storedUser = JSON.parse(sessionStorage.getItem("user")); const storedUser = JSON.parse(sessionStorage.getItem("user"));
const [categories, setCategories] = useState([]); const [categories, setCategories] = useState([]);
const [categoryMapping, setCategoryMapping] = useState({}); const [categoryMapping, setCategoryMapping] = useState({});
const [selectedCategory, setSelectedCategory] = useState("");
const [originalProduct, setOriginalProduct] = useState(null); const [originalProduct, setOriginalProduct] = useState(null);
const [editingProduct, setEditingProduct] = useState({ const [editingProduct, setEditingProduct] = useState({
name: "", name: "",
price: "", price: "",
description: "", description: "",
categories: [], category: "",
images: [], images: [],
}); });
@@ -59,7 +58,7 @@ const Selling = () => {
fetchCategories(); fetchCategories();
}, []); }, []);
// Simulate fetching products from API/database on component mount // Fetch products from API/database on component mount
useEffect(() => { useEffect(() => {
const fetchProducts = async () => { const fetchProducts = async () => {
try { try {
@@ -89,61 +88,87 @@ const Selling = () => {
}; };
fetchProducts(); fetchProducts();
}, []); // Add userId to dependency array if it might change });
// When editing a product, save the original product properly
const handleEditProduct = (product) => { const handleEditProduct = (product) => {
// Save the original product completely
setOriginalProduct(product); setOriginalProduct(product);
// Convert category ID to category name if needed
const categoryName = getCategoryNameById(product.CategoryID); const categoryName = getCategoryNameById(product.CategoryID);
setEditingProduct({ setEditingProduct({
...product, ...product,
categories: categoryName ? [categoryName] : [], category: categoryName || "", // Single category string
images: product.images || [], // Ensure images array exists images: product.images || [],
}); });
setShowForm(true); setShowForm(true);
}; };
// Then update the handleSaveProduct function to properly merge values // 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
const handleSaveProduct = async () => { const handleSaveProduct = async () => {
if (!(editingProduct.categories || []).length) { if (!editingProduct.category) {
alert("Please select at least one category"); alert("Please select a category");
return; return;
} }
try { try {
const imagePaths = []; let imagePaths = [];
// Handle images properly // Handle image uploads and get their paths
if (editingProduct.images && editingProduct.images.length > 0) { if (editingProduct.images && editingProduct.images.length > 0) {
// If there are new images uploaded (File objects) imagePaths = await uploadImages(editingProduct.images);
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) { } else if (originalProduct?.image_url) {
// If no new images but there was an original image URL // If no new images but there was an original image URL
imagePaths.push(originalProduct.image_url); imagePaths = [originalProduct.image_url];
} }
const categoryName = (editingProduct.categories || [])[0];
const categoryID = const categoryID =
categoryMapping[categoryName] || originalProduct?.CategoryID || 1; categoryMapping[editingProduct.category] ||
originalProduct?.CategoryID ||
1;
// Create payload with proper fallback to original values // Create payload with proper fallback to original values
const payload = { const payload = {
@@ -166,12 +191,7 @@ const Selling = () => {
originalProduct?.Description || originalProduct?.Description ||
"", "",
category: categoryID, category: categoryID,
images: images: imagePaths.length > 0 ? imagePaths : [],
imagePaths.length > 0
? imagePaths
: originalProduct?.image_url
? [originalProduct.image_url]
: [],
}; };
console.log("Sending payload:", payload); console.log("Sending payload:", payload);
@@ -206,7 +226,7 @@ const Selling = () => {
name: "", name: "",
price: "", price: "",
description: "", description: "",
categories: [], category: "",
images: [], images: [],
}); });
@@ -243,7 +263,7 @@ const Selling = () => {
throw new Error("Network response was not ok"); throw new Error("Network response was not ok");
} }
} catch (error) { } catch (error) {
console.error("Error fetching products:", error); console.error("Error deleting product:", error);
// You might want to set an error state here // You might want to set an error state here
} }
}; };
@@ -267,53 +287,18 @@ const Selling = () => {
name: "", name: "",
price: "", price: "",
description: "", description: "",
categories: [], category: "",
images: [], images: [],
}); });
setShowForm(true); setShowForm(true);
}; };
const addCategory = () => { // Handle category change
if ( const handleCategoryChange = (e) => {
selectedCategory && setEditingProduct({
!(editingProduct.categories || []).includes(selectedCategory) ...editingProduct,
) { category: e.target.value,
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 ( return (
@@ -323,7 +308,7 @@ const Selling = () => {
{!showForm && ( {!showForm && (
<button <button
onClick={handleAddProduct} onClick={handleAddProduct}
className="bg-emerald-600 text-white px-4 py-2 hover:bg-emerald-700" className="bg-emerald-700 text-white px-4 py-2 hover:bg-emerald-700"
> >
+ Add New Product + Add New Product
</button> </button>
@@ -335,7 +320,7 @@ const Selling = () => {
{/* Back Button */} {/* Back Button */}
<button <button
onClick={() => setShowForm(false)} onClick={() => setShowForm(false)}
className="mb-4 text-emerald-600 hover:text-emerald-800 flex items-center gap-1" className="mb-4 text-emerald-700 hover:text-emerald-800 flex items-center gap-1"
> >
<ChevronLeft size={16} /> <ChevronLeft size={16} />
<span>Back to Listings</span> <span>Back to Listings</span>
@@ -363,7 +348,7 @@ const Selling = () => {
name: e.target.value, name: e.target.value,
}) })
} }
className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-500 focus:outline-none" className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-600 focus:outline-none"
/> />
</div> </div>
@@ -382,78 +367,33 @@ const Selling = () => {
price: e.target.value, price: e.target.value,
}) })
} }
className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-500 focus:outline-none" className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-600 focus:outline-none"
/> />
</div> </div>
{/* Sold Status */} {/* Category - Single Selection Dropdown */}
<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"> <div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Categories Category
</label> </label>
<div className="flex gap-2"> <select
<select value={editingProduct.category || ""}
value={selectedCategory} onChange={handleCategoryChange}
onChange={(e) => setSelectedCategory(e.target.value)} className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-600 focus:outline-none"
className="flex-1 px-3 py-2 border border-gray-300 focus:border-emerald-500 focus:outline-none" required
> >
<option value="" disabled> <option value="" disabled>
Select a category Select a category
</option>
{categories.map((category, index) => (
<option key={index} value={category}>
{category}
</option> </option>
{categories ))}
.filter( </select>
(cat) => !(editingProduct.categories || []).includes(cat), {!editingProduct.category && (
)
.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"> <p className="text-xs text-gray-500 mt-1">
Please select at least one category Please select a category
</p> </p>
)} )}
</div> </div>
@@ -475,7 +415,7 @@ const Selling = () => {
}) })
} }
rows="4" rows="4"
className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-500 focus:outline-none" className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-600 focus:outline-none"
placeholder="Describe your product in detail..." placeholder="Describe your product in detail..."
></textarea> ></textarea>
</div> </div>
@@ -504,8 +444,8 @@ const Selling = () => {
htmlFor="image-upload" htmlFor="image-upload"
className="block w-full p-3 border border-gray-300 bg-gray-50 text-center cursor-pointer hover:bg-gray-100" 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-600 font-medium"> <span className="text-emerald-700 font-medium">
Click to upload images Click to upload images (will be saved to /public/uploads)
</span> </span>
</label> </label>
@@ -535,7 +475,11 @@ const Selling = () => {
className="relative w-20 h-20 border border-gray-200 overflow-hidden" className="relative w-20 h-20 border border-gray-200 overflow-hidden"
> >
<img <img
src={URL.createObjectURL(img)} src={
typeof img === "string"
? img
: URL.createObjectURL(img)
}
alt={`Product ${idx + 1}`} alt={`Product ${idx + 1}`}
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
@@ -559,18 +503,19 @@ const Selling = () => {
)} )}
{/* Show current image if editing */} {/* Show current image if editing */}
{editingProduct.image_url && ( {editingProduct.image_url &&
<div className="mt-3"> !(editingProduct.images || []).length && (
<p className="text-sm text-gray-600 mb-2">Current image:</p> <div className="mt-3">
<div className="relative w-20 h-20 border border-gray-200 overflow-hidden"> <p className="text-sm text-gray-600 mb-2">Current image:</p>
<img <div className="relative w-20 h-20 border border-gray-200 overflow-hidden">
src={editingProduct.image_url} <img
alt="Current product" src={editingProduct.image_url}
className="w-full h-full object-cover" alt="Current product"
/> className="w-full h-full object-cover"
/>
</div>
</div> </div>
</div> )}
)}
</div> </div>
</div> </div>
@@ -583,22 +528,9 @@ const Selling = () => {
Cancel Cancel
</button> </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 <button
onClick={handleSaveProduct} onClick={handleSaveProduct}
className="bg-emerald-600 text-white px-6 py-2 hover:bg-emerald-700 rounded-md" className="bg-emerald-700 text-white px-6 py-2 hover:bg-emerald-700 rounded-md"
> >
{editingProduct.ProductID ? "Update Product" : "Add Product"} {editingProduct.ProductID ? "Update Product" : "Add Product"}
</button> </button>
@@ -613,7 +545,7 @@ const Selling = () => {
</p> </p>
<button <button
onClick={handleAddProduct} onClick={handleAddProduct}
className="bg-emerald-600 text-white px-4 py-2 hover:bg-emerald-700" className="bg-emerald-700 text-white px-4 py-2 hover:bg-emerald-700"
> >
Create Your First Listing Create Your First Listing
</button> </button>
@@ -645,7 +577,7 @@ const Selling = () => {
</h3> </h3>
</div> </div>
<p className="text-emerald-600 font-bold mt-1"> <p className="text-emerald-700 font-bold mt-1">
${product.Price} ${product.Price}
</p> </p>
@@ -679,7 +611,7 @@ const Selling = () => {
e.preventDefault(); e.preventDefault();
handleEditProduct(product); handleEditProduct(product);
}} }}
className="text-emerald-600 hover:text-emerald-800 font-medium" className="text-emerald-700 hover:text-emerald-800 font-medium"
> >
Edit Edit
</button> </button>
@@ -692,6 +624,11 @@ 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> </div>
); );
}; };

View File

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

View File

@@ -1,91 +0,0 @@
import { useEffect, useState } from "react";
import { getTransactions, removeTransaction } from "../api/admin";
import { MdDelete } from "react-icons/md";
import Pagination from "../components/Pagination";
export default function TransactionDashboard() {
const [transactions, setTransactions] = useState([]);
const [total, setTotal] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
let pageLimit = 10;
const onChangePage = (page, limit = 10) => {
setCurrentPage(page);
fetchTransactions(page, limit);
};
const fetchTransactions = (page = 1, limit = 10) => {
getTransactions(page, limit).then(({ transactions, total }) => {
setTotal(total);
setTransactions(transactions);
});
};
const handleRemoveTransaction = (id) => {
removeTransaction(id)
.then(() => {
fetchTransactions(currentPage);
})
.catch((err) => {
console.log(err);
});
};
//Get user when initialize the component
useEffect(fetchTransactions, []);
return (
<div className="pt-10 p-20">
<h1 className="text-4xl pb-3 font-bold text-green-800 underline">
TRANSACTIONS
</h1>
{transactions.length > 0 ? (
<>
<table className="table-fixed w-full text-center border border-green-600">
<thead className="bg-green-600 h-10">
<tr>
<th>TransactionID</th>
<th>User</th>
<th>Product</th>
<th>Date</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{transactions.map((t) => (
<tr
key={t.TransactionID}
className="border border-green-600 h-10"
>
<td>{t.TransactionID}</td>
<td>{t.UserName ? t.UserName : "N/A"}</td>
<td>{t.ProductName ? t.ProductName : "N/A"}</td>
<td>{t.Date}</td>
<td>{t.PaymentStatus}</td>
<td className="flex justify-center pt-2">
<MdDelete
onClick={() => {
handleRemoveTransaction(t.TransactionID);
}}
className="hover:text-red-600 cursor-pointer transition-all text-xl"
/>
</td>
</tr>
))}
</tbody>
</table>
<Pagination
onChange={onChangePage}
pageNum={Math.ceil(total / pageLimit)}
/>
</>
) : (
<p className="text-red-700 text-xl bg-red-200 px-3 rounded-md py-1 w-fit">
No transaction exists!
</p>
)}
</div>
);
}

View File

@@ -1,8 +1,209 @@
import { useState } from "react"; import { useState, useEffect } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Calendar, CreditCard, Trash2 } from "lucide-react";
import FloatingAlert from "../components/FloatingAlert"; // adjust path if needed
const Transactions = () => { const Transactions = () => {
return <div></div>; const [transactions, setTransactions] = useState([]);
const [showAlert, setShowAlert] = useState(false);
const storedUser = JSON.parse(sessionStorage.getItem("user"));
function reloadPage() {
const docTimestamp = new Date(performance.timing.domLoading).getTime();
const now = Date.now();
if (now > docTimestamp) {
location.reload();
}
}
useEffect(() => {
const fetchTransactions = async () => {
try {
const response = await fetch(
"http://localhost:3030/api/transaction/getTransactionsByUser",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userID: storedUser.ID }),
},
);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const { transactions: txData } = await response.json();
if (!Array.isArray(txData)) return;
console.log(txData);
setTransactions(
txData.map((tx) => ({
id: tx.TransactionID,
productId: tx.ProductID,
name: tx.ProductName,
price: tx.Price != null ? parseFloat(tx.Price) : null,
image: tx.Image_URL,
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));
reloadPage();
} else {
console.error("Delete failed:", data.message);
}
} catch (err) {
console.error("Error deleting transaction:", err);
}
};
const updateTransaction = async (id) => {
try {
const res = await fetch(
"http://localhost:3030/api/transaction/updateStatus",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ transactionID: id }),
},
);
const data = await res.json();
if (data) {
setShowAlert(true);
reloadPage();
}
} 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">
{showAlert && (
<FloatingAlert
message="Status Updated"
onClose={() => setShowAlert(false)}
/>
)}
<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"
>
<div className="absolute bottom-2 right-2 flex gap-2 z-10">
<button
onClick={(e) => {
e.preventDefault();
updateTransaction(tx.id);
}}
className="text-emerald-600 hover:text-emerald-700 text-sm font-medium"
>
Complete
</button>
<button
onClick={(e) => {
e.preventDefault();
deleteTransaction(tx.id);
}}
className="text-red-500 hover:text-red-600"
>
<Trash2 size={20} />
</button>
</div>
<Link to={`/product/${tx.productId}`}>
<div className="h-48 bg-gray-200 flex items-center justify-center">
<img
src={tx.image}
alt={tx.name}
className="w-full h-full object-cover"
/>
</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>
);
}; };
export default Transactions; 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

@@ -2,8 +2,6 @@
SET SET
FOREIGN_KEY_CHECKS = 0; FOREIGN_KEY_CHECKS = 0;
TRUNCATE TABLE Product_Category;
TRUNCATE TABLE Favorites; TRUNCATE TABLE Favorites;
TRUNCATE TABLE History; TRUNCATE TABLE History;
@@ -44,7 +42,7 @@ VALUES
( (
1, 1,
'John Doe', 'John Doe',
'john.doe@example.com', 'john.doe@ucalgary.ca',
'U123456', 'U123456',
'hashedpassword1', 'hashedpassword1',
'555-123-4567', '555-123-4567',
@@ -53,7 +51,7 @@ VALUES
( (
2, 2,
'Jane Smith', 'Jane Smith',
'jane.smith@example.com', 'jane.smith@ucalgary.ca',
'U234567', 'U234567',
'hashedpassword2', 'hashedpassword2',
'555-234-5678', '555-234-5678',
@@ -72,6 +70,7 @@ VALUES
INSERT INTO INSERT INTO
Category (Name) Category (Name)
VALUES VALUES
('Other'),
('Textbooks'), ('Textbooks'),
('Electronics'), ('Electronics'),
('Furniture'), ('Furniture'),
@@ -100,8 +99,7 @@ VALUES
('Winter Clothing'), ('Winter Clothing'),
('Photography Equipment'), ('Photography Equipment'),
('Event Tickets'), ('Event Tickets'),
('Software Licenses'), ('Software Licenses');
('Transportation (Car Pool)');
-- Insert Products -- Insert Products
INSERT INTO INSERT INTO
@@ -344,62 +342,6 @@ VALUES
('/Uploads/Bluetooth-Speaker.jpg', 19), ('/Uploads/Bluetooth-Speaker.jpg', 19),
('/Uploads/Backpack.jpg', 20); ('/Uploads/Backpack.jpg', 20);
-- Insert Product-Category relationships (products with multiple categories)
INSERT INTO
Product_Category (ProductID, CategoryID)
VALUES
(1, 1),
(1, 17),
(1, 20), -- Calculus book: Textbooks, School Supplies, Math Resources
(2, 2),
(2, 11),
(2, 25), -- Laptop: Electronics, Computer Accessories, Smartphones & Tablets
(3, 3),
(3, 18),
(3, 24), -- Desk: Furniture, Office Furniture, Dorm Essentials
(4, 4),
(4, 26), -- Hoodie: Clothing, Winter Clothing
(5, 5),
(5, 13), -- Basketball: Sports Equipment, Fitness Equipment
(6, 6),
(6, 23), -- Guitar: Musical Instruments, Audio Equipment
(7, 1),
(7, 15),
(7, 20), -- Physics book: Textbooks, Lab Equipment, Math & Science Resources
(8, 8),
(8, 24), -- Mini Fridge: Kitchen Appliances, Dorm Essentials
(9, 9),
(9, 2), -- PS5 Controller: Gaming, Electronics
(10, 10),
(10, 5),
(10, 13), -- Mountain Bike: Bicycles, Sports Equipment, Fitness Equipment
(11, 11),
(11, 2), -- Mouse: Computer Accessories, Electronics
(12, 15),
(12, 17), -- Lab Coat: Lab Equipment, School Supplies
(13, 12),
(13, 17),
(13, 20), -- Calculator: Stationery, School Supplies, Math & Science Resources
(14, 13),
(14, 5), -- Yoga Mat: Fitness Equipment, Sports Equipment
(15, 26),
(15, 4),
(15, 14), -- Winter Jacket: Winter Clothing, Clothing, Winter Sports
(16, 1),
(16, 17),
(16, 19), -- CS Book: Textbooks, School Supplies, Books (Non-textbook)
(17, 24),
(17, 2), -- Desk Lamp: Dorm Essentials, Electronics
(18, 12),
(18, 17),
(18, 20), -- Scientific Calculator: Stationery, School Supplies, Math & Science
(19, 23),
(19, 2),
(19, 24), -- Bluetooth Speaker: Audio Equipment, Electronics, Dorm Essentials
(20, 22),
(20, 17),
(20, 24);
-- Insert History records -- Insert History records
INSERT INTO INSERT INTO
History (HistoryID, UserID, ProductID) History (HistoryID, UserID, ProductID)
@@ -428,7 +370,6 @@ VALUES
(1, 5), -- User 4 likes Basketball (1, 5), -- User 4 likes Basketball
(2, 8); (2, 8);
-- User 5 likes Mini Fridge
-- Insert Transactions -- Insert Transactions
INSERT INTO INSERT INTO
Transaction ( Transaction (
@@ -453,4 +394,5 @@ VALUES
1, 1,
'This is a great fake product! Totally recommend it.', 'This is a great fake product! Totally recommend it.',
5, 5,
); '2024-10-02 16:00:00'
)

View File

@@ -36,10 +36,10 @@ CREATE TABLE Product (
StockQuantity INT, StockQuantity INT,
UserID INT, UserID INT,
Description TEXT, Description TEXT,
CategoryID INT NOT NULL, CategoryID INT,
Date DATETIME DEFAULT CURRENT_TIMESTAMP, Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE SET NULL, FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID) ON DELETE SET NULL FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID)
); );
-- Fixed Image_URL table -- Fixed Image_URL table
@@ -60,19 +60,19 @@ CREATE TABLE Review (
AND Rating <= 5 AND Rating <= 5
), ),
Date DATETIME DEFAULT CURRENT_TIMESTAMP, Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE SET NULL, 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 CASCADE
); );
-- Transaction Entity (Many-to-One with User, Many-to-One with Product) -- Transaction Entity (Many-to-One with User, Many-to-One with Product)
CREATE TABLE Transaction ( CREATE TABLE Transaction (
TransactionID INT PRIMARY KEY, TransactionID INT AUTO_INCREMENT PRIMARY KEY,
UserID INT, UserID INT,
ProductID INT, ProductID INT,
Date DATETIME DEFAULT CURRENT_TIMESTAMP, Date DATETIME DEFAULT CURRENT_TIMESTAMP,
PaymentStatus VARCHAR(50), PaymentStatus VARCHAR(50),
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE, 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 CASCADE
); );
-- Recommendation Entity (Many-to-One with User, Many-to-One with Product) -- Recommendation Entity (Many-to-One with User, Many-to-One with Product)
@@ -105,15 +105,6 @@ CREATE TABLE Favorites (
UNIQUE (UserID, ProductID) 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 -- Login Authentication table
CREATE TABLE AuthVerification ( CREATE TABLE AuthVerification (
UserID INT AUTO_INCREMENT PRIMARY KEY, UserID INT AUTO_INCREMENT PRIMARY KEY,

View File

@@ -19,9 +19,9 @@ def delete_user_recommendations(user_id):
cursor = db_con.cursor() cursor = db_con.cursor()
try: try:
cursor.execute("DELETE FROM Recommendation WHERE UserID = %s", (user_id,))
db_con.commit()
print(f"Deleted existing recommendations for user {user_id}") print(f"Deleted existing recommendations for user {user_id}")
cursor.execute(f"DELETE FROM Recommendation WHERE UserID = {user_id}")
db_con.commit()
logging.info(f"Deleted existing recommendations for user {user_id}") logging.info(f"Deleted existing recommendations for user {user_id}")
return True return True
except Exception as e: except Exception as e:
@@ -32,14 +32,12 @@ def delete_user_recommendations(user_id):
cursor.close() cursor.close()
db_con.close() db_con.close()
def get_random_products(count=10, exclude_list=None): def get_random_products(count=0, exclude_list=None):
"""Get random products from the database, excluding any in the exclude_list"""
db_con = database() db_con = database()
cursor = db_con.cursor() cursor = db_con.cursor()
try: try:
if exclude_list and len(exclude_list) > 0: if exclude_list and len(exclude_list) > 0:
# Convert exclude_list to string for SQL IN clause
exclude_str = ', '.join(map(str, exclude_list)) exclude_str = ', '.join(map(str, exclude_list))
cursor.execute(f"SELECT ProductID FROM Product WHERE ProductID NOT IN ({exclude_str}) ORDER BY RAND() LIMIT {count}") cursor.execute(f"SELECT ProductID FROM Product WHERE ProductID NOT IN ({exclude_str}) ORDER BY RAND() LIMIT {count}")
else: else:
@@ -55,13 +53,11 @@ def get_random_products(count=10, exclude_list=None):
cursor.close() cursor.close()
db_con.close() db_con.close()
def get_popular_products(count=10): def get_popular_products(count=5):
"""Get popular products based on history table frequency"""
db_con = database() db_con = database()
cursor = db_con.cursor() cursor = db_con.cursor()
try: try:
# Get products that appear most frequently in history
cursor.execute(""" cursor.execute("""
SELECT ProductID, COUNT(*) as count SELECT ProductID, COUNT(*) as count
FROM History FROM History
@@ -72,7 +68,6 @@ def get_popular_products(count=10):
popular_products = [row[0] for row in cursor.fetchall()] popular_products = [row[0] for row in cursor.fetchall()]
# If not enough popular products, supplement with random ones
if len(popular_products) < count: if len(popular_products) < count:
random_products = get_random_products(count - len(popular_products), popular_products) random_products = get_random_products(count - len(popular_products), popular_products)
popular_products.extend(random_products) popular_products.extend(random_products)
@@ -81,23 +76,20 @@ def get_popular_products(count=10):
except Exception as e: except Exception as e:
logging.error(f"Error getting popular products: {str(e)}") logging.error(f"Error getting popular products: {str(e)}")
return get_random_products(count) # Fallback to random products return get_random_products(count)
finally: finally:
cursor.close() cursor.close()
db_con.close() db_con.close()
def has_user_history_or_recommendations(user_id): def has_user_history_or_recommendations(user_id):
"""Check if user exists in History or Recommendation table"""
db_con = database() db_con = database()
cursor = db_con.cursor() cursor = db_con.cursor()
try: try:
# Check if user has history cursor.execute(f"SELECT COUNT(*) FROM History WHERE UserID = {user_id}" )
cursor.execute("SELECT COUNT(*) FROM History WHERE UserID = %s", (user_id,))
history_count = cursor.fetchone()[0] history_count = cursor.fetchone()[0]
# Check if user has recommendations cursor.execute(f"SELECT COUNT(*) FROM Recommendation WHERE UserID = {user_id}")
cursor.execute("SELECT COUNT(*) FROM Recommendation WHERE UserID = %s", (user_id,))
recommendation_count = cursor.fetchone()[0] recommendation_count = cursor.fetchone()[0]
return history_count > 0 or recommendation_count > 0 return history_count > 0 or recommendation_count > 0
@@ -120,13 +112,11 @@ def get_all_products():
select_clause = "SELECT p.ProductID" select_clause = "SELECT p.ProductID"
for category in categories: for category in categories:
category_id = category[0] category_id = category[0]
select_clause += f", MAX(CASE WHEN pc.CategoryID = {category_id} THEN 1 ELSE 0 END) AS `Cat_{category_id}`" select_clause += f", MAX(CASE WHEN p.CategoryID = {category_id} THEN 1 ELSE 0 END) AS `Cat_{category_id}`"
final_query = f""" final_query = f"""
{select_clause} {select_clause}
FROM Product p FROM Product p
LEFT JOIN Product_Category pc ON p.ProductID = pc.ProductID
LEFT JOIN Category c ON pc.CategoryID = c.CategoryID
GROUP BY p.ProductID; GROUP BY p.ProductID;
""" """
@@ -137,13 +127,13 @@ def get_all_products():
product_ids = [] product_ids = []
for row in results: for row in results:
text_list = list(row) text_list = list(row)
product_id = text_list.pop(0) # Save the product ID before removing it product_id = text_list.pop(0)
final.append(text_list) final.append(text_list)
product_ids.append(product_id) product_ids.append(product_id)
cursor.close() cursor.close()
db_con.close() db_con.close()
return final, product_ids # Return both feature vectors and product IDs return final, product_ids
except Exception as e: except Exception as e:
logging.error(f"Error getting all products: {str(e)}") logging.error(f"Error getting all products: {str(e)}")
cursor.close() cursor.close()
@@ -160,15 +150,13 @@ def get_user_history(user_id):
select_clause = "SELECT p.ProductID" select_clause = "SELECT p.ProductID"
for category in categories: for category in categories:
category_id = category[0] # get the uid of the category and then append that to the new column category_id = category[0]
select_clause += f", MAX(CASE WHEN pc.CategoryID = {category_id} THEN 1 ELSE 0 END) AS `Cat_{category_id}`" select_clause += f", MAX(CASE WHEN p.CategoryID = {category_id} THEN 1 ELSE 0 END) AS `Cat_{category_id}`"
final_query = f""" final_query = f"""
{select_clause} {select_clause}
FROM Product p FROM Product p
LEFT JOIN Product_Category pc ON p.ProductID = pc.ProductID WHERE p.ProductID IN (SELECT ProductID FROM History WHERE UserID = {user_id})
LEFT JOIN Category c ON pc.CategoryID = c.CategoryID
where p.ProductID in (select ProductID from History where UserID = {user_id})
GROUP BY p.ProductID; GROUP BY p.ProductID;
""" """
@@ -191,82 +179,63 @@ def get_user_history(user_id):
def get_recommendations(user_id, top_n=5): def get_recommendations(user_id, top_n=5):
try: try:
# Always delete existing recommendations first
delete_user_recommendations(user_id) delete_user_recommendations(user_id)
# Check if user has history or recommendations
if not has_user_history_or_recommendations(user_id): if not has_user_history_or_recommendations(user_id):
# Cold start: return random products
random_recs = get_random_products(top_n) random_recs = get_random_products(top_n)
# Store these random recommendations recommendation_upload(user_id, random_recs)
history_upload(user_id, random_recs)
# Add 5 more unique random products
additional_random = get_random_products(5, random_recs) additional_random = get_random_products(5, random_recs)
history_upload(user_id, additional_random) recommendation_upload(user_id, additional_random)
return random_recs + additional_random return random_recs + additional_random
# Get all products and user history with their category vectors
all_product_features, all_product_ids = get_all_products() all_product_features, all_product_ids = get_all_products()
user_history = get_user_history(user_id) user_history = get_user_history(user_id)
if not user_history: if not user_history:
# User exists but has no history yet
popular_recs = get_popular_products(top_n) popular_recs = get_popular_products(top_n)
history_upload(user_id, popular_recs) recommendation_upload(user_id, popular_recs)
# Add 5 more unique random products
additional_random = get_random_products(5, popular_recs) additional_random = get_random_products(5, popular_recs)
history_upload(user_id, additional_random) recommendation_upload(user_id, additional_random)
return popular_recs + additional_random return popular_recs + additional_random
# Calculate similarity between all products and user history user_profile = np.mean(user_history, axis=0)
user_profile = np.mean(user_history, axis=0) # Average user preferences
similarities = cosine_similarity([user_profile], all_product_features) similarities = cosine_similarity([user_profile], all_product_features)
print(similarities)
# Get indices of the top N products sorted by similarity
product_indices = similarities[0].argsort()[-top_n:][::-1] product_indices = similarities[0].argsort()[-top_n:][::-1]
# Get the actual product IDs using the indices
recommended_product_ids = [all_product_ids[i] for i in product_indices] recommended_product_ids = [all_product_ids[i] for i in product_indices]
print(recommended_product_ids)
# Upload the core recommendations to the database recommendation_upload(user_id, recommended_product_ids)
history_upload(user_id, recommended_product_ids)
# Add 5 more unique random products that aren't in the recommendations
additional_random = get_random_products(5, recommended_product_ids) additional_random = get_random_products(5, recommended_product_ids)
history_upload(user_id, additional_random) recommendation_upload(user_id, additional_random)
# Return both the similarity-based recommendations and the random ones
return recommended_product_ids + additional_random return recommended_product_ids + additional_random
except Exception as e: except Exception as e:
logging.error(f"Recommendation error for user {user_id}: {str(e)}") logging.error(f"Recommendation error for user {user_id}: {str(e)}")
# Fallback to random products
random_products = get_random_products(top_n + 5) random_products = get_random_products(top_n + 5)
return random_products return random_products
def history_upload(userID, products): def recommendation_upload(userID, products):
"""Upload product recommendations to the database"""
db_con = database() db_con = database()
cursor = db_con.cursor() cursor = db_con.cursor()
try: try:
for product_id in products: for product_id in products:
# Use parameterized queries to prevent SQL injection
cursor.execute("INSERT INTO Recommendation (UserID, RecommendedProductID) VALUES (%s, %s)", cursor.execute("INSERT INTO Recommendation (UserID, RecommendedProductID) VALUES (%s, %s)",
(userID, product_id)) (userID, product_id))
# Commit the changes
db_con.commit() db_con.commit()
except Exception as e: except Exception as e:
logging.error(f"Error uploading recommendations: {str(e)}") logging.error(f"Error uploading recommendations: {str(e)}")
db_con.rollback() db_con.rollback()
finally: finally:
# Close the cursor and connection
cursor.close() cursor.close()
db_con.close() db_con.close()