20 Commits

Author SHA1 Message Date
estherdev03
15eddb8d13 Fix clearn up expired code 2025-04-20 22:14:04 -06:00
estherdev03
2e98a88db9 Merge branch 'esther2' of https://github.com/MannPatel0/Campus-Plug into esther2 2025-04-20 22:08:38 -06:00
Esther Tran
bcb912b6ce Merge pull request #5 from MannPatel0/main
merge
2025-04-20 22:08:03 -06:00
Mann Patel
8347689f6c uploads 2025-04-20 22:07:40 -06:00
estherdev03
e83b3640a5 Merge branch 'esther2' of https://github.com/MannPatel0/Campus-Plug into esther2 2025-04-20 22:05:42 -06:00
estherdev03
bd69bed934 Merge branch 'main' of https://github.com/MannPatel0/Campus-Plug into esther2 2025-04-20 22:03:32 -06:00
estherdev03
51dffdae30 fix clean up expired code 2025-04-20 22:00:19 -06:00
Mann Patel
1c8a7522e2 Merge branch 'mann-Branch' 2025-04-20 21:52:15 -06:00
Mann Patel
77a35810fd home and selligns page polished 2025-04-20 21:42:34 -06:00
Esther Tran
8dcaff3d6d Merge pull request #4 from MannPatel0/main
Merge pull request #3 from MannPatel0/esther2
2025-04-20 21:01:54 -06:00
Esther Tran
51cd90a409 Merge pull request #3 from MannPatel0/esther2
Finish admin
2025-04-20 20:59:36 -06:00
estherdev03
444b436983 Add transaction section to admin dashboard 2025-04-20 20:56:14 -06:00
Mann Patel
bcf849611f selling's page is now complete 2025-04-20 20:46:27 -06:00
Mann Patel
f223f3717d updating work 2025-04-20 19:09:02 -06:00
Mann Patel
0c08dbc5ce updating products 2025-04-20 17:46:00 -06:00
Mann Patel
6ef4a22e9f update on category 2025-04-20 12:50:46 -06:00
estherdev03
26cd50ab6f Finish admin dashboard and update sql code 2025-04-20 07:50:57 -06:00
estherdev03
7a2250369e Finish admin dashboard and update sql code 2025-04-20 07:48:20 -06:00
Mann Patel
d169c9ba58 update 2025-04-19 10:22:16 -06:00
Mann Patel
dee6e3ce10 getCategory update 2025-04-18 22:09:04 -06:00
64 changed files with 2526 additions and 1145 deletions

View File

@@ -0,0 +1,70 @@
const db = require("../utils/database");
exports.getAllCategoriesWithPagination = async (req, res) => {
const limit = +req.query?.limit;
const page = +req.query?.page;
const offset = (page - 1) * limit;
try {
const [data, _] = await db.execute(
"SELECT * FROM Category C ORDER BY C.CategoryID ASC LIMIT ? OFFSET ?",
[limit.toString(), offset.toString()],
);
const [result] = await db.execute("SELECT COUNT(*) AS count FROM Category");
const { count: total } = result[0];
return res.json({ data, total });
} catch (error) {
res.json({ error: "Cannot fetch categories from database!" });
}
};
exports.addCategory = async (req, res) => {
const { name } = req.body;
try {
const [result] = await db.execute(
"INSERT INTO Category (Name) VALUES (?)",
[name],
);
res.json({ message: "Adding new category successfully!" });
} catch (error) {
res.json({ error: "Cannot add new category!" });
}
};
exports.removeCategory = async (req, res) => {
const { id } = req.params;
try {
const [result] = await db.execute(
`DELETE FROM Category WHERE CategoryID = ?`,
[id],
);
res.json({ message: "Delete category successfully!" });
} catch (error) {
res.json({ error: "Cannot remove category from database!" });
}
};
exports.getAllCategory = async (req, res) => {
try {
const [data, fields] = await db.execute(`SELECT * FROM Category`);
const formattedData = {};
data.forEach((row) => {
formattedData[row.CategoryID] = row.Name;
});
res.json({
success: true,
message: "Categories fetched successfully",
data: formattedData,
});
} catch (error) {
console.error("Error fetching categories:", error);
return res.status(500).json({
success: false,
error: "Database error occurred",
});
}
};

View File

@@ -32,11 +32,49 @@ exports.addProduct = async (req, res) => {
} }
}; };
exports.removeProduct = async (req, res) => {
const { userID, productID } = req.body;
console.log(userID);
try {
// First delete images
await db.execute(`DELETE FROM Image_URL 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 Product_Category WHERE ProductID = ?`, [
productID,
]);
await db.execute(`DELETE FROM Product_Category WHERE ProductID = ?`, [
productID,
]);
await db.execute(`DELETE FROM Transaction WHERE ProductID = ?`, [
productID,
]);
await db.execute(
`DELETE FROM Recommendation WHERE RecommendedProductID = ?`,
[productID],
);
// Then delete the product
await db.execute(`DELETE FROM Product WHERE UserID = ? AND ProductID = ?`, [
userID,
productID,
]);
res.json({
success: true,
message: "Product removed successfully",
});
} catch (error) {
console.error("Error removing product:", error);
return res.json({ error: "Could not remove product" });
}
};
exports.addFavorite = async (req, res) => { exports.addFavorite = async (req, res) => {
const { userID, productID } = req.body; const { userID, productID } = req.body;
console.log(userID); console.log(userID);
try { try {
// Use parameterized query to prevent SQL injection
const [result] = await db.execute( const [result] = await db.execute(
`INSERT INTO Favorites (UserID, ProductID) VALUES (?, ?)`, `INSERT INTO Favorites (UserID, ProductID) VALUES (?, ?)`,
[userID, productID], [userID, productID],
@@ -72,6 +110,103 @@ exports.removeFavorite = async (req, res) => {
} }
}; };
exports.updateProduct = async (req, res) => {
const { productId } = req.params;
const { name, description, price, category, images } = req.body;
console.log(productId);
const connection = await db.getConnection();
try {
await connection.beginTransaction();
// Step 1: Check if the product exists
const [checkProduct] = await connection.execute(
"SELECT * FROM Product WHERE ProductID = ?",
[productId],
);
if (checkProduct.length === 0) {
await connection.rollback();
return res.status(404).json({ error: "Product not found" });
}
// Step 2: Update the product
await connection.execute(
`
UPDATE Product
SET Name = ?, Description = ?, Price = ?, CategoryID = ?
WHERE ProductID = ?
`,
[name, description, price, category, productId],
);
// Step 3: Delete existing images
await connection.execute(`DELETE FROM Image_URL WHERE ProductID = ?`, [
productId,
]);
// Step 4: Insert new image URLs
for (const imageUrl of images) {
await connection.execute(
`INSERT INTO Image_URL (ProductID, URL) VALUES (?, ?)`,
[productId, imageUrl],
);
}
await connection.commit();
res.json({ success: true, message: "Product updated successfully" });
} catch (error) {
await connection.rollback();
console.error("Update product error:", error);
res.status(500).json({ error: "Failed to update product" });
} finally {
connection.release();
}
};
exports.myProduct = async (req, res) => {
const { userID } = req.body;
try {
const [result] = await db.execute(
`
SELECT
p.ProductID,
p.Name,
p.Description,
p.Price,
p.CategoryID,
p.UserID,
p.Date,
u.Name AS SellerName,
MIN(i.URL) AS image_url
FROM Product p
JOIN User u ON p.UserID = u.UserID
LEFT JOIN Image_URL i ON p.ProductID = i.ProductID
WHERE p.UserID = ?
GROUP BY
p.ProductID,
p.Name,
p.Description,
p.Price,
p.CategoryID,
p.UserID,
p.Date,
u.Name;
`,
[userID],
);
res.json({
success: true,
data: result,
});
} catch (error) {
console.error("Error retrieving favorites:", error);
res.status(500).json({ error: "Could not retrieve favorite products" });
}
};
exports.getFavorites = async (req, res) => { exports.getFavorites = async (req, res) => {
const { userID } = req.body; const { userID } = req.body;
@@ -211,32 +346,61 @@ exports.getProductById = async (req, res) => {
} }
}; };
// db_con.query( exports.getProductWithPagination = async (req, res) => {
// "SELECT ProductID FROM product WHERE ProductID = ?", const limit = +req.query.limit;
// [productID], const page = +req.query.page;
// (err, results) => {
// if (err) {
// console.error("Error checking product:", err);
// return res.json({ error: "Database error" });
// }
// if (results.length === 0) { const offset = (page - 1) * limit;
// return res.json({ error: "Product does not exist" });
// }
// },
// );
// db_con.query( try {
// "INSERT INTO Favorites (UserID, ProductID) VALUES (?, ?)", const [data, fields] = await db.execute(
// [userID, productID], `
// (err, result) => { SELECT
// if (err) { P.ProductID,
// console.error("Error adding favorite product:", err); P.Name AS ProductName,
// return res.json({ error: "Could not add favorite product" }); P.Price,
// } P.Date AS DateUploaded,
// res.json({ U.Name AS SellerName,
// success: true, MIN(I.URL) AS ProductImage,
// message: "Product added to favorites successfully", C.Name AS Category
// }); FROM Product P
// }, LEFT JOIN Image_URL I ON P.ProductID = I.ProductID
// ); LEFT JOIN User U ON P.UserID = U.UserID
LEFT JOIN Category C ON P.CategoryID = C.CategoryID
GROUP BY
P.ProductID,
P.Name,
P.Price,
P.Date,
U.Name,
C.Name
ORDER BY P.ProductID ASC
LIMIT ? OFFSET ?
`,
[limit.toString(), offset.toString()],
);
const [result] = await db.execute(
`SELECT COUNT(*) AS totalProd FROM Product`,
);
const { totalProd } = result[0];
return res.json({ totalProd, products: data });
} catch (error) {
res.json({ error: "Error fetching products!" });
}
};
exports.removeProduct = async (req, res) => {
const { id } = req.params;
try {
const [result] = await db.execute(
`DELETE FROM Product WHERE ProductID = ?`,
[id],
);
res.json({ message: "Delete product successfully!" });
} catch (error) {
res.json({ error: "Cannot remove product from database!" });
}
};

View File

@@ -0,0 +1,40 @@
const db = require("../utils/database");
exports.getTransactionWithPagination = async (req, res) => {
const limit = +req.query?.limit;
const page = +req.query?.page;
const offset = (page - 1) * limit;
try {
const [data, _] = await db.execute(
`SELECT T.TransactionID, DATE_FORMAT(T.Date, '%b-%d-%Y %h:%i %p') as Date, T.PaymentStatus, U.Name as UserName, P.Name as ProductName
FROM Transaction T
LEFT JOIN User U ON T.UserID = U.UserID
LEFT JOIN Product P ON T.ProductID = P.ProductID
ORDER BY T.TransactionID ASC LIMIT ? OFFSET ?`,
[limit.toString(), offset.toString()]
);
const [result] = await db.execute(
"SELECT COUNT(*) AS count FROM Transaction"
);
const { count: total } = result[0];
return res.json({ data, total });
} catch (error) {
res.json({ error: "Cannot fetch transactions from database!" });
}
};
exports.removeTransation = async (req, res) => {
const { id } = req.params;
try {
const [result] = await db.execute(
"DELETE FROM Transaction WHERE TransactionID = ?;",
[id.toString()]
);
return res.json({ message: "Remove transaction successfully!" });
} catch (error) {
return res
.status(500)
.json({ error: "Cannot remove transactions from database!" });
}
};

View File

@@ -13,13 +13,13 @@ exports.sendVerificationCode = async (req, res) => {
// Generate a random 6-digit code // Generate a random 6-digit code
const verificationCode = crypto.randomInt(100000, 999999).toString(); const verificationCode = crypto.randomInt(100000, 999999).toString();
console.log( console.log(
`Generated verification code for ${email}: ${verificationCode}`, `Generated verification code for ${email}: ${verificationCode}`
); );
// Check if email already exists in verification table // Check if email already exists in verification table
const [results, fields] = await db.execute( const [results, fields] = await db.execute(
"SELECT * FROM AuthVerification WHERE Email = ?", "SELECT * FROM AuthVerification WHERE Email = ?",
[email], [email]
); );
if (results.length > 0) { if (results.length > 0) {
@@ -27,7 +27,7 @@ exports.sendVerificationCode = async (req, res) => {
const [result] = await db.execute( const [result] = await db.execute(
`UPDATE AuthVerification SET VerificationCode = ?, Authenticated = FALSE, Date = CURRENT_TIMESTAMP `UPDATE AuthVerification SET VerificationCode = ?, Authenticated = FALSE, Date = CURRENT_TIMESTAMP
WHERE Email = ?`, WHERE Email = ?`,
[verificationCode, email], [verificationCode, email]
); );
// Send email and respond // Send email and respond
@@ -37,7 +37,7 @@ exports.sendVerificationCode = async (req, res) => {
// Insert new record // Insert new record
const [result] = await db.execute( const [result] = await db.execute(
"INSERT INTO AuthVerification (Email, VerificationCode, Authenticated) VALUES (?, ?, FALSE)", "INSERT INTO AuthVerification (Email, VerificationCode, Authenticated) VALUES (?, ?, FALSE)",
[email, verificationCode], [email, verificationCode]
); );
// Send email and respond // Send email and respond
await sendVerificationEmail(email, verificationCode); await sendVerificationEmail(email, verificationCode);
@@ -62,7 +62,7 @@ exports.verifyCode = async (req, res) => {
// Check verification code // Check verification code
const [results, fields] = await db.execute( const [results, fields] = await db.execute(
"SELECT * FROM AuthVerification WHERE Email = ? AND VerificationCode = ? AND Authenticated = 0 AND Date > DATE_SUB(NOW(), INTERVAL 15 MINUTE)", "SELECT * FROM AuthVerification WHERE Email = ? AND VerificationCode = ? AND Authenticated = 0 AND Date > DATE_SUB(NOW(), INTERVAL 15 MINUTE)",
[email, code], [email, code]
); );
if (results.length === 0) { if (results.length === 0) {
console.log(`Invalid or expired verification code for email ${email}`); console.log(`Invalid or expired verification code for email ${email}`);
@@ -76,7 +76,7 @@ exports.verifyCode = async (req, res) => {
// Mark as authenticated // Mark as authenticated
const [result] = await db.execute( const [result] = await db.execute(
"UPDATE AuthVerification SET Authenticated = TRUE WHERE Email = ?", "UPDATE AuthVerification SET Authenticated = TRUE WHERE Email = ?",
[email], [email]
); );
res.json({ res.json({
success: true, success: true,
@@ -95,7 +95,7 @@ exports.completeSignUp = async (req, res) => {
try { try {
const [results, fields] = await db.execute( const [results, fields] = await db.execute(
`SELECT * FROM AuthVerification WHERE Email = ? AND Authenticated = 1;`, `SELECT * FROM AuthVerification WHERE Email = ? AND Authenticated = 1;`,
[data.email], [data.email]
); );
if (results.length === 0) { if (results.length === 0) {
@@ -105,7 +105,7 @@ exports.completeSignUp = async (req, res) => {
// Create the user // Create the user
const [createResult] = await db.execute( const [createResult] = await db.execute(
`INSERT INTO User (Name, Email, UCID, Password, Phone, Address) `INSERT INTO User (Name, Email, UCID, Password, Phone, Address)
VALUES ('${data.name}', '${data.email}', '${data.UCID}', '${data.password}', '${data.phone}', '${data.address}')`, VALUES ('${data.name}', '${data.email}', '${data.UCID}', '${data.password}', '${data.phone}', '${data.address}')`
); );
// Insert role using the user's ID // Insert role using the user's ID
@@ -113,12 +113,12 @@ exports.completeSignUp = async (req, res) => {
`INSERT INTO UserRole (UserID, Client, Admin) `INSERT INTO UserRole (UserID, Client, Admin)
VALUES (LAST_INSERT_ID(), ${data.client || true}, ${ VALUES (LAST_INSERT_ID(), ${data.client || true}, ${
data.admin || false data.admin || false
})`, })`
); );
// Delete verification record // Delete verification record
const [deleteResult] = await db.execute( const [deleteResult] = await db.execute(
`DELETE FROM AuthVerification WHERE Email = '${data.email}'`, `DELETE FROM AuthVerification WHERE Email = '${data.email}'`
); );
res.json({ res.json({
@@ -310,7 +310,7 @@ exports.deleteUser = async (req, res) => {
// Delete from UserRole first (assuming foreign key constraint) // Delete from UserRole first (assuming foreign key constraint)
const [result1] = await db.execute( const [result1] = await db.execute(
"DELETE FROM UserRole WHERE UserID = ?", "DELETE FROM UserRole WHERE UserID = ?",
[userId], [userId]
); );
// Then delete from User table // Then delete from User table
@@ -328,3 +328,38 @@ exports.deleteUser = async (req, res) => {
return res.status(500).json({ error: "Could not delete user!" }); return res.status(500).json({ error: "Could not delete user!" });
} }
}; };
exports.getUsersWithPagination = async (req, res) => {
const limit = +req.query.limit;
const page = +req.query.page;
const offset = (page - 1) * limit;
try {
const [users, fields] = await db.execute(
"SELECT * FROM User LIMIT ? OFFSET ?",
[limit.toString(), offset.toString()]
);
const [result] = await db.execute("SELECT COUNT(*) AS count FROM User");
const { count: total } = result[0];
res.json({ users, total });
} catch (error) {
console.error("Errors: ", error);
return res.status(500).json({ error: "\nCould not fetch users!" });
}
};
exports.isAdmin = async (req, res) => {
const { id } = req.params;
try {
const [result] = await db.execute(
"SELECT R.Admin FROM marketplace.userrole R WHERE R.UserID = ?",
[id]
);
const { Admin } = result[0];
res.json({ isAdmin: Admin });
} catch (error) {
res.json({ error: "Cannot verify admin status!" });
}
};

View File

@@ -9,6 +9,8 @@ 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 transactionRouter = require("./routes/transaction");
const { generateEmailTransporter } = require("./utils/mail"); const { generateEmailTransporter } = require("./utils/mail");
const { const {
@@ -42,7 +44,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/category", categoryRouter);
// 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

@@ -0,0 +1,16 @@
const express = require("express");
const {
getAllCategoriesWithPagination,
addCategory,
removeCategory,
getAllCategory,
} = require("../controllers/category");
const router = express.Router();
router.get("/getCategories", getAllCategoriesWithPagination);
router.post("/addCategory", addCategory);
router.delete("/:id", removeCategory);
router.get("/", getAllCategory);
module.exports = router;

View File

@@ -7,6 +7,10 @@ const {
getAllProducts, getAllProducts,
getProductById, getProductById,
addProduct, addProduct,
removeProduct,
getProductWithPagination,
myProduct,
updateProduct,
} = require("../controllers/product"); } = require("../controllers/product");
const router = express.Router(); const router = express.Router();
@@ -20,8 +24,18 @@ router.post("/addFavorite", addFavorite);
router.post("/getFavorites", getFavorites); router.post("/getFavorites", getFavorites);
router.post("/delFavorite", removeFavorite); router.post("/delFavorite", removeFavorite);
router.post("/delProduct", removeProduct);
router.post("/myProduct", myProduct);
router.post("/addProduct", addProduct); router.post("/addProduct", addProduct);
router.get("/getProduct", getAllProducts); router.get("/getProduct", getAllProducts);
//Remove product
router.delete("/:id", removeProduct);
//Get products with pagination
router.get("/getProductWithPagination", getProductWithPagination);
router.get("/:id", getProductById); // Simplified route router.get("/:id", getProductById); // Simplified route
router.put("/update/:productId", updateProduct);
module.exports = router; module.exports = router;

View File

@@ -0,0 +1,12 @@
const express = require("express");
const {
getTransactionWithPagination,
removeTransation,
} = require("../controllers/transaction");
const router = express.Router();
router.get("/getTransactions", getTransactionWithPagination);
router.delete("/:id", removeTransation);
module.exports = router;

View File

@@ -8,6 +8,8 @@ const {
updateUser, updateUser,
deleteUser, deleteUser,
doLogin, doLogin,
isAdmin,
getUsersWithPagination,
} = require("../controllers/user"); } = require("../controllers/user");
const router = express.Router(); const router = express.Router();
@@ -36,4 +38,10 @@ router.post("/update", updateUser);
//Delete A uses Data: //Delete A uses Data:
router.post("/delete", deleteUser); router.post("/delete", deleteUser);
//Check admin status
router.get("/isAdmin/:id", isAdmin);
//Fetch user with pagination
router.get("/getUserWithPagination", getUsersWithPagination);
module.exports = router; module.exports = router;

View File

@@ -4,6 +4,11 @@ const pool = mysql.createPool({
host: "localhost", host: "localhost",
user: "root", user: "root",
database: "Marketplace", database: "Marketplace",
password: "12345678",
}); });
// const pool = mysql.createPool(
// "singlestore://mann-619d0:<mann-619d0 Password>@svc-3482219c-a389-4079-b18b-d50662524e8a-shared-dml.aws-virginia-6.svc.singlestore.com:3333/db_mann_48ba9?ssl={}",
// );
module.exports = pool.promise(); module.exports = pool.promise();

View File

@@ -1,4 +1,5 @@
const { generateEmailTransporter } = require("./mail"); const { generateEmailTransporter } = require("./mail");
const db = require("../utils/database");
// Helper function to send email // Helper function to send email
async function sendVerificationEmail(email, verificationCode) { async function sendVerificationEmail(email, verificationCode) {
@@ -17,18 +18,17 @@ async function sendVerificationEmail(email, verificationCode) {
} }
// Clean up expired verification codes (run this periodically) // Clean up expired verification codes (run this periodically)
function cleanupExpiredCodes() { const cleanupExpiredCodes = () => {
db_con.query( db.execute(
"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) => { )
if (err) { .then((res) => {
console.log(`Cleaned up expired verification codes`);
})
.catch((err) => {
console.error("Error cleaning up expired codes:", err); console.error("Error cleaning up expired codes:", err);
} else { });
console.log(`Cleaned up ${result} expired verification codes`); };
}
}
);
}
const checkDatabaseConnection = async (db) => { const checkDatabaseConnection = async (db) => {
try { try {

View File

@@ -13,6 +13,7 @@
"lucide-react": "^0.477.0", "lucide-react": "^0.477.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-icons": "^5.5.0",
"react-router-dom": "^7.2.0" "react-router-dom": "^7.2.0"
}, },
"devDependencies": { "devDependencies": {
@@ -4369,6 +4370,15 @@
"react": "^19.0.0" "react": "^19.0.0"
} }
}, },
"node_modules/react-icons": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
"integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
"license": "MIT",
"peerDependencies": {
"react": "*"
}
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",

View File

@@ -15,6 +15,7 @@
"lucide-react": "^0.477.0", "lucide-react": "^0.477.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-icons": "^5.5.0",
"react-router-dom": "^7.2.0" "react-router-dom": "^7.2.0"
}, },
"devDependencies": { "devDependencies": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

View File

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

Before

Width:  |  Height:  |  Size: 774 KiB

After

Width:  |  Height:  |  Size: 774 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 301 KiB

After

Width:  |  Height:  |  Size: 301 KiB

View File

Before

Width:  |  Height:  |  Size: 421 KiB

After

Width:  |  Height:  |  Size: 421 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View File

Before

Width:  |  Height:  |  Size: 236 KiB

After

Width:  |  Height:  |  Size: 236 KiB

View File

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 197 KiB

View File

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

View File

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 128 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 201 KiB

After

Width:  |  Height:  |  Size: 201 KiB

View File

Before

Width:  |  Height:  |  Size: 577 KiB

After

Width:  |  Height:  |  Size: 577 KiB

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View File

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 923 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 KiB

View File

@@ -13,6 +13,13 @@ 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"; // Make sure to import the SearchPage
import Dashboard from "./pages/Dashboard";
import UserDashboard from "./pages/UserDashboard";
import ProductDashboard from "./pages/ProductDashboard";
import DashboardNav from "./components/DashboardNav";
import CategoryDashboard from "./pages/CategoryDashboard";
import { verifyIsAdmin } from "./api/admin";
import TransactionDashboard from "./pages/TransactionDashboard";
function App() { function App() {
// Authentication state - initialize from localStorage if available // Authentication state - initialize from localStorage if available
@@ -30,7 +37,10 @@ function App() {
const [error, setError] = useState(""); const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [userId, setUserId] = useState(null); // Product recommendation states
const [isGeneratingRecommendations, setIsGeneratingRecommendations] =
useState(false);
const [recommendations, setRecommendations] = useState([]);
// New verification states // New verification states
const [verificationStep, setVerificationStep] = useState("initial"); // 'initial', 'code-sent', 'verifying' const [verificationStep, setVerificationStep] = useState("initial"); // 'initial', 'code-sent', 'verifying'
@@ -53,8 +63,89 @@ function App() {
}, []); }, []);
useEffect(() => { useEffect(() => {
if (isAuthenticated && user) {
sendSessionDataToServer(); sendSessionDataToServer();
}, []); }
}, [isAuthenticated, user]);
// Generate product recommendations when user logs in
useEffect(() => {
if (isAuthenticated && user) {
generateProductRecommendations();
}
}, [isAuthenticated, user]);
// Generate product recommendations
const generateProductRecommendations = async () => {
try {
setIsGeneratingRecommendations(true);
// 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(() => {
const userInfo = sessionStorage.getItem("user")
? JSON.parse(sessionStorage.getItem("user"))
: "";
const id = userInfo?.ID;
verifyIsAdmin(id).then((data) => {
setIsAdmin(data.isAdmin);
});
}, [user]);
const handleShowAdminDashboard = () => {
setShowAdminDashboard(true);
};
const handleCloseAdminDashboard = () => {
setShowAdminDashboard(false);
};
// Send verification code // Send verification code
const sendVerificationCode = async (userData) => { const sendVerificationCode = async (userData) => {
@@ -182,6 +273,7 @@ function App() {
if (result.success) { if (result.success) {
// Create user object from API response // Create user object from API response
const newUser = { const newUser = {
ID: result.userID || result.ID,
name: result.name || userData.name, name: result.name || userData.name,
email: result.email || userData.email, email: result.email || userData.email,
UCID: result.UCID || userData.ucid, UCID: result.UCID || userData.ucid,
@@ -196,13 +288,17 @@ function App() {
sessionStorage.setItem("user", JSON.stringify(newUser)); sessionStorage.setItem("user", JSON.stringify(newUser));
// After successful signup, send session data to server // After successful signup, send session data to server
sendSessionDataToServer(); // Call it after signup sendSessionDataToServer();
// Reset verification steps // Reset verification steps
setVerificationStep("initial"); setVerificationStep("initial");
setTempUserData(null); setTempUserData(null);
console.log("Signup completed successfully"); console.log("Signup completed successfully");
// Generate recommendations for the new user
generateProductRecommendations();
return true; return true;
} else { } else {
setError(result.message || "Failed to complete signup"); setError(result.message || "Failed to complete signup");
@@ -301,9 +397,11 @@ function App() {
sessionStorage.setItem("isAuthenticated", "true"); sessionStorage.setItem("isAuthenticated", "true");
sessionStorage.setItem("user", JSON.stringify(userObj)); sessionStorage.setItem("user", JSON.stringify(userObj));
sessionStorage.getItem("user");
console.log("Login successful for:", userData.email); console.log("Login successful for:", userData.email);
// Start generating recommendations with a slight delay
// This will happen in the useEffect, but we set a loading state to show to the user
setIsGeneratingRecommendations(true);
} else { } else {
// Show error message for invalid credentials // Show error message for invalid credentials
setError("Invalid email or password"); setError("Invalid email or password");
@@ -337,11 +435,12 @@ function App() {
setUser(null); setUser(null);
setVerificationStep("initial"); setVerificationStep("initial");
setTempUserData(null); setTempUserData(null);
setRecommendations([]);
// Clear localStorage // Clear localStorage
//
sessionStorage.removeItem("user"); sessionStorage.removeItem("user");
sessionStorage.removeItem("isAuthenticated"); sessionStorage.removeItem("isAuthenticated");
sessionStorage.removeItem("userRecommendations");
console.log("User logged out"); console.log("User logged out");
}; };
@@ -369,8 +468,6 @@ function App() {
try { try {
// Retrieve data from sessionStorage // Retrieve data from sessionStorage
const user = JSON.parse(sessionStorage.getItem("user")); const user = JSON.parse(sessionStorage.getItem("user"));
// const isAuthenticated =
// sessionStorage.getItem("isAuthenticated") === "true";
if (!user || !isAuthenticated) { if (!user || !isAuthenticated) {
console.log("User is not authenticated"); console.log("User is not authenticated");
@@ -384,8 +481,6 @@ function App() {
isAuthenticated, isAuthenticated,
}; };
console.log("Sending user data to the server:", requestData);
// Send data to Python server (replace with your actual server URL) // Send data to Python server (replace with your actual server URL)
const response = await fetch("http://0.0.0.0:5000/api/user/session", { const response = await fetch("http://0.0.0.0:5000/api/user/session", {
method: "POST", method: "POST",
@@ -407,6 +502,13 @@ function App() {
} }
}; };
// Loading overlay component
const LoadingOverlay = () => (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="animate-spin rounded-full h-12 w-12 border-4 border-green-500 border-t-transparent"></div>
</div>
);
// Login component // Login component
const LoginComponent = () => ( const LoginComponent = () => (
<div className="flex h-screen bg-white"> <div className="flex h-screen bg-white">
@@ -672,12 +774,43 @@ function App() {
return children; return children;
}; };
// If user is admin, show admin naviagtion
if (showAdminDashboard) {
return (
<Router>
<div className="flex">
<DashboardNav handleCloseAdminDashboard={handleCloseAdminDashboard} />
<Routes>
{/* Admin routes */}
<Route path="/admin" element={<Dashboard />} />
<Route path="/admin/user" element={<UserDashboard />} />
<Route path="/admin/product" element={<ProductDashboard />} />
<Route path="/admin/category" element={<CategoryDashboard />} />
<Route
path="/admin/transaction"
element={<TransactionDashboard />}
/>
<Route path="*" element={<Dashboard />} />
</Routes>
</div>
</Router>
);
}
return ( return (
<Router> <Router>
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50">
{/* Show loading overlay when generating recommendations */}
{isGeneratingRecommendations && <LoadingOverlay />}
{/* Only show navbar when authenticated */} {/* Only show navbar when authenticated */}
{isAuthenticated && ( {isAuthenticated && (
<Navbar onLogout={handleLogout} userName={user?.name} /> <Navbar
isAdmin={isAdmin}
onLogout={handleLogout}
userName={user?.name}
handleShowAdminDashboard={handleShowAdminDashboard}
/>
)} )}
<Routes> <Routes>
{/* Public routes */} {/* Public routes */}
@@ -691,7 +824,7 @@ function App() {
element={ element={
<ProtectedRoute> <ProtectedRoute>
<div className="container mx-auto px-4 py-6"> <div className="container mx-auto px-4 py-6">
<Home /> <Home recommendations={recommendations} />
</div> </div>
</ProtectedRoute> </ProtectedRoute>
} }

120
frontend/src/api/admin.js Normal file
View File

@@ -0,0 +1,120 @@
import client from "./client";
export const getUsers = async (page, limit = 10) => {
try {
const { data } = await client.get(
`/user/getUserWithPagination?page=${page}&limit=${limit}`
);
return { users: data.users, total: data.total };
} catch (error) {
const { response } = error;
if (response?.data) return response.data;
return { error: error.message || error };
}
};
export const getProducts = async (page, limit = 10) => {
try {
const { data } = await client.get(
`/product/getProductWithPagination?limit=${limit}&page=${page}`
);
return { products: data.products, total: data.totalProd };
} catch (error) {
const { response } = error;
if (response?.data) return response.data;
return { error: error.message || error };
}
};
export const getCategories = async (page, limit = 10) => {
try {
const { data } = await client.get(
`/category/getCategories?page=${page}&limit=${limit}`
);
return { data: data.data, total: data.total };
} catch (error) {
const { response } = error;
if (response?.data) return response.data;
return { error: error.message || error };
}
};
export const 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 };
}
};
export const removeUser = async (id) => {
try {
const { data } = await client.post(`/user/delete`, { userId: id });
return { message: data.message };
} catch (error) {
const { response } = error;
if (response?.data) return response.data;
return { error: error.message || error };
}
};
export const removeProduct = async (id) => {
try {
const { data } = await client.delete(`/product/${id}`);
return { message: data.message };
} catch (error) {
const { response } = error;
if (response?.data) return response.data;
return { error: error.message || error };
}
};
export const verifyIsAdmin = async (id) => {
try {
const { data } = await client.get(`/user/isAdmin/${id}`);
return { isAdmin: data.isAdmin };
} catch (error) {
const { response } = error;
if (response?.data) return response.data;
return { error: error.message || error };
}
};
export const removeTransaction = async (id) => {
try {
const { data } = await client.delete(`/transaction/${id}`);
return { message: data.message };
} catch (error) {
const { response } = error;
if (response?.data) return response.data;
return { error: error.message || error };
}
};

View File

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

View File

@@ -0,0 +1,65 @@
import { useState } from "react";
import { MdAddBox } from "react-icons/md";
import { addCategory } from "../api/admin";
export default function CategoryForm({ visible, onAddCategory }) {
const [category, setCategory] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
if (!category.trim()) {
document.getElementById("noti").innerHTML = "Category name is missing!";
document
.getElementById("noti")
.classList.add("bg-red-200", "text-red-500");
document.getElementById("noti").classList.remove("opacity-0");
return;
}
addCategory(category)
.then((message) => {
document
.getElementById("noti")
.classList.remove("opacity-0", "bg-red-200", "text-red-500");
document
.getElementById("noti")
.classList.add("bg-green-200", "text-green-800");
document.getElementById("noti").innerHTML = `${message.message}`;
setCategory("");
onAddCategory();
})
.catch((err) => {
console.log(err);
});
};
const handleChange = ({ target }) => {
setCategory(target.value);
if (target.value.trim())
document.getElementById("noti").classList.add("opacity-0");
};
if (!visible) return;
return (
<form onSubmit={handleSubmit} action="" className="flex p-2 items-center">
<label htmlFor="category" className="text-green-700">
Category:
</label>
<input
type="text"
className="border border-green-700 ml-2 rounded-sm focus:bg-green-100 text-green-900"
name="category"
id="category"
onChange={handleChange}
value={category}
/>
<button type="submit" className="text-2xl pl-1 text-green-700">
<MdAddBox className="text-3xl" />
</button>
<p
id="noti"
className="text-red-500 bg-red-200 px-2 rounded-sm opacity-0 mx-2"
></p>
</form>
);
}

View File

@@ -0,0 +1,98 @@
import { Link, NavLink } from "react-router-dom";
import { FaUserTag } from "react-icons/fa";
import { FaBoxArchive } from "react-icons/fa6";
import { MdOutlineCategory } from "react-icons/md";
import { FaArrowLeft } from "react-icons/fa";
import { FaMoneyBillTransfer } from "react-icons/fa6";
export default function DashboardNav({ handleCloseAdminDashboard }) {
const handleClick = () => {
handleCloseAdminDashboard();
};
return (
<div>
<div className="w-3xs h-screen bg-green-700 border border-green-700">
<ul>
<li>
<div className="flex-shrink-0 p-6 bg-green-200">
<Link to="/admin" className="flex items-center">
<img
src="/icon/icon-512.png"
alt="Campus Plug"
className="h-8 px-2 "
/>
<span className="hidden md:block text-green-700 font-bold text-2xl">
Campus Plug
</span>
</Link>
</div>
</li>
<li className="w-fit pl-10">
<NavLink
to="/admin/user"
className={({ isActive }) =>
(isActive
? " text-green-400"
: "text-white transition-all hover:text-green-200") +
" flex items-center px-5 text-lg pt-5 "
}
>
<FaUserTag />
<span className="pl-3">Users</span>
</NavLink>
</li>
<li className="w-fit pl-10">
<NavLink
to="/admin/product"
className={({ isActive }) =>
(isActive
? "text-green-400"
: "text-white transition-all hover:text-green-200") +
" flex items-center px-5 text-lg pt-5"
}
>
<FaBoxArchive />
<span className="pl-3">Products</span>
</NavLink>
</li>
<li className="w-fit pl-10">
<NavLink
to="/admin/category"
className={({ isActive }) =>
(isActive
? "text-green-400"
: "text-white transition-all hover:text-green-200") +
" flex items-center px-5 text-lg pt-5"
}
>
<MdOutlineCategory />
<span className="pl-3">Categories</span>
</NavLink>
</li>
<li className="w-fit pl-10">
<NavLink
to="/admin/transaction"
className={({ isActive }) =>
(isActive
? "text-green-400"
: "text-white transition-all hover:text-green-200") +
" flex items-center px-5 text-lg pt-5"
}
>
<FaMoneyBillTransfer />
<span className="pl-3">Transaction</span>
</NavLink>
</li>
</ul>
<div
onClick={handleClick}
className=" text-center my-8 underline text-white underline-offset-4 flex justify-center items-center hover:cursor-pointer h-fit w-fit mx-auto hover:text-green-200 transition"
>
<FaArrowLeft className="text-sm mx-2 mt-1" />
<span>Go back to user page</span>
</div>
</div>
</div>
);
}

View File

@@ -3,7 +3,7 @@ import { Link, useNavigate } from "react-router-dom";
import UserDropdown from "./UserDropdown"; import UserDropdown from "./UserDropdown";
import { Search, Heart } from "lucide-react"; import { Search, Heart } from "lucide-react";
const Navbar = ({ onLogout, userName }) => { const Navbar = ({ onLogout, userName, isAdmin, handleShowAdminDashboard }) => {
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const navigate = useNavigate(); const navigate = useNavigate();
@@ -76,7 +76,12 @@ const Navbar = ({ onLogout, userName }) => {
</Link> </Link>
{/* User Profile */} {/* User Profile */}
<UserDropdown onLogout={onLogout} userName={userName} /> <UserDropdown
isAdmin={isAdmin}
onLogout={onLogout}
userName={userName}
handleShowAdminDashboard={handleShowAdminDashboard}
/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,99 @@
import { useState } from "react";
import { NavLink } from "react-router-dom";
export default function Pagination({ pageNum, onChange }) {
const [currentPage, setCurrentPage] = useState(1);
const pages = [];
for (let i = 1; i <= pageNum; i++) {
pages.push(i);
}
const handleClick = (page) => {
setCurrentPage(page);
onChange(page);
};
const handleTogglePage = (type) => {
let current = currentPage;
if (type == "next")
current = current + 1 <= pageNum ? current + 1 : current;
else current = current - 1 >= 1 ? current - 1 : current;
setCurrentPage(current);
onChange(current);
};
return (
<>
<nav aria-label="Page navigation" className="flex justify-end">
<ul className="flex items-center -space-x-px h-8 text-sm mt-4 pr-0 font-bold">
<li>
<NavLink
onClick={() => {
handleTogglePage("previous");
}}
className=" flex items-center justify-center px-3 h-8 ms-0 leading-tight border-e-0 border-gray-300 rounded-s-lg hover:bg-gray-100 text-white bg-green-700 border border-gray-300 hover:bg-green-600 hover:text-white"
>
<span className="sr-only">Previous</span>
<svg
className="w-2.5 h-2.5 rtl:rotate-180"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 6 10"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M5 1 1 5l4 4"
/>
</svg>
</NavLink>
</li>
{pages.map((page) => (
<li key={page}>
<NavLink
className={`${
currentPage == page ? "bg-green-600" : "bg-green-700"
} +
" flex items-center justify-center px-3 h-8 leading-tight text-white border border-gray-300 hover:bg-green-600 hover:text-white"`}
onClick={() => {
handleClick(page);
}}
>
{page}
</NavLink>
</li>
))}
<li>
<NavLink
onClick={() => {
handleTogglePage("next");
}}
className="flex items-center justify-center px-3 h-8 leading-tight border border-gray-300 rounded-e-lg text-white bg-green-700 border border-gray-300 hover:bg-green-600 hover:text-white"
>
<span className="sr-only">Next</span>
<svg
className="w-2.5 h-2.5 rtl:rotate-180"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 6 10"
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="m1 9 4-4-4-4"
/>
</svg>
</NavLink>
</li>
</ul>
</nav>
</>
);
}

View File

@@ -1,410 +0,0 @@
import React, { useState } from "react";
import { X, ChevronLeft, Plus, Trash2, Check } from "lucide-react";
const ProductForm = ({
editingProduct,
setEditingProduct,
onSave,
onCancel,
}) => {
const [selectedCategory, setSelectedCategory] = useState("");
const storedUser = JSON.parse(sessionStorage.getItem("user"));
const categories = [
"Electronics",
"Clothing",
"Home & Garden",
"Toys & Games",
"Books",
"Sports & Outdoors",
"Automotive",
"Beauty & Personal Care",
"Health & Wellness",
"Jewelry",
"Art & Collectibles",
"Food & Beverages",
"Office Supplies",
"Pet Supplies",
"Music & Instruments",
"Other",
];
// Map category names to their respective IDs
const categoryMapping = {
Electronics: 1,
Clothing: 2,
"Home & Garden": 3,
"Toys & Games": 4,
Books: 5,
"Sports & Outdoors": 6,
Automotive: 7,
"Beauty & Personal Care": 8,
"Health & Wellness": 9,
Jewelry: 10,
"Art & Collectibles": 11,
"Food & Beverages": 12,
"Office Supplies": 13,
"Pet Supplies": 14,
"Music & Instruments": 15,
Other: 16,
};
const handleSave = async () => {
// Check if the user has selected at least one category
if (!(editingProduct.categories || []).length) {
alert("Please select at least one category");
return;
}
try {
// First, upload images if there are any
const imagePaths = [];
// If we have files to upload, we'd handle the image upload here
// This is a placeholder for where you'd implement image uploads
// For now, we'll simulate the API expecting paths:
if (editingProduct.images && editingProduct.images.length > 0) {
// Simulating image paths for demo purposes
// In a real implementation, you would upload these files first
// and then use the returned paths
editingProduct.images.forEach((file, index) => {
const simulatedPath = `/public/uploads/${file.name}`;
imagePaths.push(simulatedPath);
});
}
// Get the category ID from the first selected category
const categoryName = (editingProduct.categories || [])[0];
const categoryID = categoryMapping[categoryName] || 3; // Default to 3 if not found
// Prepare payload according to API expectations
const payload = {
name: editingProduct.name || "",
price: parseFloat(editingProduct.price) || 0,
qty: 1, // Hardcoded as per your requirement
userID: storedUser.ID,
description: editingProduct.description || "",
category: categoryID,
images: imagePaths,
};
console.log("Sending payload:", payload);
const response = await fetch(
"http://localhost:3030/api/product/addProduct",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
},
);
if (!response.ok) {
const errorData = await response.text();
throw new Error(`Failed to add product: ${errorData}`);
}
const data = await response.json();
console.log("Product added:", data);
if (onSave) onSave(data);
} catch (error) {
console.error("Error saving product:", error);
alert(`Error saving product: ${error.message}`);
}
};
const addCategory = () => {
if (
selectedCategory &&
!(editingProduct.categories || []).includes(selectedCategory)
) {
setEditingProduct((prev) => ({
...prev,
categories: [...(prev.categories || []), selectedCategory],
}));
setSelectedCategory("");
}
};
const removeCategory = (categoryToRemove) => {
setEditingProduct((prev) => ({
...prev,
categories: (prev.categories || []).filter(
(cat) => cat !== categoryToRemove,
),
}));
};
const toggleSoldStatus = () => {
setEditingProduct((prev) => ({
...prev,
isSold: !prev.isSold,
}));
};
return (
<div className="bg-white border border-gray-200 shadow-md p-6">
{/* Back Button */}
<button
onClick={onCancel}
className="mb-4 text-emerald-600 hover:text-emerald-800 flex items-center gap-1"
>
<ChevronLeft size={16} />
<span>Back to Listings</span>
</button>
<h3 className="text-xl font-bold text-gray-800 mb-6 border-b border-gray-200 pb-3">
{editingProduct?.id ? "Edit Your Product" : "List a New Product"}
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Product Name */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Product Name
</label>
<input
type="text"
value={editingProduct.name || ""}
onChange={(e) =>
setEditingProduct({ ...editingProduct, name: e.target.value })
}
className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-500 focus:outline-none"
/>
</div>
{/* Price */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Price ($)
</label>
<input
type="number"
value={editingProduct.price || ""}
onChange={(e) =>
setEditingProduct({
...editingProduct,
price: e.target.value,
})
}
className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-500 focus:outline-none"
/>
</div>
{/* Sold Status */}
<div className="md:col-span-2">
<div className="flex items-center mt-2">
<input
type="checkbox"
id="soldStatus"
checked={editingProduct.isSold || false}
onChange={toggleSoldStatus}
className="w-4 h-4 text-emerald-600 rounded focus:ring-emerald-500"
/>
<label htmlFor="soldStatus" className="ml-2 text-sm text-gray-700">
Mark as {editingProduct.isSold ? "Available" : "Sold"}
</label>
{editingProduct.isSold && (
<span className="ml-2 inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">
Sold
</span>
)}
</div>
</div>
{/* Categories */}
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Categories
</label>
<div className="flex gap-2">
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="flex-1 px-3 py-2 border border-gray-300 focus:border-emerald-500 focus:outline-none"
>
<option value="" disabled>
Select a category
</option>
{categories
.filter(
(cat) => !(editingProduct.categories || []).includes(cat),
)
.map((category) => (
<option key={category} 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) => (
<span
key={category}
className="inline-flex items-center px-2 py-1 bg-emerald-100 text-emerald-800"
>
{category}
<button
type="button"
onClick={() => removeCategory(category)}
className="ml-1 text-emerald-600 hover:text-emerald-800"
>
<X size={14} />
</button>
</span>
))}
</div>
) : (
<p className="text-xs text-gray-500 mt-1">
Please select at least one category
</p>
)}
</div>
{/* Description */}
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Description
</label>
<textarea
value={editingProduct.description || ""}
onChange={(e) =>
setEditingProduct({
...editingProduct,
description: e.target.value,
})
}
rows="4"
className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-500 focus:outline-none"
placeholder="Describe your product in detail..."
></textarea>
</div>
{/* Image Upload */}
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Product Images <span className="text-gray-500">(Max 5)</span>
</label>
<input
type="file"
accept="image/*"
multiple
onChange={(e) => {
const files = Array.from(e.target.files).slice(0, 5);
setEditingProduct((prev) => ({
...prev,
images: [...(prev.images || []), ...files].slice(0, 5),
}));
}}
className="hidden"
id="image-upload"
/>
<label
htmlFor="image-upload"
className="block w-full p-3 border border-gray-300 bg-gray-50 text-center cursor-pointer hover:bg-gray-100"
>
<span className="text-emerald-600 font-medium">
Click to upload images
</span>
</label>
{/* Image previews */}
{(editingProduct.images || []).length > 0 && (
<div className="mt-3">
<div className="flex justify-between items-center mb-2">
<p className="text-sm text-gray-600">
{editingProduct.images.length}{" "}
{editingProduct.images.length === 1 ? "image" : "images"}{" "}
selected
</p>
<button
onClick={() =>
setEditingProduct((prev) => ({ ...prev, images: [] }))
}
className="text-sm text-red-600 hover:text-red-800 flex items-center gap-1"
>
<Trash2 size={14} />
<span>Clear all</span>
</button>
</div>
<div className="flex flex-wrap gap-2">
{editingProduct.images.map((img, idx) => (
<div
key={idx}
className="relative w-20 h-20 border border-gray-200 overflow-hidden"
>
<img
src={URL.createObjectURL(img)}
alt={`Product ${idx + 1}`}
className="w-full h-full object-cover"
/>
<button
onClick={() => {
const updated = [...editingProduct.images];
updated.splice(idx, 1);
setEditingProduct((prev) => ({
...prev,
images: updated,
}));
}}
className="absolute top-0 right-0 bg-white bg-opacity-80 w-6 h-6 flex items-center justify-center text-gray-700 hover:text-red-600"
>
<X size={14} />
</button>
</div>
))}
</div>
</div>
)}
</div>
</div>
{/* Actions */}
<div className="mt-6 flex justify-between border-t border-gray-200 pt-4">
<button
onClick={toggleSoldStatus}
className={`flex items-center gap-1 px-4 py-2 rounded-md transition-colors ${
editingProduct.isSold
? "bg-green-100 text-green-700 hover:bg-green-200"
: "bg-red-100 text-red-700 hover:bg-red-200"
}`}
>
<Check size={16} />
<span>Mark as {editingProduct.isSold ? "Available" : "Sold"}</span>
</button>
<div className="flex gap-3">
<button
onClick={onCancel}
className="bg-gray-100 text-gray-700 px-4 py-2 hover:bg-gray-200 rounded-md"
>
Cancel
</button>
<button
onClick={handleSave}
className="bg-emerald-600 text-white px-6 py-2 hover:bg-emerald-700 rounded-md"
>
{editingProduct.id ? "Update Product" : "Add Product"}
</button>
</div>
</div>
</div>
);
};
export default ProductForm;

View File

@@ -1,8 +1,14 @@
import { useState, useRef, useEffect } from "react"; import { useState, useRef, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { User, Settings, ShoppingBag, DollarSign, LogOut } from "lucide-react"; import { User, Settings, ShoppingBag, DollarSign, LogOut } from "lucide-react";
import { RiAdminLine } from "react-icons/ri";
const UserDropdown = ({ onLogout, userName }) => { const UserDropdown = ({
onLogout,
userName,
isAdmin,
handleShowAdminDashboard,
}) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null); const dropdownRef = useRef(null);
const navigate = useNavigate(); const navigate = useNavigate();
@@ -89,6 +95,20 @@ const UserDropdown = ({ onLogout, userName }) => {
Settings Settings
</Link> </Link>
{isAdmin ? (
<Link
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
onClick={() => {
handleShowAdminDashboard();
}}
>
<RiAdminLine className="h-4 w-4 mr-2 text-gray-500" />
Admin
</Link>
) : (
<></>
)}
<button <button
className="flex w-full items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" className="flex w-full items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
onClick={handleLogout} onClick={handleLogout}

View File

@@ -0,0 +1,105 @@
import { useEffect, useState } from "react";
import { getCategories, removeCategory } from "../api/admin";
import { MdDelete } from "react-icons/md";
import Pagination from "../components/Pagination";
import { IoAddCircleSharp } from "react-icons/io5";
import CategoryForm from "../components/CategoryForm";
export default function CategoryDashboard() {
const [categories, setCategories] = useState([]);
const [total, setTotal] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
let pageLimit = 10;
const [visible, setVisible] = useState(false);
const onChangePage = (page, limit = 10) => {
setCurrentPage(page);
fetchCategory(page, limit);
};
const fetchCategory = (page = 1, limit = 10) => {
getCategories(page, limit).then(({ data, total }) => {
setCategories(data);
setTotal(total);
});
};
const notiChange = () => {
fetchCategory(currentPage);
};
const handleToggleForm = () => {
setVisible((curr) => !curr);
};
const handleRemove = (id) => {
removeCategory(id)
.then(() => {
fetchCategory(currentPage);
})
.catch((err) => {
console.log(err);
});
};
//Get user when initialize the component
useEffect(fetchCategory, []);
return (
<div className="pt-10 p-20 w-full">
<h1 className="text-4xl pb-3 font-bold text-green-800 underline">
CATEGORIES
</h1>
<button
onClick={handleToggleForm}
className="flex justify-end items-center bg-blue-500 rounded-md px-2 text-white text-sm p-1 mb-1 ml-auto hover:cursor-pointer hover:bg-blue-600 transtion"
>
<span className="pr-1 text-xs">Add</span>
<IoAddCircleSharp />
</button>
<CategoryForm onAddCategory={notiChange} visible={visible} />
{categories.length > 0 ? (
<>
<table className="table-fixed w-full text-center border border-green-600">
<thead className="bg-green-600 h-10">
<tr>
<th>CategoryID</th>
<th>Name</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{categories.map((category) => (
<tr
key={category.UserID}
className="border border-green-600 h-10"
>
<td>{category.CategoryID}</td>
<td>{category.Name}</td>
<td className="flex justify-center pt-2">
<MdDelete
onClick={() => {
handleRemove(category.CategoryID);
}}
className="hover:text-red-600 cursor-pointer transition-all text-xl"
/>
</td>
</tr>
))}
</tbody>
</table>
<Pagination
pageNum={Math.ceil(total / pageLimit)}
onChange={onChangePage}
/>
</>
) : (
<p className="text-red-700 text-xl bg-red-200 px-3 rounded-md py-1 w-fit">
No category exists!
</p>
)}
</div>
);
}

View File

@@ -0,0 +1,7 @@
export default function Dashboard() {
return (
<div className="text-3xl font-bold p-3 text-green-800">
Welcome to admin dashboard
</div>
);
}

View File

@@ -1,12 +1,6 @@
import { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { import { useState, useEffect, useRef } from "react";
Tag, import { Tag, ChevronLeft, ChevronRight, Bookmark, Loader } from "lucide-react";
ChevronLeft,
ChevronRight,
Bookmark,
BookmarkCheck,
} from "lucide-react";
import FloatingAlert from "../components/FloatingAlert"; // adjust path if needed import FloatingAlert from "../components/FloatingAlert"; // adjust path if needed
@@ -14,14 +8,22 @@ const Home = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [listings, setListings] = useState([]); const [listings, setListings] = useState([]);
const [recommended, setRecommended] = useState([]); const [recommended, setRecommended] = useState([]);
const [history, sethistory] = useState([]); const [history, setHistory] = useState([]);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [showAlert, setShowAlert] = useState(false); const [showAlert, setShowAlert] = useState(false);
const [isLoading, setIsLoading] = useState({
recommendations: true,
listings: true,
history: true,
});
const recommendationsFetched = useRef(false);
const historyFetched = useRef(false);
//After user data storing the session. //After user data storing the session.
const storedUser = JSON.parse(sessionStorage.getItem("user")); const storedUser = JSON.parse(sessionStorage.getItem("user"));
const toggleFavorite = async (id) => { const toggleFavorite = async (id) => {
try {
const response = await fetch( const response = await fetch(
"http://localhost:3030/api/product/addFavorite", "http://localhost:3030/api/product/addFavorite",
{ {
@@ -38,14 +40,18 @@ const Home = () => {
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
setShowAlert(true); setShowAlert(true);
// Close alert after 3 seconds
setTimeout(() => setShowAlert(false), 3000);
}
console.log(`Add Product -> Favorites: ${id}`);
} catch (error) {
console.error("Error adding favorite:", error);
} }
console.log(`Add Product -> History: ${id}`);
}; };
const addHistory = async (id) => { const addHistory = async (id) => {
const response = await fetch( try {
"http://localhost:3030/api/history/addHistory", await fetch("http://localhost:3030/api/history/addHistory", {
{
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -54,23 +60,23 @@ const Home = () => {
userID: storedUser.ID, userID: storedUser.ID,
productID: id, productID: id,
}), }),
}, });
); } catch (error) {
console.error("Error adding to history:", error);
}
}; };
function reloadPage() { // Fetch recommended products
var doctTimestamp = new Date(performance.timing.domLoading).getTime();
var now = Date.now();
var tenSec = 10 * 1000;
if (now > doctTimestamp + tenSec) {
location.reload();
}
}
reloadPage();
useEffect(() => { useEffect(() => {
const fetchrecomProducts = async () => { const fetchRecommendedProducts = async () => {
// Skip if already fetched or no user data
if (recommendationsFetched.current || !storedUser || !storedUser.ID)
return;
setIsLoading((prev) => ({ ...prev, recommendations: true }));
try { try {
recommendationsFetched.current = true; // Mark as fetched before the API call
const response = await fetch( const response = await fetch(
"http://localhost:3030/api/engine/recommended", "http://localhost:3030/api/engine/recommended",
{ {
@@ -83,36 +89,42 @@ const Home = () => {
}), }),
}, },
); );
if (!response.ok) throw new Error("Failed to fetch products"); if (!response.ok) throw new Error("Failed to fetch recommendations");
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
setRecommended( setRecommended(
data.data.map((product) => ({ data.data.map((product) => ({
id: product.ProductID, id: product.ProductID,
title: product.ProductName, // Use the alias from SQL title: product.ProductName,
price: product.Price, price: product.Price,
category: product.Category, // Ensure this gets the category name category: product.Category,
image: product.ProductImage, // Use the alias for image URL image: product.ProductImage,
seller: product.SellerName, // Fetch seller name properly seller: product.SellerName,
datePosted: product.DateUploaded, // Use the actual date datePosted: product.DateUploaded,
isFavorite: false, // Default state isFavorite: false,
})), })),
); );
reloadPage();
} else { } else {
throw new Error(data.message || "Error fetching products"); throw new Error(data.message || "Error fetching recommendations");
} }
} catch (error) { } catch (error) {
console.error("Error fetching products:", error); console.error("Error fetching recommendations:", error);
setError(error.message); setError(error.message);
// Reset the flag if there's an error so it can try again
recommendationsFetched.current = false;
} finally {
setIsLoading((prev) => ({ ...prev, recommendations: false }));
} }
}; };
fetchrecomProducts();
}, []);
fetchRecommendedProducts();
}, [storedUser]); // Keep dependency
// Fetch all products
useEffect(() => { useEffect(() => {
const fetchProducts = async () => { const fetchProducts = async () => {
setIsLoading((prev) => ({ ...prev, listings: true }));
try { try {
const response = await fetch( const response = await fetch(
"http://localhost:3030/api/product/getProduct", "http://localhost:3030/api/product/getProduct",
@@ -120,18 +132,17 @@ const Home = () => {
if (!response.ok) throw new Error("Failed to fetch products"); if (!response.ok) throw new Error("Failed to fetch products");
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
setListings( setListings(
data.data.map((product) => ({ data.data.map((product) => ({
id: product.ProductID, id: product.ProductID,
title: product.ProductName, // Use the alias from SQL title: product.ProductName,
price: product.Price, price: product.Price,
category: product.Category, // Ensure this gets the category name category: product.Category,
image: product.ProductImage, // Use the alias for image URL image: product.ProductImage,
seller: product.SellerName, // Fetch seller name properly seller: product.SellerName,
datePosted: product.DateUploaded, // Use the actual date datePosted: product.DateUploaded,
isFavorite: false, // Default state isFavorite: false,
})), })),
); );
} else { } else {
@@ -140,15 +151,24 @@ const Home = () => {
} catch (error) { } catch (error) {
console.error("Error fetching products:", error); console.error("Error fetching products:", error);
setError(error.message); setError(error.message);
} finally {
setIsLoading((prev) => ({ ...prev, listings: false }));
} }
}; };
fetchProducts(); fetchProducts();
}, []); }, []);
// Fetch user history
useEffect(() => { useEffect(() => {
const fetchrecomProducts = async () => { const fetchUserHistory = async () => {
// Get the user's data from localStorage // Skip if already fetched or no user data
if (historyFetched.current || !storedUser || !storedUser.ID) return;
setIsLoading((prev) => ({ ...prev, history: true }));
try { try {
historyFetched.current = true; // Mark as fetched before the API call
const response = await fetch( const response = await fetch(
"http://localhost:3030/api/history/getHistory", "http://localhost:3030/api/history/getHistory",
{ {
@@ -161,52 +181,168 @@ const Home = () => {
}), }),
}, },
); );
if (!response.ok) throw new Error("Failed to fetch products"); if (!response.ok) throw new Error("Failed to fetch history");
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
sethistory( setHistory(
data.data.map((product) => ({ data.data.map((product) => ({
id: product.ProductID, id: product.ProductID,
title: product.ProductName, // Use the alias from SQL title: product.ProductName,
price: product.Price, price: product.Price,
category: product.Category, // Ensure this gets the category name category: product.Category,
image: product.ProductImage, // Use the alias for image URL image: product.ProductImage,
seller: product.SellerName, // Fetch seller name properly seller: product.SellerName,
datePosted: product.DateUploaded, // Use the actual date datePosted: product.DateUploaded,
})), })),
); );
} else { } else {
throw new Error(data.message || "Error fetching products"); throw new Error(data.message || "Error fetching history");
} }
} catch (error) { } catch (error) {
console.error("Error fetching products:", error); console.error("Error fetching history:", error);
setError(error.message); setError(error.message);
// Reset the flag if there's an error so it can try again
historyFetched.current = false;
} finally {
setIsLoading((prev) => ({ ...prev, history: false }));
} }
}; };
fetchrecomProducts();
}, []); fetchUserHistory();
}, [storedUser]); // Keep dependency
const handleSelling = () => { const handleSelling = () => {
navigate("/selling"); navigate("/selling");
}; };
// Loading indicator component
const LoadingSection = () => (
<div className="flex justify-center items-center h-48">
<Loader className="animate-spin text-emerald-600 h-8 w-8" />
</div>
);
// Product card component to reduce duplication
const ProductCard = ({ product, addToHistory = false }) => (
<Link
key={product.id}
to={`/product/${product.id}`}
onClick={addToHistory ? () => addHistory(product.id) : undefined}
className="bg-white border border-gray-200 hover:shadow-md transition-shadow w-70 flex-shrink-0 relative"
>
<div className="relative">
<img
src={product.image}
alt={product.title}
className="w-full h-48 object-cover"
/>
<button
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
toggleFavorite(product.id);
}}
className="absolute top-0 right-0 p-2 rounded-bl-md bg-emerald-600 hover:bg-emerald-500 transition shadow-sm"
>
<Bookmark className="text-white w-5 h-5" />
</button>
</div>
<div className="p-4">
<h3 className="text-lg font-medium text-gray-800 leading-tight">
{product.title}
</h3>
<span className="font-semibold text-emerald-600 block mt-1">
${product.price}
</span>
<div className="flex items-center text-sm text-gray-500 mt-2">
<Tag className="h-4 w-4 mr-1" />
<span>{product.category}</span>
</div>
<div className="flex justify-between items-center pt-2 border-t border-gray-100 mt-3">
<span className="text-xs text-gray-500">{product.datePosted}</span>
<span className="text-sm font-medium text-gray-700">
{product.seller}
</span>
</div>
</div>
</Link>
);
// Scrollable product list component to reduce duplication
const ScrollableProductList = ({
containerId,
products,
children,
isLoading,
addToHistory = false,
}) => (
<div className="relative py-4">
{children}
<div className="relative">
<button
onClick={() =>
document
.getElementById(containerId)
.scrollBy({ left: -400, behavior: "smooth" })
}
className="absolute left-0 top-1/2 transform -translate-y-1/2 bg-gray-800 bg-opacity-70 text-white p-4 rounded-full z-20 hidden md:flex items-center justify-center w-12 h-12"
>
<ChevronLeft size={24} />
</button>
<div
id={containerId}
className="overflow-x-auto whitespace-nowrap flex space-x-6 scroll-smooth scrollbar-hide px-10 pl-0 rounded min-h-[250px]"
>
{isLoading ? (
<LoadingSection />
) : products.length > 0 ? (
products.map((product) => (
<ProductCard
key={product.id}
product={product}
addToHistory={addToHistory}
/>
))
) : (
<div className="flex justify-center items-center w-full h-48 text-gray-500">
No products available
</div>
)}
</div>
<button
onClick={() =>
document
.getElementById(containerId)
.scrollBy({ left: 400, behavior: "smooth" })
}
className="absolute right-0 top-1/2 transform -translate-y-1/2 bg-gray-800 bg-opacity-70 text-white p-4 rounded-full z-20 hidden md:flex items-center justify-center w-12 h-12"
>
<ChevronRight size={24} />
</button>
</div>
</div>
);
return ( return (
<div className="flex flex-col min-h-screen"> <div className="flex flex-col min-h-screen">
<div className="flex-grow"> <div className="flex-grow">
{/* Hero Section with School Background */} {/* Hero Section with School Background */}
<div className="relative py-12 px-4 mb-8 shadow-sm"> <div className="relative py-12 px-4 mb-8 shadow-sm">
{/* Background Image - Positioned at bottom */}
<div className="absolute inset-0 z-0 overflow-hidden bg-black bg-opacity-100"> <div className="absolute inset-0 z-0 overflow-hidden bg-black bg-opacity-100">
<img <img
src="../public/Ucalgary.png" src="../public/Ucalgary.png"
alt="University of Calgary" alt="University of Calgary"
className="w-full h-full object-cover object-bottom opacity-50" className="w-full h-full object-cover object-bottom opacity-45"
/> />
{/* Dark overlay for better text readability */}
</div> </div>
{/* Content */}
<div className="max-w-2xl mx-auto text-center relative z-1"> <div className="max-w-2xl mx-auto text-center relative z-1">
<h1 className="text-3xl font-bold text-white mb-4"> <h1 className="text-3xl font-bold text-white mb-4">
Buy and Sell on Campus Buy and Sell on Campus
@@ -217,297 +353,60 @@ const Home = () => {
</p> </p>
<button <button
onClick={handleSelling} onClick={handleSelling}
className="bg-emerald-500 hover:bg-emerald-600 text-white font-medium py-2 px-6 focus:outline-none focus:ring-2 focus:ring-emerald-400 transition-colors" className="bg-emerald-600 hover:bg-emerald-700 text-white font-medium py-2 px-6 focus:outline-none focus:ring-2 focus:ring-emerald-400 transition-colors"
> >
Post an Item Post an Item
</button> </button>
</div> </div>
</div> </div>
{/* Recent Listings */} {/* Floating Alert */}
{showAlert && ( {showAlert && (
<FloatingAlert <FloatingAlert
message="Product added to favorites!" message="Product added to favorites!"
onClose={() => setShowAlert(false)} onClose={() => setShowAlert(false)}
/> />
)} )}
<div className="relative py-4">
{/* Recommendations Section */}
<ScrollableProductList
containerId="RecomContainer"
products={recommended}
isLoading={isLoading.recommendations}
addToHistory={true}
>
<h2 className="text-xl font-semibold text-gray-800 mb-4"> <h2 className="text-xl font-semibold text-gray-800 mb-4">
Recommendation Recommended For You
</h2> </h2>
</ScrollableProductList>
<div className="relative"> {/* Recent Listings Section */}
{/* Left Button - Overlaid on products */} <ScrollableProductList
<button containerId="listingsContainer"
onClick={() => products={listings}
document isLoading={isLoading.listings}
.getElementById("RecomContainer") addToHistory={true}
.scrollBy({ left: -400, behavior: "smooth" })
}
className="absolute left-0 top-1/2 transform -translate-y-1/2 bg-gray-800 bg-opacity-70 text-white p-4 rounded-full z-20 hidden md:flex items-center justify-center w-12 h-12"
> >
<ChevronLeft size={24} />{" "}
</button>
{/* Scrollable Listings Container */}
<div
id="RecomContainer"
className="overflow-x-auto whitespace-nowrap flex space-x-6 scroll-smooth scrollbar-hide px-10 pl-0 rounded"
>
{recommended.map((recommended) => (
<Link
key={recommended.id}
to={`/product/${recommended.id}`}
onClick={() => addHistory(recommended.id)}
className="bg-white border border-gray-200 hover:shadow-md transition-shadow w-70 flex-shrink-0 relative"
>
<div className="relative">
<img
src={recommended.image}
alt={recommended.title}
className="w-full h-48 object-cover"
/>
<button
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
toggleFavorite(recommended.id);
}}
className="absolute top-0 right-0 p-2 rounded-bl-md bg-emerald-600 hover:bg-emerald-500 transition shadow-sm"
>
<Bookmark className="text-white w-5 h-5" />
</button>
</div>
<div className="p-4">
<h3 className="text-lg font-medium text-gray-800 leading-tight">
{recommended.title}
</h3>
<span className="font-semibold text-emerald-600 block mt-1">
${recommended.price}
</span>
<div className="flex items-center text-sm text-gray-500 mt-2">
<Tag className="h-4 w-4 mr-1" />
<span>{recommended.category}</span>
</div>
<div className="flex justify-between items-center pt-2 border-t border-gray-100 mt-3">
<span className="text-xs text-gray-500">
{recommended.datePosted}
</span>
<span className="text-sm font-medium text-gray-700">
{recommended.seller}
</span>
</div>
</div>
</Link>
))}
</div>
{/* Right Button - Overlaid on products */}
<button
onClick={() =>
document
.getElementById("RecomContainer")
.scrollBy({ left: 400, behavior: "smooth" })
}
className="absolute right-0 top-1/2 transform -translate-y-1/2 bg-gray-800 bg-opacity-70 text-white p-4 rounded-full z-20 hidden md:flex items-center justify-center w-12 h-12"
>
<ChevronRight size={24} />{" "}
</button>
</div>
</div>
{/* Recent Listings */}
{showAlert && (
<FloatingAlert
message="Product added to favorites!"
onClose={() => setShowAlert(false)}
/>
)}
<div className="relative py-4">
<h2 className="text-xl font-semibold text-gray-800 mb-4"> <h2 className="text-xl font-semibold text-gray-800 mb-4">
Recent Listings Recent Listings
</h2> </h2>
</ScrollableProductList>
<div className="relative">
{/* Left Button - Overlaid on products */}
<button
onClick={() =>
document
.getElementById("listingsContainer")
.scrollBy({ left: -400, behavior: "smooth" })
}
className="absolute left-0 top-1/2 transform -translate-y-1/2 bg-gray-800 bg-opacity-70 text-white p-4 rounded-full z-20 hidden md:flex items-center justify-center w-12 h-12"
>
<ChevronLeft size={24} />{" "}
</button>
{/* Scrollable Listings Container */}
<div
id="listingsContainer"
className="overflow-x-auto whitespace-nowrap flex space-x-6 scroll-smooth scrollbar-hide px-10 pl-0"
>
{listings.map((listing) => (
<Link
key={listing.id}
to={`/product/${listing.id}`}
className="bg-white border border-gray-200 hover:shadow-md transition-shadow w-70 flex-shrink-0 relative"
>
<div className="relative">
<img
src={listing.image}
alt={listing.title}
onClick={() => addHistory(listing.id)}
className="w-full h-48 object-cover"
/>
<button
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
toggleFavorite(listing.id);
}}
className="absolute top-0 right-0 p-2 rounded-bl-md bg-emerald-600 hover:bg-emerald-500 transition shadow-sm"
>
<Bookmark className="text-white w-5 h-5" />
</button>
</div>
<div className="p-4">
<h3 className="text-lg font-medium text-gray-800 leading-tight">
{listing.title}
</h3>
<span className="font-semibold text-emerald-600 block mt-1">
${listing.price}
</span>
<div className="flex items-center text-sm text-gray-500 mt-2">
<Tag className="h-4 w-4 mr-1" />
<span>{listing.category}</span>
</div>
<div className="flex justify-between items-center pt-2 border-t border-gray-100 mt-3">
<span className="text-xs text-gray-500">
{listing.datePosted}
</span>
<span className="text-sm font-medium text-gray-700">
{listing.seller}
</span>
</div>
</div>
</Link>
))}
</div>
{/* Right Button - Overlaid on products */}
<button
onClick={() =>
document
.getElementById("listingsContainer")
.scrollBy({ left: 400, behavior: "smooth" })
}
className="absolute right-0 top-1/2 transform -translate-y-1/2 bg-gray-800 bg-opacity-70 text-white p-4 rounded-full z-20 hidden md:flex items-center justify-center w-12 h-12"
>
<ChevronRight size={24} />{" "}
</button>
</div>
</div>
{/* History Section */} {/* History Section */}
{showAlert && ( {(history.length > 0 || isLoading.history) && (
<FloatingAlert <ScrollableProductList
message="Product added to favorites!" containerId="HistoryContainer"
onClose={() => setShowAlert(false)} products={history}
/> isLoading={isLoading.history}
>
<h2 className="text-xl font-semibold text-gray-800 mb-4">
Your Browsing History
</h2>
</ScrollableProductList>
)} )}
<div className="relative py-4">
<h2 className="text-xl font-semibold text-gray-800 mb-4">History</h2>
<div className="relative">
{/* Left Button - Overlaid on products */}
<button
onClick={() =>
document
.getElementById("HistoryContainer")
.scrollBy({ left: -400, behavior: "smooth" })
}
className="absolute left-0 top-1/2 transform -translate-y-1/2 bg-gray-800 bg-opacity-70 text-white p-4 rounded-full z-20 hidden md:flex items-center justify-center w-12 h-12"
>
<ChevronLeft size={24} />{" "}
</button>
{/* Scrollable Listings Container */}
<div
id="HistoryContainer"
className="overflow-x-auto whitespace-nowrap flex space-x-6 scroll-smooth scrollbar-hide px-10 pl-0"
>
{history.map((history) => (
<Link
key={history.id}
to={`/product/${history.id}`}
className="bg-white border border-gray-200 hover:shadow-md transition-shadow w-70 flex-shrink-0 relative"
>
<div className="relative">
<img
src={history.image}
alt={history.title}
className="w-full h-48 object-cover"
/>
<button
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
toggleFavorite(history.id);
}}
className="absolute top-0 right-0 p-2 rounded-bl-md bg-emerald-600 hover:bg-emerald-500 transition shadow-sm"
>
<Bookmark className="text-white w-5 h-5" />
</button>
</div> </div>
<div className="p-4"> {/* Footer */}
<h3 className="text-lg font-medium text-gray-800 leading-tight">
{history.title}
</h3>
<span className="font-semibold text-emerald-600 block mt-1">
${history.price}
</span>
<div className="flex items-center text-sm text-gray-500 mt-2">
<Tag className="h-4 w-4 mr-1" />
<span>{history.category}</span>
</div>
<div className="flex justify-between items-center pt-2 border-t border-gray-100 mt-3">
<span className="text-xs text-gray-500">
{history.datePosted}
</span>
<span className="text-sm font-medium text-gray-700">
{history.seller}
</span>
</div>
</div>
</Link>
))}
</div>
{/* Right Button - Overlaid on products */}
<button
onClick={() =>
document
.getElementById("HistoryContainer")
.scrollBy({ left: 400, behavior: "smooth" })
}
className="absolute right-0 top-1/2 transform -translate-y-1/2 bg-gray-800 bg-opacity-70 text-white p-4 rounded-full z-20 hidden md:flex items-center justify-center w-12 h-12"
>
<ChevronRight size={24} />{" "}
</button>
</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="container mx-auto px-4">
<div className="flex flex-col md:flex-row justify-between items-center"> <div className="flex flex-col md:flex-row justify-between items-center">

View File

@@ -0,0 +1,91 @@
import { useEffect, useState } from "react";
import { getProducts, removeProduct } from "../api/admin";
import { MdDelete } from "react-icons/md";
import Pagination from "../components/Pagination";
export default function ProductDashboard() {
const [products, setProducts] = useState([]);
const [total, setTotal] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
let pageLimit = 10;
const onChangePage = (page, limit = 10) => {
setCurrentPage(page);
fetchProducts(page, limit);
};
const fetchProducts = (page = 1, limit = 10) => {
getProducts(page, limit).then(({ products, total }) => {
setTotal(total);
setProducts(products);
});
};
const handleRemoveProduct = (id) => {
removeProduct(id)
.then((res) => {
fetchProducts(currentPage);
})
.catch((err) => {
console.log(err);
});
};
//Get user when initialize the component
useEffect(fetchProducts, []);
return (
<div className="pt-10 p-20">
<h1 className="text-4xl pb-3 font-bold text-green-800 underline">
PRODUCTS
</h1>
{products.length > 0 ? (
<>
<table className="table-fixed w-full text-center border border-green-600">
<thead className="bg-green-600 h-10">
<tr>
<th>ProductID</th>
<th>Name</th>
<th>Price</th>
<th>Category</th>
<th>Seller</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{products.map((product) => (
<tr
key={product.ProductID}
className="border border-green-600 h-10"
>
<td>{product.ProductID}</td>
<td>{product.ProductName}</td>
<td>{product.Price}</td>
<td>{product.Category ? product.Category : "N/A"}</td>
<td>{product.SellerName ? product.SellerName : "N/A"}</td>
<td className="flex justify-center pt-2">
<MdDelete
onClick={() => {
handleRemoveProduct(product.ProductID);
}}
className="hover:text-red-600 cursor-pointer transition-all text-xl"
/>
</td>
</tr>
))}
</tbody>
</table>
<Pagination
onChange={onChangePage}
pageNum={Math.ceil(total / pageLimit)}
/>
</>
) : (
<p className="text-red-700 text-xl bg-red-200 px-3 rounded-md py-1 w-fit">
No product exists!
</p>
)}
</div>
);
}

View File

@@ -1,7 +1,6 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useParams, Link } from "react-router-dom"; import { useParams, Link } from "react-router-dom";
import { import {
Heart,
ArrowLeft, ArrowLeft,
Tag, Tag,
User, User,
@@ -26,7 +25,6 @@ const ProductDetail = () => {
reviews: null, reviews: null,
submit: null, submit: null,
}); });
const [isFavorite, setIsFavorite] = useState(false);
const [showContactOptions, setShowContactOptions] = useState(false); const [showContactOptions, setShowContactOptions] = useState(false);
const [currentImage, setCurrentImage] = useState(0); const [currentImage, setCurrentImage] = useState(0);
const [reviews, setReviews] = useState([]); const [reviews, setReviews] = useState([]);
@@ -52,7 +50,6 @@ const ProductDetail = () => {
if (data.success) { if (data.success) {
setShowAlert(true); setShowAlert(true);
} }
console.log(`Add Product -> History: ${id}`);
}; };
const [reviewForm, setReviewForm] = useState({ const [reviewForm, setReviewForm] = useState({
@@ -248,7 +245,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-green-500"></div> <div className="animate-spin h-32 w-32 border-t-2 border-emerald-600"></div>
</div> </div>
); );
} }
@@ -262,7 +259,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-green-500 text-white px-4 py-2 hover:bg-green-600" className="mt-4 inline-block bg-emerald-600 text-white px-4 py-2 hover:bg-emerald-700"
> >
Back to Listings Back to Listings
</Link> </Link>
@@ -279,7 +276,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-green-500 text-white px-4 py-2 hover:bg-green-600" className="mt-4 inline-block bg-emerald-600 text-white px-4 py-2 hover:bg-emerald-700"
> >
Back to Listings Back to Listings
</Link> </Link>
@@ -291,15 +288,15 @@ const ProductDetail = () => {
// Render product details // Render product details
return ( return (
<div className="max-w-6xl mx-auto px-4 py-8"> <div className="max-w-6xl mx-auto px-4 py-8">
<div className="mb-6"> {/* <div className="mb-6">
<Link <Link
to="/search" to="/search"
className="flex items-center text-green-600 hover:text-green-700" className="flex items-center text-emerald-700 hover:text-emerald-700"
> >
<ArrowLeft className="h-4 w-4 mr-1" /> <ArrowLeft className="h-4 w-4 mr-1" />
<span>Back</span> <span>Back</span>
</Link> </Link>
</div> </div> */}
{showAlert && ( {showAlert && (
<FloatingAlert <FloatingAlert
message="Product added to favorites!" message="Product added to favorites!"
@@ -351,7 +348,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-green-500 border-2" : "border-gray-200"} min-w-[100px] cursor-pointer`} className={`bg-white border ${currentImage === index ? "border-emerald-600 border-2" : "border-gray-200"} min-w-[100px] cursor-pointer`}
onClick={() => selectImage(index)} onClick={() => selectImage(index)}
> >
<img <img
@@ -381,13 +378,13 @@ const ProductDetail = () => {
e.preventDefault(); e.preventDefault();
toggleFavorite(product.ProductID); toggleFavorite(product.ProductID);
}} }}
className="top-0 p-2 rounded-bl-md bg-emerald-600 hover:bg-emerald-500 transition shadow-sm" className="top-0 p-2 rounded-bl-md bg-emerald-700 hover:bg-emerald-600 transition shadow-sm"
> >
<Bookmark className="text-white w-5 h-5" /> <Bookmark className="text-white w-5 h-5" />
</button> </button>
</div> </div>
<div className="text-2xl font-bold text-green-600 mb-4"> <div className="text-2xl font-bold text-emerald-700 mb-4">
$ $
{typeof product.Price === "number" {typeof product.Price === "number"
? product.Price.toFixed(2) ? product.Price.toFixed(2)
@@ -418,7 +415,7 @@ const ProductDetail = () => {
<div className="relative"> <div className="relative">
<button <button
onClick={() => setShowContactOptions(!showContactOptions)} onClick={() => setShowContactOptions(!showContactOptions)}
className="w-full bg-green-500 hover:bg-green-600 text-white font-medium py-3 px-4 mb-3" className="w-full bg-emerald-700 hover:bg-emerald-700 text-white font-medium py-3 px-4 mb-3"
> >
Contact Seller Contact Seller
</button> </button>
@@ -430,7 +427,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-green-500" /> <Phone className="h-5 w-5 text-emerald-600" />
<span>Call Seller</span> <span>Call Seller</span>
</a> </a>
)} )}
@@ -440,7 +437,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-green-500" /> <Mail className="h-5 w-5 text-emerald-600" />
<span>Email Seller</span> <span>Email Seller</span>
</a> </a>
)} )}
@@ -477,7 +474,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-green-500"></div> <div className="animate-spin h-8 w-8 border-t-2 border-emerald-600"></div>
</div> </div>
) : error.reviews ? ( ) : error.reviews ? (
<div className="text-red-500 mb-4"> <div className="text-red-500 mb-4">
@@ -524,7 +521,7 @@ const ProductDetail = () => {
<div className="mt-4"> <div className="mt-4">
<button <button
onClick={() => setShowReviewForm(true)} onClick={() => setShowReviewForm(true)}
className="bg-green-500 hover:bg-green-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"
> >
Write a Review Write a Review
</button> </button>
@@ -582,7 +579,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-green-500" className="w-full p-3 border border-gray-300 focus:outline-none focus:border-emerald-600"
rows="4" rows="4"
required required
></textarea> ></textarea>
@@ -598,7 +595,7 @@ const ProductDetail = () => {
</button> </button>
<button <button
type="submit" type="submit"
className="px-4 py-2 bg-green-500 text-white hover:bg-green-600" className="px-4 py-2 bg-emerald-600 text-white hover:bg-emerald-700"
disabled={loading.submitting} disabled={loading.submitting}
> >
{loading.submitting ? "Submitting..." : "Submit Review"} {loading.submitting ? "Submitting..." : "Submit Review"}

View File

@@ -1,69 +1,205 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import ProductForm from "../components/ProductForm"; import { useLocation, Link } from "react-router-dom";
import { X, ChevronLeft, Plus, Trash2 } from "lucide-react";
const Selling = () => { const Selling = () => {
// State to store user's products
const [products, setProducts] = useState([]); const [products, setProducts] = useState([]);
// State to control when editing form is shown
const [showForm, setShowForm] = useState(false); const [showForm, setShowForm] = useState(false);
// State to store the product being edited (or empty for new product) const storedUser = JSON.parse(sessionStorage.getItem("user"));
const [categories, setCategories] = useState([]);
const [categoryMapping, setCategoryMapping] = useState({});
const [selectedCategory, setSelectedCategory] = useState("");
const [originalProduct, setOriginalProduct] = useState(null);
const [editingProduct, setEditingProduct] = useState({ const [editingProduct, setEditingProduct] = useState({
name: "", name: "",
price: "", price: "",
description: "", description: "",
categories: [], categories: [],
status: "Unsold",
images: [], images: [],
}); });
function reloadPage() {
var doctTimestamp = new Date(performance.timing.domLoading).getTime();
var now = Date.now();
if (now > doctTimestamp) {
location.reload();
}
}
// Fetch categories from API
useEffect(() => {
const fetchCategories = async () => {
try {
const response = await fetch("http://localhost:3030/api/category");
if (!response.ok) throw new Error("Failed to fetch categories");
const responseJson = await response.json();
const data = responseJson.data;
// Create an array of category names for the dropdown
const categoryNames = [];
const mapping = {};
// Process the data properly to avoid rendering objects
Object.entries(data).forEach(([id, name]) => {
// Make sure each category name is a string
const categoryName = String(name);
categoryNames.push(categoryName);
mapping[categoryName] = parseInt(id);
});
setCategories(categoryNames);
setCategoryMapping(mapping);
} catch (error) {
console.error("Error fetching categories:", error);
}
};
fetchCategories();
}, []);
// Simulate fetching products from API/database on component mount // Simulate fetching products from API/database on component mount
useEffect(() => { useEffect(() => {
// This would be replaced with a real API call
const fetchProducts = async () => { const fetchProducts = async () => {
// Mock data try {
const mockProducts = [ // Replace with your actual API endpoint
const response = await fetch(
"http://localhost:3030/api/product/myProduct",
{ {
id: "1", method: "POST",
name: "Vintage Camera", headers: {
price: "299.99", "Content-Type": "application/json",
description: "A beautiful vintage film camera in excellent condition",
categories: ["Electronics", "Art & Collectibles"],
status: "Unsold",
images: ["/public/Pictures/Dell1.jpg"],
}, },
{ body: JSON.stringify({
id: "2", userID: storedUser.ID,
name: "Leather Jacket", }),
price: "149.50",
description: "Genuine leather jacket, worn only a few times",
categories: ["Clothing"],
status: "Unsold",
images: [],
}, },
]; );
setProducts(mockProducts); if (!response.ok) {
throw new Error("Network response was not ok");
}
const datajson = await response.json();
setProducts(datajson.data);
} catch (error) {
console.error("Error fetching products:", error);
// You might want to set an error state here
}
}; };
fetchProducts(); fetchProducts();
}, []); }, []); // Add userId to dependency array if it might change
// Handle creating or updating a product // When editing a product, save the original product properly
const handleSaveProduct = () => { const handleEditProduct = (product) => {
if (editingProduct.id) { // Save the original product completely
// Update existing product setOriginalProduct(product);
setProducts(
products.map((p) => (p.id === editingProduct.id ? editingProduct : p)), // Convert category ID to category name if needed
); const categoryName = getCategoryNameById(product.CategoryID);
} else {
// Create new product setEditingProduct({
const newProduct = { ...product,
...editingProduct, categories: categoryName ? [categoryName] : [],
id: Date.now().toString(), // Generate a temporary ID images: product.images || [], // Ensure images array exists
});
setShowForm(true);
}; };
setProducts([...products, newProduct]);
// Then update the handleSaveProduct function to properly merge values
const handleSaveProduct = async () => {
if (!(editingProduct.categories || []).length) {
alert("Please select at least one category");
return;
} }
try {
const imagePaths = [];
// Handle images properly
if (editingProduct.images && editingProduct.images.length > 0) {
// If there are new images uploaded (File objects)
const newImages = editingProduct.images.filter(
(img) => img instanceof File,
);
newImages.forEach((file) => {
const simulatedPath = `/public/uploads/${file.name}`;
imagePaths.push(simulatedPath);
});
// Also include any existing image URLs that are strings, not File objects
const existingImages = editingProduct.images.filter(
(img) => typeof img === "string",
);
if (existingImages.length > 0) {
imagePaths.push(...existingImages);
}
} else if (originalProduct?.image_url) {
// If no new images but there was an original image URL
imagePaths.push(originalProduct.image_url);
}
const categoryName = (editingProduct.categories || [])[0];
const categoryID =
categoryMapping[categoryName] || originalProduct?.CategoryID || 1;
// Create payload with proper fallback to original values
const payload = {
name:
editingProduct.Name ||
editingProduct.name ||
originalProduct?.Name ||
"",
price: parseFloat(
editingProduct.Price ||
editingProduct.price ||
originalProduct?.Price ||
0,
),
qty: 1,
userID: storedUser.ID,
description:
editingProduct.Description ||
editingProduct.description ||
originalProduct?.Description ||
"",
category: categoryID,
images:
imagePaths.length > 0
? imagePaths
: originalProduct?.image_url
? [originalProduct.image_url]
: [],
};
console.log("Sending payload:", payload);
const endpoint = editingProduct.ProductID
? `http://localhost:3030/api/product/update/${editingProduct.ProductID}`
: "http://localhost:3030/api/product/addProduct";
const method = editingProduct.ProductID ? "PUT" : "POST";
const response = await fetch(endpoint, {
method,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorData = await response.text();
throw new Error(
`${editingProduct.ProductID ? "Failed to update" : "Failed to add"} product: ${errorData}`,
);
}
const data = await response.json();
console.log("Product saved:", data);
// Reset form and hide it // Reset form and hide it
setShowForm(false); setShowForm(false);
setEditingProduct({ setEditingProduct({
@@ -71,25 +207,58 @@ const Selling = () => {
price: "", price: "",
description: "", description: "",
categories: [], categories: [],
status: "Unsold",
images: [], images: [],
}); });
};
// Handle product deletion setOriginalProduct(null); // reset original as well
const handleDeleteProduct = (productId) => {
if (window.confirm("Are you sure you want to delete this product?")) { reloadPage();
setProducts(products.filter((p) => p.id !== productId)); } catch (error) {
console.error("Error saving product:", error);
alert(`Error saving product: ${error.message}`);
} }
}; };
// Handle editing a product // Handle product deletion
const handleEditProduct = (product) => { const handleDeleteProduct = async (productId) => {
setEditingProduct({ try {
...product, // Replace with your actual API endpoint
images: product.images || [], // Ensure images array exists const response = await fetch(
}); "http://localhost:3030/api/product/delProduct",
setShowForm(true); {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userID: storedUser.ID,
productID: productId,
}),
},
);
reloadPage();
console.log("deleteproodidt");
if (!response.ok) {
throw new Error("Network response was not ok");
}
} catch (error) {
console.error("Error fetching products:", error);
// You might want to set an error state here
}
};
// Helper function to get category name from ID
const getCategoryNameById = (categoryId) => {
if (!categoryId || !categoryMapping) return null;
// Find the category name by ID
for (const [name, id] of Object.entries(categoryMapping)) {
if (id === categoryId) {
return name;
}
}
return null;
}; };
// Handle adding a new product // Handle adding a new product
@@ -99,12 +268,54 @@ const Selling = () => {
price: "", price: "",
description: "", description: "",
categories: [], categories: [],
status: "Unsold",
images: [], images: [],
}); });
setShowForm(true); setShowForm(true);
}; };
const addCategory = () => {
if (
selectedCategory &&
!(editingProduct.categories || []).includes(selectedCategory)
) {
setEditingProduct((prev) => ({
...prev,
categories: [...(prev.categories || []), selectedCategory],
}));
setSelectedCategory("");
}
};
const removeCategory = (categoryToRemove) => {
setEditingProduct((prev) => ({
...prev,
categories: (prev.categories || []).filter(
(cat) => cat !== categoryToRemove,
),
}));
};
const markAsSold = async () => {
// This would call an API to move the product to the transaction table
try {
// API call would go here
console.log(
"Moving product to transaction table:",
editingProduct.ProductID,
);
// Toggle the sold status in the UI
setEditingProduct((prev) => ({
...prev,
isSold: !prev.isSold,
}));
// You would add your API call here to update the backend
} catch (error) {
console.error("Error marking product as sold:", error);
}
};
return ( return (
<div className="container mx-auto p-4 max-w-6xl"> <div className="container mx-auto p-4 max-w-6xl">
<div className="flex justify-between items-center mb-6"> <div className="flex justify-between items-center mb-6">
@@ -120,12 +331,279 @@ const Selling = () => {
</div> </div>
{showForm ? ( {showForm ? (
<ProductForm <div className="bg-white border border-gray-200 shadow-md p-6">
editingProduct={editingProduct} {/* Back Button */}
setEditingProduct={setEditingProduct} <button
onSave={handleSaveProduct} onClick={() => setShowForm(false)}
onCancel={() => setShowForm(false)} className="mb-4 text-emerald-600 hover:text-emerald-800 flex items-center gap-1"
>
<ChevronLeft size={16} />
<span>Back to Listings</span>
</button>
<h3 className="text-xl font-bold text-gray-800 mb-6 border-b border-gray-200 pb-3">
{editingProduct?.ProductID
? "Edit Your Product"
: "List a New Product"}
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Product Name */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Product Name
</label>
<input
type="text"
value={editingProduct.Name || editingProduct.name || ""}
onChange={(e) =>
setEditingProduct({
...editingProduct,
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"
/> />
</div>
{/* Price */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Price ($)
</label>
<input
type="number"
value={editingProduct.Price || editingProduct.price || ""}
onChange={(e) =>
setEditingProduct({
...editingProduct,
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"
/>
</div>
{/* Sold Status */}
<div className="md:col-span-2">
<div className="flex items-center mt-2">
{editingProduct.isSold && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">
Sold
</span>
)}
</div>
</div>
{/* Categories */}
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Categories
</label>
<div className="flex gap-2">
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="flex-1 px-3 py-2 border border-gray-300 focus:border-emerald-500 focus:outline-none"
>
<option value="" disabled>
Select a category
</option>
{categories
.filter(
(cat) => !(editingProduct.categories || []).includes(cat),
)
.map((category, index) => (
<option key={index} value={category}>
{category}
</option>
))}
</select>
<button
type="button"
onClick={addCategory}
disabled={!selectedCategory}
className="px-3 py-2 bg-emerald-600 text-white hover:bg-emerald-700 disabled:bg-gray-300 disabled:cursor-not-allowed flex items-center gap-1"
>
<Plus size={16} />
<span>Add</span>
</button>
</div>
{/* Selected Categories */}
{(editingProduct.categories || []).length > 0 ? (
<div className="mt-2 flex flex-wrap gap-2">
{(editingProduct.categories || []).map((category, index) => (
<span
key={index}
className="inline-flex items-center px-2 py-1 bg-emerald-100 text-emerald-800"
>
{category}
<button
type="button"
onClick={() => removeCategory(category)}
className="ml-1 text-emerald-600 hover:text-emerald-800"
>
<X size={14} />
</button>
</span>
))}
</div>
) : (
<p className="text-xs text-gray-500 mt-1">
Please select at least one category
</p>
)}
</div>
{/* Description */}
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Description
</label>
<textarea
value={
editingProduct.Description || editingProduct.description || ""
}
onChange={(e) =>
setEditingProduct({
...editingProduct,
Description: e.target.value,
description: e.target.value,
})
}
rows="4"
className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-500 focus:outline-none"
placeholder="Describe your product in detail..."
></textarea>
</div>
{/* Image Upload */}
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Product Images <span className="text-gray-500">(Max 5)</span>
</label>
<input
type="file"
accept="image/*"
multiple
onChange={(e) => {
const files = Array.from(e.target.files).slice(0, 5);
setEditingProduct((prev) => ({
...prev,
images: [...(prev.images || []), ...files].slice(0, 5),
}));
}}
className="hidden"
id="image-upload"
/>
<label
htmlFor="image-upload"
className="block w-full p-3 border border-gray-300 bg-gray-50 text-center cursor-pointer hover:bg-gray-100"
>
<span className="text-emerald-600 font-medium">
Click to upload images
</span>
</label>
{/* Image previews */}
{(editingProduct.images || []).length > 0 && (
<div className="mt-3">
<div className="flex justify-between items-center mb-2">
<p className="text-sm text-gray-600">
{editingProduct.images.length}{" "}
{editingProduct.images.length === 1 ? "image" : "images"}{" "}
selected
</p>
<button
onClick={() =>
setEditingProduct((prev) => ({ ...prev, images: [] }))
}
className="text-sm text-red-600 hover:text-red-800 flex items-center gap-1"
>
<Trash2 size={14} />
<span>Clear all</span>
</button>
</div>
<div className="flex flex-wrap gap-2">
{editingProduct.images.map((img, idx) => (
<div
key={idx}
className="relative w-20 h-20 border border-gray-200 overflow-hidden"
>
<img
src={URL.createObjectURL(img)}
alt={`Product ${idx + 1}`}
className="w-full h-full object-cover"
/>
<button
onClick={() => {
const updated = [...editingProduct.images];
updated.splice(idx, 1);
setEditingProduct((prev) => ({
...prev,
images: updated,
}));
}}
className="absolute top-0 right-0 bg-white bg-opacity-80 w-6 h-6 flex items-center justify-center text-gray-700 hover:text-red-600"
>
<X size={14} />
</button>
</div>
))}
</div>
</div>
)}
{/* Show current image if editing */}
{editingProduct.image_url && (
<div className="mt-3">
<p className="text-sm text-gray-600 mb-2">Current image:</p>
<div className="relative w-20 h-20 border border-gray-200 overflow-hidden">
<img
src={editingProduct.image_url}
alt="Current product"
className="w-full h-full object-cover"
/>
</div>
</div>
)}
</div>
</div>
{/* Actions */}
<div className="mt-6 flex justify-end gap-3 border-t border-gray-200 pt-4">
<button
onClick={() => setShowForm(false)}
className="bg-gray-100 text-gray-700 px-4 py-2 hover:bg-gray-200 rounded-md"
>
Cancel
</button>
{editingProduct.ProductID && (
<button
onClick={markAsSold}
className={`px-4 py-2 rounded-md transition-colors ${
editingProduct.isSold
? "bg-green-600 text-white hover:bg-green-700"
: "bg-red-600 text-white hover:bg-red-700"
}`}
>
Mark as {editingProduct.isSold ? "Available" : "Sold"}
</button>
)}
<button
onClick={handleSaveProduct}
className="bg-emerald-600 text-white px-6 py-2 hover:bg-emerald-700 rounded-md"
>
{editingProduct.ProductID ? "Update Product" : "Add Product"}
</button>
</div>
</div>
) : ( ) : (
<> <>
{products.length === 0 ? ( {products.length === 0 ? (
@@ -143,15 +621,16 @@ const Selling = () => {
) : ( ) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{products.map((product) => ( {products.map((product) => (
<div <Link
key={product.id} key={product.ProductID}
className="border-2 border-gray-200 overflow-hidden hover:shadow-md transition-shadow" to={`/product/${product.ProductID}`}
> >
<div className="border-2 border-gray-200 overflow-hidden hover:shadow-md transition-shadow">
<div className="h-48 bg-gray-200 flex items-center justify-center"> <div className="h-48 bg-gray-200 flex items-center justify-center">
{product.images && product.images.length > 0 ? ( {product.image_url && product.image_url.length > 0 ? (
<img <img
src={product.images[0] || ""} src={product.image_url || ""}
alt={product.name} alt={product.Name}
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
) : ( ) : (
@@ -162,49 +641,44 @@ const Selling = () => {
<div className="p-4"> <div className="p-4">
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
<h3 className="text-lg font-semibold text-gray-800"> <h3 className="text-lg font-semibold text-gray-800">
{product.name} {product.Name}
</h3> </h3>
<span
className={`px-2 py-1 text-xs ${
product.status === "Sold"
? "bg-gray-200 text-gray-700"
: "bg-emerald-100 text-emerald-800"
}`}
>
{product.status}
</span>
</div> </div>
<p className="text-emerald-600 font-bold mt-1"> <p className="text-emerald-600 font-bold mt-1">
${product.price} ${product.Price}
</p> </p>
{product.categories && product.categories.length > 0 && ( {product.CategoryID && (
<div className="mt-2 flex flex-wrap gap-1"> <div className="mt-2 flex flex-wrap gap-1">
{product.categories.map((category) => ( <span className="text-xs bg-gray-100 text-gray-600 px-2 py-1">
<span {getCategoryNameById(product.CategoryID) ||
key={category} product.CategoryID}
className="text-xs bg-gray-100 text-gray-600 px-2 py-1 "
>
{category}
</span> </span>
))}
</div> </div>
)} )}
<p className="text-gray-500 text-sm mt-2 line-clamp-2"> <p className="text-gray-500 text-sm mt-2 line-clamp-2">
{product.description} {product.Description}
</p> </p>
<div className="mt-4 flex justify-end gap-2"> <div className="mt-4 flex justify-end gap-2">
<button <button
onClick={() => handleDeleteProduct(product.id)} onClick={(e) => {
e.stopPropagation();
e.preventDefault();
handleDeleteProduct(product.ProductID);
}}
className="text-red-600 hover:text-red-800" className="text-red-600 hover:text-red-800"
> >
Delete Delete
</button> </button>
<button <button
onClick={() => handleEditProduct(product)} onClick={(e) => {
e.stopPropagation();
e.preventDefault();
handleEditProduct(product);
}}
className="text-emerald-600 hover:text-emerald-800 font-medium" className="text-emerald-600 hover:text-emerald-800 font-medium"
> >
Edit Edit
@@ -212,6 +686,7 @@ const Selling = () => {
</div> </div>
</div> </div>
</div> </div>
</Link>
))} ))}
</div> </div>
)} )}

View File

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

View File

@@ -0,0 +1,91 @@
import { useEffect, useState } from "react";
import { getUsers, removeUser } from "../api/admin";
import { MdDelete } from "react-icons/md";
import Pagination from "../components/Pagination";
export default function UserDashboard() {
const [users, setUsers] = useState([]);
const [total, setTotal] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
let pageLimit = 10;
const onChangePage = (page, limit = 10) => {
setCurrentPage(page);
fetchUsers(page, limit);
};
const fetchUsers = (page = 1, limit = 10) => {
getUsers(page, limit).then(({ users, total }) => {
setUsers(users);
setTotal(total);
});
};
const handleRemoveUser = (id) => {
removeUser(id)
.then((res) => {
fetchUsers(currentPage);
})
.catch((err) => {
console.log(err);
});
};
//Get user when initialize the component
useEffect(fetchUsers, []);
return (
<div className="pt-10 p-20">
<h1 className="text-4xl pb-3 font-bold text-green-800 underline">
USERS
</h1>
{users.length > 0 ? (
<>
{" "}
<table className="table-fixed w-full text-center border border-green-600">
<thead className="bg-green-600 h-10">
<tr>
<th>UserID</th>
<th>UCID</th>
<th>Name</th>
<th>Email</th>
<th>Phone</th>
<th>Address</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr key={user.UserID} className="border border-green-600 h-10">
<td>{user.UserID}</td>
<td>{user.UCID}</td>
<td>{user.Name}</td>
<td>{user.Email}</td>
<td>{user.Phone}</td>
<td>{user.Address}</td>
<td className="flex justify-center pt-2">
<MdDelete
onClick={() => {
handleRemoveUser(user.UserID);
}}
className="hover:text-red-600 cursor-pointer transition-all text-xl"
/>
</td>
</tr>
))}
</tbody>
</table>
<Pagination
pageNum={Math.ceil(total / pageLimit)}
onChange={onChangePage}
/>
</>
) : (
<p className="text-red-700 text-xl bg-red-200 px-3 rounded-md py-1">
No user exists!
</p>
)}
</div>
);
}

View File

@@ -67,40 +67,41 @@ VALUES
(1, TRUE, TRUE), (1, TRUE, TRUE),
(2, TRUE, FALSE); (2, TRUE, FALSE);
-- Insert Categories
-- Insert Categories -- Insert Categories
INSERT INTO INSERT INTO
Category (CategoryID, Name) Category (Name)
VALUES VALUES
(1, 'Textbooks'), ('Textbooks'),
(2, 'Electronics'), ('Electronics'),
(3, 'Furniture'), ('Furniture'),
(4, 'Clothing'), ('Clothing'),
(5, 'Sports Equipment'), ('Sports Equipment'),
(6, 'Musical Instruments'), ('Musical Instruments'),
(7, 'Art Supplies'), ('Art Supplies'),
(8, 'Kitchen Appliances'), ('Kitchen Appliances'),
(9, 'Gaming'), ('Gaming'),
(10, 'Bicycles'), ('Bicycles'),
(11, 'Computer Accessories'), ('Computer Accessories'),
(12, 'Stationery'), ('Stationery'),
(13, 'Fitness Equipment'), ('Fitness Equipment'),
(14, 'Winter Sports'), ('Winter Sports'),
(15, 'Lab Equipment'), ('Lab Equipment'),
(16, 'Camping Gear'), ('Camping Gear'),
(17, 'School Supplies'), ('School Supplies'),
(18, 'Office Furniture'), ('Office Furniture'),
(19, 'Books (Non-textbook)'), ('Books (-textbook)'),
(20, 'Math & Science Resources'), ('Math & Science Resources'),
(21, 'Engineering Tools'), ('Engineering Tools'),
(22, 'Backpacks & Bags'), ('Backpacks & Bags'),
(23, 'Audio Equipment'), ('Audio Equipment'),
(24, 'Dorm Essentials'), ('Dorm Essentials'),
(25, 'Smartphones & Tablets'), ('Smartphones & Tablets'),
(26, 'Winter Clothing'), ('Winter Clothing'),
(27, 'Photography Equipment'), ('Photography Equipment'),
(28, 'Event Tickets'), ('Event Tickets'),
(29, 'Software Licenses'), ('Software Licenses'),
(30, 'Transportation (Car Pool)'); ('Transportation (Car Pool)');
-- Insert Products -- Insert Products
INSERT INTO INSERT INTO
@@ -319,29 +320,29 @@ VALUES
INSERT INTO INSERT INTO
Image_URL (URL, ProductID) Image_URL (URL, ProductID)
VALUES VALUES
('/Pictures/Dell1.jpg', 1), ('/Uploads/Dell1.jpg', 1),
('/Pictures/Dell2.jpg', 1), ('/Uploads/Dell2.jpg', 1),
('/Pictures/Dell3.jpg', 1), ('/Uploads/Dell3.jpg', 1),
('/Pictures/HP-Laptop1.jpg', 2), ('/Uploads/HP-Laptop1.jpg', 2),
('/Pictures/HP-Laptop1.jpg', 2), ('/Uploads/HP-Laptop1.jpg', 2),
('/Pictures/Dorm-Desk.jpg', 3), ('/Uploads/Dorm-Desk.jpg', 3),
('/Pictures/University-Hoodie.jpg', 4), ('/Uploads/University-Hoodie.jpg', 4),
('/Pictures/Basketball.jpg', 5), ('/Uploads/Basketball.jpg', 5),
('/Pictures/Acoustic-Guitar.jpg', 6), ('/Uploads/Acoustic-Guitar.jpg', 6),
('/Pictures/Physics-Textbook.jpg', 7), ('/Uploads/Physics-Textbook.jpg', 7),
('/Pictures/Mini-Fridge.jpg', 8), ('/Uploads/Mini-Fridge.jpg', 8),
('/Pictures/Controller.jpg', 9), ('/Uploads/Controller.jpg', 9),
('/Pictures/Mountain-Bike.jpg', 10), ('/Uploads/Mountain-Bike.jpg', 10),
('/Pictures/Wireless-Mouse.jpg', 11), ('/Uploads/Wireless-Mouse.jpg', 11),
('/Pictures/Lab-Coat.jpg', 12), ('/Uploads/Lab-Coat.jpg', 12),
('/Pictures/Calculator.jpg', 13), ('/Uploads/Calculator.jpg', 13),
('/Pictures/Yoga-Mat.jpg', 14), ('/Uploads/Yoga-Mat.jpg', 14),
('/Pictures/Winter-Jacket.jpg', 15), ('/Uploads/Winter-Jacket.jpg', 15),
('/Pictures/CS-Textbook.jpg', 16), ('/Uploads/CS-Textbook.jpg', 16),
('/Pictures/Desk-Lamp.jpg', 17), ('/Uploads/Desk-Lamp.jpg', 17),
('/Pictures/HP-Calculator.jpg', 18), ('/Uploads/HP-Calculator.jpg', 18),
('/Pictures/Bluetooth-Speaker.jpg', 19), ('/Uploads/Bluetooth-Speaker.jpg', 19),
('/Pictures/Backpack.jpg', 20); ('/Uploads/Backpack.jpg', 20);
-- Insert Product-Category relationships (products with multiple categories) -- Insert Product-Category relationships (products with multiple categories)
INSERT INTO INSERT INTO

View File

@@ -24,7 +24,7 @@ CREATE TABLE UserRole (
-- Category Entity (must be created before Product or else error) -- Category Entity (must be created before Product or else error)
CREATE TABLE Category ( CREATE TABLE Category (
CategoryID INT PRIMARY KEY, CategoryID INT AUTO_INCREMENT PRIMARY KEY,
Name VARCHAR(255) NOT NULL Name VARCHAR(255) NOT NULL
); );
@@ -38,15 +38,15 @@ CREATE TABLE Product (
Description TEXT, Description TEXT,
CategoryID INT NOT NULL, CategoryID INT NOT NULL,
Date DATETIME DEFAULT CURRENT_TIMESTAMP, Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID), FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE SET NULL,
FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID) FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID) ON DELETE SET NULL
); );
-- Fixed Image_URL table -- Fixed Image_URL table
CREATE TABLE Image_URL ( CREATE TABLE Image_URL (
URL VARCHAR(255), URL VARCHAR(255),
ProductID INT, ProductID INT,
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE
); );
-- Fixed Review Entity (Many-to-One with User, Many-to-One with Product) -- Fixed Review Entity (Many-to-One with User, Many-to-One with Product)
@@ -60,8 +60,8 @@ 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), FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE SET NULL,
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) 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)
@@ -71,8 +71,8 @@ CREATE TABLE Transaction (
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), FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE SET NULL
); );
-- Recommendation Entity (Many-to-One with User, Many-to-One with Product) -- Recommendation Entity (Many-to-One with User, Many-to-One with Product)
@@ -81,8 +81,8 @@ CREATE TABLE Recommendation (
UserID INT, UserID INT,
RecommendedProductID INT, RecommendedProductID INT,
Date DATETIME DEFAULT CURRENT_TIMESTAMP, Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID), FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
FOREIGN KEY (RecommendedProductID) REFERENCES Product (ProductID) FOREIGN KEY (RecommendedProductID) REFERENCES Product (ProductID) ON DELETE CASCADE
); );
-- History Entity (Many-to-One with User, Many-to-One with Product) -- History Entity (Many-to-One with User, Many-to-One with Product)
@@ -91,8 +91,8 @@ CREATE TABLE History (
UserID INT, UserID INT,
ProductID INT, ProductID INT,
Date DATETIME DEFAULT CURRENT_TIMESTAMP, Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID), FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE
); );
-- Favorites Entity (Many-to-One with User, Many-to-One with Product) -- Favorites Entity (Many-to-One with User, Many-to-One with Product)
@@ -100,8 +100,8 @@ CREATE TABLE Favorites (
FavoriteID INT AUTO_INCREMENT PRIMARY KEY, FavoriteID INT AUTO_INCREMENT PRIMARY KEY,
UserID INT, UserID INT,
ProductID INT, ProductID INT,
FOREIGN KEY (UserID) REFERENCES User (UserID), FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
FOREIGN KEY (ProductID) REFERENCES Product (ProductID), FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE,
UNIQUE (UserID, ProductID) UNIQUE (UserID, ProductID)
); );
@@ -110,8 +110,8 @@ CREATE TABLE Product_Category (
ProductID INT, ProductID INT,
CategoryID INT, CategoryID INT,
PRIMARY KEY (ProductID, CategoryID), PRIMARY KEY (ProductID, CategoryID),
FOREIGN KEY (ProductID) REFERENCES Product (ProductID), FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE,
FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID) FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID) ON DELETE CASCADE
); );
-- Login Authentication table -- Login Authentication table

View File

@@ -3,7 +3,7 @@ import mysql.connector
from sklearn.metrics.pairwise import cosine_similarity from sklearn.metrics.pairwise import cosine_similarity
import numpy as np import numpy as np
import logging import logging
from unittest import result import random
def database(): def database():
db_connection = mysql.connector.connect( db_connection = mysql.connector.connect(
@@ -14,34 +14,106 @@ def database():
) )
return db_connection return db_connection
def get_popular_products(): def delete_user_recommendations(user_id):
pass
def delete_user_recommendation(userID, Array):
db_con = database() db_con = database()
cursor = db_con.cursor() cursor = db_con.cursor()
try: try:
for item in Array: cursor.execute("DELETE FROM Recommendation WHERE UserID = %s", (user_id,))
#Product ID starts form index 1
item_value = item + 1
print(item_value)
# Use parameterized queries to prevent SQL injection
cursor.execute(f"INTO Recommendation (UserID, RecommendedProductID) VALUES ({userID}, {item_value});")
db_con.commit() db_con.commit()
print(f"Deleted existing recommendations for user {user_id}")
logging.info(f"Deleted existing recommendations for user {user_id}")
return True
except Exception as e:
logging.error(f"Error deleting recommendations for user {user_id}: {str(e)}")
db_con.rollback()
return False
finally:
cursor.close()
db_con.close()
#results = cursor.fetchall() def get_random_products(count=10, exclude_list=None):
#print(results) """Get random products from the database, excluding any in the exclude_list"""
except:
pass
def get_all_products():
db_con = database() db_con = database()
cursor = db_con.cursor() cursor = db_con.cursor()
try:
if exclude_list and len(exclude_list) > 0:
# Convert exclude_list to string for SQL IN clause
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}")
else:
cursor.execute(f"SELECT ProductID FROM Product ORDER BY RAND() LIMIT {count}")
random_products = [row[0] for row in cursor.fetchall()]
return random_products
except Exception as e:
logging.error(f"Error getting random products: {str(e)}")
return []
finally:
cursor.close()
db_con.close()
def get_popular_products(count=10):
"""Get popular products based on history table frequency"""
db_con = database()
cursor = db_con.cursor()
try:
# Get products that appear most frequently in history
cursor.execute("""
SELECT ProductID, COUNT(*) as count
FROM History
GROUP BY ProductID
ORDER BY count DESC
LIMIT %s
""", (count,))
popular_products = [row[0] for row in cursor.fetchall()]
# If not enough popular products, supplement with random ones
if len(popular_products) < count:
random_products = get_random_products(count - len(popular_products), popular_products)
popular_products.extend(random_products)
return popular_products
except Exception as e:
logging.error(f"Error getting popular products: {str(e)}")
return get_random_products(count) # Fallback to random products
finally:
cursor.close()
db_con.close()
def has_user_history_or_recommendations(user_id):
"""Check if user exists in History or Recommendation table"""
db_con = database()
cursor = db_con.cursor()
try:
# Check if user has history
cursor.execute("SELECT COUNT(*) FROM History WHERE UserID = %s", (user_id,))
history_count = cursor.fetchone()[0]
# Check if user has recommendations
cursor.execute("SELECT COUNT(*) FROM Recommendation WHERE UserID = %s", (user_id,))
recommendation_count = cursor.fetchone()[0]
return history_count > 0 or recommendation_count > 0
except Exception as e:
logging.error(f"Error checking user history/recommendations: {str(e)}")
return False
finally:
cursor.close()
db_con.close()
def get_all_products():
db_con = database()
cursor = db_con.cursor()
try:
cursor.execute("SELECT CategoryID FROM Category") cursor.execute("SELECT CategoryID FROM Category")
categories = cursor.fetchall() categories = cursor.fetchall()
@@ -62,25 +134,33 @@ def get_all_products():
results = cursor.fetchall() results = cursor.fetchall()
final = [] final = []
product_ids = []
for row in results: for row in results:
text_list = list(row) text_list = list(row)
text_list.pop(0) product_id = text_list.pop(0) # Save the product ID before removing it
final.append(text_list) final.append(text_list)
product_ids.append(product_id)
cursor.close() cursor.close()
db_con.close() db_con.close()
return final return final, product_ids # Return both feature vectors and product IDs
except Exception as e:
logging.error(f"Error getting all products: {str(e)}")
cursor.close()
db_con.close()
return [], []
def get_user_history(user_id): def get_user_history(user_id):
db_con = database() db_con = database()
cursor = db_con.cursor() cursor = db_con.cursor()
try:
cursor.execute("SELECT CategoryID FROM Category") cursor.execute("SELECT CategoryID FROM Category")
categories = cursor.fetchall() categories = cursor.fetchall()
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 catefory and then append that to the new column category_id = category[0] # get the uid of the category and then append that to the new column
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 pc.CategoryID = {category_id} THEN 1 ELSE 0 END) AS `Cat_{category_id}`"
final_query = f""" final_query = f"""
@@ -103,56 +183,88 @@ def get_user_history(user_id):
cursor.close() cursor.close()
db_con.close() db_con.close()
return final return final
except Exception as e:
logging.error(f"Error getting user history: {str(e)}")
cursor.close()
db_con.close()
return []
def get_recommendations(user_id, top_n=10): def get_recommendations(user_id, top_n=5):
try: try:
# Always delete existing recommendations first
delete_user_recommendations(user_id)
# Check if user has history or recommendations
if not has_user_history_or_recommendations(user_id):
# Cold start: return random products
random_recs = get_random_products(top_n)
# Store these random recommendations
history_upload(user_id, random_recs)
# Add 5 more unique random products
additional_random = get_random_products(5, random_recs)
history_upload(user_id, additional_random)
return random_recs + additional_random
# Get all products and user history with their category vectors # Get all products and user history with their category vectors
all_products = 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:
# #Cold start: return popular products if not user_history:
# return get_popular_products(top_n) # User exists but has no history yet
popular_recs = get_popular_products(top_n)
history_upload(user_id, popular_recs)
# Add 5 more unique random products
additional_random = get_random_products(5, popular_recs)
history_upload(user_id, additional_random)
return popular_recs + additional_random
# Calculate similarity between all products and user history # Calculate similarity between all products and user history
user_profile = np.mean(user_history, axis=0) # Average user preferences user_profile = np.mean(user_history, axis=0) # Average user preferences
similarities = cosine_similarity([user_profile], all_products) similarities = cosine_similarity([user_profile], all_product_features)
# finds the indices of the top N products that have the highest print(similarities)
# cosine similarity with the user's profile and sorted from most similar to least similar.
# 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]
print("product", product_indices)
# Get the recommended product IDs # Get the actual product IDs using the indices
recommended_products = [all_products[i][0] for i in product_indices] # Product IDs recommended_product_ids = [all_product_ids[i] for i in product_indices]
# Upload the recommendations to the database # Upload the core recommendations to the database
history_upload(user_id, product_indices) # Pass the indices directly to history_upload 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)
history_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
return recommended_products
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)}")
# return get_popular_products(top_n) # Fallback to popular products # Fallback to random products
random_products = get_random_products(top_n + 5)
return random_products
def history_upload(userID, anrr): def history_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 item in anrr: for product_id in products:
#Product ID starts form index 1
item_value = item + 1
print(item_value)
# Use parameterized queries to prevent SQL injection # Use parameterized queries to prevent SQL injection
cursor.execute(f"INSERT INTO Recommendation (UserID, RecommendedProductID) VALUES ({userID}, {item_value});") cursor.execute("INSERT INTO Recommendation (UserID, RecommendedProductID) VALUES (%s, %s)",
(userID, product_id))
# Commit the changes # Commit the changes
db_con.commit() db_con.commit()
# If you need results, you'd typically fetch them after a SELECT query
#results = cursor.fetchall()
#print(results)
except Exception as e: except Exception as e:
print(f"Error: {e}") logging.error(f"Error uploading recommendations: {str(e)}")
db_con.rollback() db_con.rollback()
finally: finally:
# Close the cursor and connection # Close the cursor and connection