43 Commits
noah2 ... main

Author SHA1 Message Date
Mann Patel
d6b5e8ff1b ref: update readme 2026-02-20 21:58:45 -07:00
Mann Patel
7ffba4c14c Update README.md 2025-04-30 13:46:04 -06:00
Mann Patel
c6d98b6d77 Update App.jsx 2025-04-22 18:47:20 -06:00
Mann Patel
b4ac53a8d0 code update 2025-04-22 18:01:10 -06:00
Mann Patel
b7937018e5 updating the transaction 2025-04-22 16:33:23 -06:00
Mann Patel
9d05adacfb removing unwanted table product_category 2025-04-22 14:27:32 -06:00
Mann Patel
bbddc8566a Enforce @ucalgary.ca emails for registration & require login after account creation 2025-04-22 12:18:10 -06:00
Mann Patel
4ba6dfa7be UI Color update 2025-04-21 22:46:39 -06:00
estherdev03
0a1db869f7 fix delete category 2025-04-21 17:03:09 -06:00
estherdev03
a8745ed94c Fix admin 2025-04-21 16:53:51 -06:00
Mann Patel
505f6cd134 updating alerts ui/ux 2025-04-21 01:19:39 -06:00
Mann Patel
53686bd71d Merge branch 'aaqil-Branch' into mann-Branch 2025-04-21 01:08:26 -06:00
Mann Patel
635ba76ed4 Merge branch 'main' into mann-Branch 2025-04-21 01:04:03 -06:00
Mann Patel
5228bf73c9 refactor for redundant code 2025-04-21 01:01:58 -06:00
aruhani
6d2f736541 Updated transaction route file 2025-04-20 23:52:28 -06:00
aruhani
7670bb2b99 Pushing new delete icon 2025-04-20 23:46:22 -06:00
noahnghg
3c7a1a876a fix duplicate products 2025-04-20 23:42:22 -06:00
noahnghg
46bd77025f fix the routers and UI 2025-04-20 23:36:58 -06:00
aruhani
eff9d9d91b Updating Image URL 2025-04-20 23:22:15 -06:00
aruhani
89f5032212 Added a delete function 2025-04-20 23:07:01 -06:00
noahnghg
691980bf7c fix the controller again to show every transaction 2025-04-20 23:03:04 -06:00
noahnghg
3ea45b5400 fix the transaction UI 2025-04-20 23:01:16 -06:00
aruhani
644db7707c Update Frontend Transaction 2025-04-20 22:33:12 -06:00
Mann Patel
8347689f6c uploads 2025-04-20 22:07:40 -06:00
Mann Patel
f82e111e39 uploads image folder 2025-04-20 22:07:02 -06:00
aruhani
d381ef7973 Transaction frontend update
Included a default message when no items are in the table
2025-04-20 22:03:04 -06:00
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
noahnghg
25f2c3d8af adding stuff to the onClick 2025-04-20 21:18:43 -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
aruhani
f7e4b49ac8 Backend for Transactions
Added in 3 files, for the backend
2025-04-20 10:42:52 -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
aruhani
90789b6fd4 Merge branch 'main' into aaqil 2025-04-19 00:00:44 -06:00
Mann Patel
dee6e3ce10 getCategory update 2025-04-18 22:09:04 -06:00
Mann Patel
d78b0c32e0 Merge branch 'mannBranch' into aaqil 2025-03-24 23:12:43 -06:00
aruhani
c2e13d56f2 Removing Password Temporarily 2025-03-24 22:43:06 -06:00
67 changed files with 3013 additions and 1505 deletions

View File

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

BIN
assets/CampusPlug.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

View File

@@ -0,0 +1,78 @@
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 {
if (id == "1") throw Error("You're not allowed to delete this category!");
const [updateResult] = await db.execute(
"UPDATE Product SET CategoryID = 1 WHERE CategoryID = ?",
[id]
);
const [result] = await db.execute(
`DELETE FROM Category WHERE CategoryID = ?`,
[id]
);
res.json({ message: "Delete category successfully!" });
} catch (error) {
res.json({
error: error.message || "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,44 @@ 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 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 +105,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 +341,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.removeAnyProduct = async (req, res) => {
const { id } = req.params;
console.log(id);
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,226 @@
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!" });
}
};
// Create a new transaction
exports.createTransaction = async (req, res) => {
const { userID, productID, date, paymentStatus } = req.body;
try {
// Check if the transaction already exists for the same user and product
const [existingTransaction] = await db.execute(
`SELECT TransactionID FROM Transaction WHERE UserID = ? AND ProductID = ?`,
[userID, productID],
);
if (existingTransaction.length > 0) {
return res.status(400).json({
success: false,
message: "Transaction already exists for this user and product",
});
}
// Format the date
const formattedDate = new Date(date)
.toISOString()
.slice(0, 19)
.replace("T", " ");
// Insert the new transaction
const [result] = await db.execute(
`INSERT INTO Transaction (UserID, ProductID, Date, PaymentStatus)
VALUES (?, ?, ?, ?)`,
[userID, productID, formattedDate, paymentStatus],
);
res.json({
success: true,
message: "Transaction created successfully",
transactionID: result.insertId,
});
} catch (error) {
console.error("Error creating transaction:", error);
res.status(500).json({ error: "Could not create transaction" });
}
};
// Get all transactions for a given product
exports.getTransactionsByProduct = async (req, res) => {
const { productID } = req.params;
try {
const [transactions] = await db.execute(
`SELECT
T.TransactionID,
T.UserID,
T.ProductID,
T.Date,
T.PaymentStatus,
P.Name AS ProductName,
MIN(I.URL) AS Image_URL
FROM Transaction T
JOIN Product P ON T.ProductID = P.ProductID
JOIN Image_URL I ON I.ProductID = T.ProductID
GROUP BY T.TransactionID, T.UserID, T.ProductID, T.Date, T.PaymentStatus, P.Name`,
);
res.json({
success: true,
transactions,
});
} catch (error) {
console.error("Error fetching transactions by product:", error);
res.status(500).json({ error: "Could not retrieve transactions" });
}
};
// Get all transactions for a given user
exports.getTransactionsByUser = async (req, res) => {
const { userID } = req.body;
try {
const [transactions] = await db.execute(
`SELECT
T.TransactionID,
T.UserID,
T.ProductID,
T.Date,
T.PaymentStatus,
P.Name AS ProductName,
MIN(I.URL) AS Image_URL
FROM Transaction T
JOIN Product P ON T.ProductID = P.ProductID
JOIN Image_URL I ON I.ProductID = T.ProductID
WHERE T.UserID = ?
GROUP BY
T.TransactionID,
T.UserID,
T.ProductID,
T.Date,
T.PaymentStatus,
P.Name;
`,
[userID],
);
res.json({
success: true,
transactions,
});
} catch (error) {
console.error("Error fetching transactions by user:", error);
res.status(500).json({ error: "Could not retrieve transactions" });
}
};
// Get all transactions in the system
exports.getAllTransactions = async (req, res) => {
try {
const [transactions] = await db.execute(
`SELECT
T.TransactionID,
T.UserID,
T.ProductID,
T.Date,
T.PaymentStatus,
P.Name AS ProductName,
MIN(I.URL) AS Image_URL
FROM Transaction T
JOIN Product P ON T.ProductID = P.ProductID
JOIN Image_URL I ON P.ProductID = I.ProductID
GROUP BY T.TransactionID, T.UserID, T.ProductID, T.Date, T.PaymentStatus, P.Name`,
);
res.json({
success: true,
transactions,
});
} catch (error) {
console.error("Error fetching all transactions:", error);
res.status(500).json({ error: "Could not retrieve transactions" });
}
};
// Delete a transaction
exports.deleteTransaction = async (req, res) => {
const { transactionID } = req.body;
try {
const [result] = await db.execute(
`DELETE FROM Transaction
WHERE TransactionID = ?`,
[transactionID],
);
if (result.affectedRows === 0) {
return res
.status(404)
.json({ success: false, message: "Transaction not found" });
}
res.json({
success: true,
message: "Transaction deleted successfully",
});
} catch (error) {
console.error("Error deleting transaction:", error);
res.status(500).json({ error: "Could not delete transaction" });
}
};
exports.updateTransactionStatus = async (req, res) => {
const { transactionID } = req.body;
try {
const [result] = await db.execute(
`UPDATE Transaction
SET PaymentStatus = 'completed'
WHERE TransactionID = ?;`,
[transactionID],
);
res.json({
success: true,
message: "Transaction updated successfully",
});
} catch (error) {
console.error("Error deleting transaction:", error);
res.status(500).json({ error: "Could not delete transaction" });
}
};

View File

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

@@ -10,6 +10,10 @@ 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 {
cleanupExpiredCodes, cleanupExpiredCodes,
@@ -42,7 +46,9 @@ app.use("/api/search", searchRouter);
app.use("/api/engine", recommendedRouter); app.use("/api/engine", recommendedRouter);
app.use("/api/history", history); app.use("/api/history", history);
app.use("/api/review", review); app.use("/api/review", review);
app.use("/api/transaction", transactionRouter);
app.use("/api/category", categoryRouter);
app.use("/api/transaction", transactionRouter);
// Set up a scheduler to run cleanup every hour // Set up a scheduler to run cleanup every hour
clean_up_time = 30 * 60 * 1000; clean_up_time = 30 * 60 * 1000;

View File

@@ -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,11 @@ const {
getAllProducts, getAllProducts,
getProductById, getProductById,
addProduct, addProduct,
removeProduct,
removeAnyProduct,
getProductWithPagination,
myProduct,
updateProduct,
} = require("../controllers/product"); } = require("../controllers/product");
const router = express.Router(); const router = express.Router();
@@ -20,8 +25,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("/any/:id", removeAnyProduct);
//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,30 @@
// routes/transaction.js
const express = require("express");
const {
createTransaction,
getTransactionsByProduct,
getTransactionsByUser,
getAllTransactions,
deleteTransaction,
getTransactionWithPagination,
removeTransation,
updateTransactionStatus,
} = require("../controllers/transaction");
const router = express.Router();
// logging middleware
router.use((req, res, next) => {
console.log(`Incoming ${req.method} ${req.originalUrl}`);
next();
});
router.post("/createTransaction", createTransaction);
router.get("/getTransactionsByProduct/:productID", getTransactionsByProduct);
router.post("/getTransactionsByUser", getTransactionsByUser);
router.post("/updateStatus", updateTransactionStatus);
router.post("/getAllTransactions", getAllTransactions);
router.delete("/deleteTransaction", deleteTransaction);
router.get("/getTransactions", getTransactionWithPagination);
router.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

@@ -6,4 +6,8 @@ const pool = mysql.createPool({
database: "Marketplace", database: "Marketplace",
}); });
// 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

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

View File

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

@@ -4,6 +4,7 @@ import {
Routes, Routes,
Route, Route,
Navigate, Navigate,
useLocation,
} from "react-router-dom"; } from "react-router-dom";
import Navbar from "./components/Navbar"; import Navbar from "./components/Navbar";
import Home from "./pages/Home"; import Home from "./pages/Home";
@@ -12,7 +13,10 @@ import Selling from "./pages/Selling";
import Transactions from "./pages/Transactions"; import Transactions from "./pages/Transactions";
import Favorites from "./pages/Favorites"; import Favorites from "./pages/Favorites";
import ProductDetail from "./pages/ProductDetail"; import ProductDetail from "./pages/ProductDetail";
import SearchPage from "./pages/SearchPage"; // Make sure to import the SearchPage import SearchPage from "./pages/SearchPage";
import Dashboard from "./pages/Dashboard"; // The single consolidated dashboard component
import DashboardNav from "./components/DashboardNav";
import { verifyIsAdmin } from "./api/admin";
function App() { function App() {
// Authentication state - initialize from localStorage if available // Authentication state - initialize from localStorage if available
@@ -30,7 +34,22 @@ 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([]);
// Admin state
const [isAdmin, setIsAdmin] = useState(false);
const [showAdminDashboard, setShowAdminDashboard] = useState(false);
// Check URL to determine if we're in admin mode
useEffect(() => {
// If URL contains /admin, set showAdminDashboard to true
if (window.location.pathname.includes("/admin")) {
setShowAdminDashboard(true);
}
}, []);
// New verification states // New verification states
const [verificationStep, setVerificationStep] = useState("initial"); // 'initial', 'code-sent', 'verifying' const [verificationStep, setVerificationStep] = useState("initial"); // 'initial', 'code-sent', 'verifying'
@@ -53,8 +72,46 @@ 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 () => {
setIsGeneratingRecommendations(true);
await new Promise((resolve) => setTimeout(resolve, 500));
setIsGeneratingRecommendations(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);
// Update URL without reloading page
window.history.pushState({}, "", "/admin");
};
const handleCloseAdminDashboard = () => {
setShowAdminDashboard(false);
// Update URL without reloading page
window.history.pushState({}, "", "/");
};
// Send verification code // Send verification code
const sendVerificationCode = async (userData) => { const sendVerificationCode = async (userData) => {
@@ -154,7 +211,6 @@ function App() {
// Complete signup // Complete signup
const completeSignUp = async (userData) => { const completeSignUp = async (userData) => {
try { try {
setIsLoading(true);
setError(""); setError("");
console.log("Completing signup for:", userData.email); console.log("Completing signup for:", userData.email);
@@ -182,6 +238,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,
@@ -189,20 +246,25 @@ function App() {
// Set authenticated user // Set authenticated user
setUser(newUser); setUser(newUser);
setIsAuthenticated(true); setIsSignUp(false);
//setIsAuthenticated(true);
// Save to localStorage to persist across refreshes // Save to localStorage to persist across refreshes
sessionStorage.setItem("isAuthenticated", "true"); sessionStorage.setItem("isAuthenticated", "true");
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");
@@ -231,12 +293,11 @@ function App() {
setError("Email and password are required"); setError("Email and password are required");
setIsLoading(false); setIsLoading(false);
return; return;
} else if (!formValues.email.endsWith("@ucalgary.ca")) {
setError("Please use your UCalgary email address (@ucalgary.ca)");
setIsLoading(false);
return;
} }
// else if (!formValues.email.endsWith("@ucalgary.ca")) {
// setError("Please use your UCalgary email address (@ucalgary.ca)");
// setIsLoading(false);
// return;
// }
try { try {
if (isSignUp) { if (isSignUp) {
// Handle Sign Up with verification // Handle Sign Up with verification
@@ -301,9 +362,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 +400,13 @@ function App() {
setUser(null); setUser(null);
setVerificationStep("initial"); setVerificationStep("initial");
setTempUserData(null); setTempUserData(null);
setRecommendations([]);
setShowAdminDashboard(false);
// 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 +434,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 +447,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 +468,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-emerald-600 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">
@@ -470,7 +538,7 @@ function App() {
id="name" id="name"
name="name" name="name"
placeholder="Enter your name" placeholder="Enter your name"
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-green-500 focus:ring-1 focus:ring-green-500" className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-emerald-600 focus:ring-1 focus:ring-emerald-600"
required={isSignUp} required={isSignUp}
/> />
</div> </div>
@@ -489,7 +557,7 @@ function App() {
id="ucid" id="ucid"
name="ucid" name="ucid"
placeholder="1234567" placeholder="1234567"
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-green-500 focus:ring-1 focus:ring-green-500" className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-emerald-600 focus:ring-1 focus:ring-emerald-600"
required={isSignUp} required={isSignUp}
/> />
</div> </div>
@@ -507,7 +575,7 @@ function App() {
id="email" id="email"
name="email" name="email"
placeholder="your.email@ucalgary.ca" placeholder="your.email@ucalgary.ca"
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-green-500 focus:ring-1 focus:ring-green-500" className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-emerald-600 focus:ring-1 focus:ring-emerald-600"
required required
/> />
</div> </div>
@@ -525,7 +593,7 @@ function App() {
id="phone" id="phone"
name="phone" name="phone"
placeholder="+1(123)456 7890" placeholder="+1(123)456 7890"
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-green-500 focus:ring-1 focus:ring-green-500" className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-emerald-600 focus:ring-1 focus:ring-emerald-600"
required={isSignUp} required={isSignUp}
/> />
</div> </div>
@@ -544,7 +612,7 @@ function App() {
id="address" id="address"
name="address" name="address"
placeholder="Your address" placeholder="Your address"
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-green-500 focus:ring-1 focus:ring-green-500" className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-emerald-600 focus:ring-1 focus:ring-emerald-600"
required={isSignUp} required={isSignUp}
/> />
</div> </div>
@@ -566,7 +634,7 @@ function App() {
? "Create a secure password" ? "Create a secure password"
: "Enter your password" : "Enter your password"
} }
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-green-500 focus:ring-1 focus:ring-green-500" className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-emerald-600 focus:ring-1 focus:ring-emerald-600"
required required
/> />
</div> </div>
@@ -575,7 +643,7 @@ function App() {
<button <button
type="submit" type="submit"
disabled={isLoading} disabled={isLoading}
className="w-full px-6 py-2 text-base font-medium text-white bg-green-500 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-400 focus:ring-offset-2 transition-colors disabled:bg-green-300" className="w-full px-6 py-2 text-base font-medium text-white bg-emerald-600 hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-emerald-400 focus:ring-offset-2 transition-colors disabled:bg-emerald-300"
> >
{isLoading {isLoading
? "Please wait..." ? "Please wait..."
@@ -602,7 +670,7 @@ function App() {
id="verificationCode" id="verificationCode"
name="verificationCode" name="verificationCode"
placeholder="Enter the 6-digit code" placeholder="Enter the 6-digit code"
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-green-500 focus:ring-1 focus:ring-green-500" className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-emerald-600 focus:ring-1 focus:ring-emerald-600"
required required
/> />
<p className="mt-1 text-xs text-gray-500"> <p className="mt-1 text-xs text-gray-500">
@@ -614,7 +682,7 @@ function App() {
<button <button
type="submit" type="submit"
disabled={isLoading} disabled={isLoading}
className="w-full px-6 py-2 text-base font-medium text-white bg-green-500 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-400 focus:ring-offset-2 transition-colors disabled:bg-green-300" className="w-full px-6 py-2 text-base font-medium text-white bg-emerald-600 hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-emerald-400 focus:ring-offset-2 transition-colors disabled:bg-emerald-300"
> >
{isLoading ? "Please wait..." : "Verify Code"} {isLoading ? "Please wait..." : "Verify Code"}
</button> </button>
@@ -632,7 +700,7 @@ function App() {
type="button" type="button"
onClick={handleResendCode} onClick={handleResendCode}
disabled={isLoading} disabled={isLoading}
className="text-sm text-green-500 hover:text-green-700" className="text-sm text-emerald-600 hover:text-emerald-700"
> >
Resend code Resend code
</button> </button>
@@ -650,7 +718,7 @@ function App() {
<button <button
onClick={toggleAuthMode} onClick={toggleAuthMode}
type="button" type="button"
className="text-green-500 font-medium hover:text-green-700" className="text-emerald-600 font-medium hover:text-emerald-700"
> >
{isSignUp ? "Sign in" : "Sign up"} {isSignUp ? "Sign in" : "Sign up"}
</button> </button>
@@ -674,16 +742,39 @@ function App() {
return ( return (
<Router> <Router>
{/* If admin dashboard should be shown */}
{showAdminDashboard ? (
<div className="flex">
<DashboardNav handleCloseAdminDashboard={handleCloseAdminDashboard} />
<Routes>
{/* Single admin route for consolidated dashboard */}
<Route path="/admin/*" element={<Dashboard />} />
{/* Any other path in admin mode should go to dashboard */}
<Route path="*" element={<Navigate to="/admin" />} />
</Routes>
</div>
) : (
/* Normal user interface */
<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 */}
<Route <Route
path="/login" path="/login"
element={isAuthenticated ? <Navigate to="/" /> : <LoginComponent />} element={
isAuthenticated ? <Navigate to="/" /> : <LoginComponent />
}
/> />
{/* Protected routes */} {/* Protected routes */}
<Route <Route
@@ -691,7 +782,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>
} }
@@ -761,6 +852,7 @@ function App() {
/> />
</Routes> </Routes>
</div> </div>
)}
</Router> </Router>
); );
} }

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

@@ -0,0 +1,120 @@
// api.js
import axios from "axios";
const client = axios.create({
baseURL: "http://localhost:3030/api",
});
// Users
export const getUsers = async (page = 1, limit = 10) => {
try {
const { data } = await client.get(
`/user/getUserWithPagination?page=${page}&limit=${limit}`
);
return { users: data.users, total: data.total };
} catch (error) {
return handleError(error);
}
};
export const removeUser = async (id) => {
try {
const { data } = await client.post(`/user/delete`, { userId: id });
return { message: data.message };
} catch (error) {
return handleError(error);
}
};
export const verifyIsAdmin = async (id) => {
try {
const { data } = await client.get(`/user/isAdmin/${id}`);
return { isAdmin: data.isAdmin };
} catch (error) {
return handleError(error);
}
};
// Products
export const getProducts = async (page = 1, limit = 10) => {
try {
const { data } = await client.get(
`/product/getProductWithPagination?limit=${limit}&page=${page}`
);
return { products: data.products, total: data.totalProd };
} catch (error) {
return handleError(error);
}
};
export const removeProduct = async (id) => {
try {
const { data } = await client.delete(`/product/any/${id}`);
return { message: data.message };
} catch (error) {
return handleError(error);
}
};
// Categories
export const getCategories = async (page, limit = 10) => {
try {
const { data } = await client.get(
`/category/getCategories?page=${page}&limit=${limit}`
);
return { data: data.data, total: data.total };
} catch (error) {
return handleError(error);
}
};
export const addCategory = async (name) => {
try {
const { data } = await client.post(`/category/addCategory`, { name });
return { message: data.message };
} catch (error) {
return handleError(error);
}
};
export const removeCategory = async (id) => {
try {
const { data } = await client.delete(`/category/${id}`);
if (data.error) throw Error(data.error);
return { message: data.message };
} catch (error) {
return handleError(error);
}
};
// Transactions
export const getTransactions = async (page = 1, limit = 10) => {
try {
const { data } = await client.get(
`/transaction/getTransactions?limit=${limit}&page=${page}`
);
return { transactions: data.data, total: data.total };
} catch (error) {
return handleError(error);
}
};
export const removeTransaction = async (id) => {
try {
const { data } = await client.delete(`/transaction/${id}`);
return { message: data.message };
} catch (error) {
return handleError(error);
}
};
// Shared Error Handler
const handleError = (error) => {
const { response } = error;
if (response?.data) return response.data;
return alert(error.message || error);
};
// Optional: export client if you want to use it elsewhere
export default client;

View File

@@ -0,0 +1,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-emerald-200", "text-emerald-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-emerald-700">
Category:
</label>
<input
type="text"
className="border border-emerald-700 ml-2 rounded-sm focus:bg-emerald-100 text-emerald-900"
name="category"
id="category"
onChange={handleChange}
value={category}
/>
<button type="submit" className="text-2xl pl-1 text-emerald-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,16 @@
import { FaArrowLeft } from "react-icons/fa";
export default function DashboardNav({ handleCloseAdminDashboard }) {
return (
<div className="w-48 min-w-[12rem] bg-gray-100 text-emerald-600 flex flex-col p-4 shadow-md">
<h2 className="text-lg font-semibold mb-4">Admin Dashboard</h2>
<button
onClick={handleCloseAdminDashboard}
className="flex items-center gap-2 text-sm font-medium hover:text-emerald-700 underline underline-offset-4 transition"
>
<FaArrowLeft className="text-xs mt-[1px]" />
Back to User Page
</button>
</div>
);
}

View File

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

View File

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

View File

@@ -119,7 +119,7 @@ const Favorites = () => {
</p> </p>
<Link <Link
to="/" to="/"
className="inline-block bg-emerald-500 hover:bg-emerald-600 text-white font-medium py-2 px-4" className="inline-block bg-emerald-600 hover:bg-emerald-700 text-white font-medium py-2 px-4"
> >
Browse Listings Browse Listings
</Link> </Link>
@@ -161,7 +161,7 @@ const Favorites = () => {
</button> </button>
</div> </div>
<p className="text-emerald-600 font-bold mt-1"> <p className="text-emerald-700 font-bold mt-1">
${product.price.toFixed(2)} ${product.price.toFixed(2)}
</p> </p>
@@ -200,56 +200,8 @@ const Favorites = () => {
)} )}
<footer className="bg-gray-800 text-white py-6 mt-12"> <footer className="bg-gray-800 text-white py-6 mt-12">
<div className="container mx-auto px-4"> <div className="border-t border-gray-700 text-center text-sm text-gray-400">
<div className="flex flex-col md:flex-row justify-between items-center"> <p>© 2025 Campus Marketplace. All rights reserved.</p>
<div className="mb-4 md:mb-0">
<h3 className="text-lg font-semibold mb-2">Campus Marketplace</h3>
<p className="text-gray-400 text-sm">
Your trusted university trading platform
</p>
</div>
<div className="flex space-x-6">
<div>
<h4 className="font-medium mb-2">Quick Links</h4>
<ul className="text-sm text-gray-400">
<li className="mb-1">
<Link to="/" className="hover:text-white transition">
Home
</Link>
</li>
<li className="mb-1">
<Link to="/selling" className="hover:text-white transition">
Sell an Item
</Link>
</li>
<li className="mb-1">
<Link
to="/favorites"
className="hover:text-white transition"
>
My Favorites
</Link>
</li>
</ul>
</div>
<div>
<h4 className="font-medium mb-2">Contact</h4>
<ul className="text-sm text-gray-400">
<li className="mb-1">support@campusmarket.com</li>
<li className="mb-1">University of Calgary</li>
</ul>
</div>
</div>
</div>
<div className="border-t border-gray-700 mt-6 pt-6 text-center text-sm text-gray-400">
<p>
© {new Date().getFullYear()} Campus Marketplace. All rights
reserved.
</p>
</div>
</div> </div>
</footer> </footer>
</div> </div>

View File

@@ -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-700 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-700 hover:bg-emerald-600 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-700 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,348 +353,61 @@ 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-700 hover:bg-emerald-700 text-white font-medium py-2 px-6 focus:outline-none focus:ring-2 focus:ring-emerald-400 transition-colors"
> >
Post an Item Post an Item
</button> </button>
</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">
<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="border-t border-gray-700 text-center text-sm text-gray-400">
<div className="flex flex-col md:flex-row justify-between items-center"> <p>© 2025 Campus Marketplace. All rights reserved.</p>
<div className="mb-4 md:mb-0">
<h3 className="text-lg font-semibold mb-2">Campus Marketplace</h3>
<p className="text-gray-400 text-sm">
Your trusted university trading platform
</p>
</div>
<div className="flex space-x-6">
<div>
<h4 className="font-medium mb-2">Quick Links</h4>
<ul className="text-sm text-gray-400">
<li className="mb-1">
<Link to="/" className="hover:text-white transition">
Home
</Link>
</li>
<li className="mb-1">
<Link to="/selling" className="hover:text-white transition">
Sell an Item
</Link>
</li>
<li className="mb-1">
<Link
to="/favorites"
className="hover:text-white transition"
>
My Favorites
</Link>
</li>
</ul>
</div>
<div>
<h4 className="font-medium mb-2">Contact</h4>
<ul className="text-sm text-gray-400">
<li className="mb-1">support@campusmarket.com</li>
<li className="mb-1">University of Calgary</li>
</ul>
</div>
</div>
</div>
<div className="border-t border-gray-700 mt-6 pt-6 text-center text-sm text-gray-400">
<p>
© {new Date().getFullYear()} Campus Marketplace. All rights
reserved.
</p>
</div>
</div> </div>
</footer> </footer>
</div> </div>

View File

@@ -1,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,12 +25,13 @@ 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([]);
const [showReviewForm, setShowReviewForm] = useState(false); const [showReviewForm, setShowReviewForm] = useState(false);
const [showAlert, setShowAlert] = useState(false); const [showAlert, setShowAlert] = useState(false);
const [showAlert1, setShowAlert1] = useState(false);
const storedUser = JSON.parse(sessionStorage.getItem("user")); const storedUser = JSON.parse(sessionStorage.getItem("user"));
const toggleFavorite = async (id) => { const toggleFavorite = async (id) => {
@@ -52,7 +52,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 +247,7 @@ const ProductDetail = () => {
if (loading.product) { if (loading.product) {
return ( return (
<div className="flex justify-center items-center h-screen"> <div className="flex justify-center items-center h-screen">
<div className="animate-spin h-32 w-32 border-t-2 border-green-500"></div> <div className="animate-spin h-32 w-32 border-t-2 border-emerald-700"></div>
</div> </div>
); );
} }
@@ -262,7 +261,7 @@ const ProductDetail = () => {
<p className="text-gray-600">{error.product}</p> <p className="text-gray-600">{error.product}</p>
<Link <Link
to="/" to="/"
className="mt-4 inline-block bg-green-500 text-white px-4 py-2 hover:bg-green-600" className="mt-4 inline-block bg-emerald-700 text-white px-4 py-2 hover:bg-emerald-700"
> >
Back to Listings Back to Listings
</Link> </Link>
@@ -279,7 +278,7 @@ const ProductDetail = () => {
<h2 className="text-2xl text-red-500 mb-4">Product Not Found</h2> <h2 className="text-2xl text-red-500 mb-4">Product Not Found</h2>
<Link <Link
to="/" to="/"
className="mt-4 inline-block bg-green-500 text-white px-4 py-2 hover:bg-green-600" className="mt-4 inline-block bg-emerald-700 text-white px-4 py-2 hover:bg-emerald-700"
> >
Back to Listings Back to Listings
</Link> </Link>
@@ -291,21 +290,27 @@ 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!"
onClose={() => setShowAlert(false)} onClose={() => setShowAlert(false)}
/> />
)} )}
{showAlert1 && (
<FloatingAlert
message="Product added to transaction!"
onClose={() => setShowAlert1(false)}
/>
)}
<div className="flex flex-col md:flex-row gap-8"> <div className="flex flex-col md:flex-row gap-8">
<div className="md:w-3/5"> <div className="md:w-3/5">
@@ -351,7 +356,7 @@ const ProductDetail = () => {
{product.images.map((image, index) => ( {product.images.map((image, index) => (
<div <div
key={index} key={index}
className={`bg-white border ${currentImage === index ? "border-green-500 border-2" : "border-gray-200"} min-w-[100px] cursor-pointer`} className={`bg-white border ${currentImage === index ? "border-emerald-700 border-2" : "border-gray-200"} min-w-[100px] cursor-pointer`}
onClick={() => selectImage(index)} onClick={() => selectImage(index)}
> >
<img <img
@@ -381,13 +386,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-700 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)
@@ -417,8 +422,40 @@ const ProductDetail = () => {
<div className="relative"> <div className="relative">
<button <button
onClick={() => setShowContactOptions(!showContactOptions)} onClick={async () => {
className="w-full bg-green-500 hover:bg-green-600 text-white font-medium py-3 px-4 mb-3" try {
// Create a transaction record
const transactionData = {
userID: storedUser.ID, // User ID from session storage
productID: product.ProductID, // Product ID from the product details
date: new Date().toISOString(), // Current date in ISO format
paymentStatus: "Pending", // Default payment status
};
const response = await fetch(
"http://localhost:3030/api/transaction/createTransaction",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(transactionData),
},
);
const result = await response.json();
if (result.success) {
setShowAlert1(true);
}
// Toggle contact options visibility
setShowContactOptions(!showContactOptions);
} catch (error) {
console.error("Error creating transaction:", error);
alert(`Error: ${error.message}`);
}
}}
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white font-medium py-3 px-4 mb-3"
> >
Contact Seller Contact Seller
</button> </button>
@@ -430,7 +467,7 @@ const ProductDetail = () => {
href={`tel:${product.SellerPhone}`} href={`tel:${product.SellerPhone}`}
className="flex items-center gap-2 p-3 hover:bg-gray-50 border-b border-gray-100" className="flex items-center gap-2 p-3 hover:bg-gray-50 border-b border-gray-100"
> >
<Phone className="h-5 w-5 text-green-500" /> <Phone className="h-5 w-5 text-emerald-700" />
<span>Call Seller</span> <span>Call Seller</span>
</a> </a>
)} )}
@@ -440,7 +477,7 @@ const ProductDetail = () => {
href={`mailto:${product.SellerEmail}`} href={`mailto:${product.SellerEmail}`}
className="flex items-center gap-2 p-3 hover:bg-gray-50" className="flex items-center gap-2 p-3 hover:bg-gray-50"
> >
<Mail className="h-5 w-5 text-green-500" /> <Mail className="h-5 w-5 text-emerald-700" />
<span>Email Seller</span> <span>Email Seller</span>
</a> </a>
)} )}
@@ -461,7 +498,10 @@ const ProductDetail = () => {
{product.SellerName || "Unknown Seller"} {product.SellerName || "Unknown Seller"}
</h3> </h3>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
Member since {product.SellerJoinDate || "N/A"} Product listed since{" "}
{product.Date
? new Date(product.Date).toLocaleDateString()
: "N/A"}
</p> </p>
</div> </div>
</div> </div>
@@ -477,7 +517,7 @@ const ProductDetail = () => {
<div className="bg-white border border-gray-200 p-6"> <div className="bg-white border border-gray-200 p-6">
{loading.reviews ? ( {loading.reviews ? (
<div className="flex justify-center py-8"> <div className="flex justify-center py-8">
<div className="animate-spin h-8 w-8 border-t-2 border-green-500"></div> <div className="animate-spin h-8 w-8 border-t-2 border-emerald-700"></div>
</div> </div>
) : error.reviews ? ( ) : error.reviews ? (
<div className="text-red-500 mb-4"> <div className="text-red-500 mb-4">
@@ -524,7 +564,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-700 hover:bg-emerald-700 text-white font-medium py-2 px-4"
> >
Write a Review Write a Review
</button> </button>
@@ -582,7 +622,7 @@ const ProductDetail = () => {
id="comment" id="comment"
value={reviewForm.comment} value={reviewForm.comment}
onChange={handleReviewInputChange} onChange={handleReviewInputChange}
className="w-full p-3 border border-gray-300 focus:outline-none focus:border-green-500" className="w-full p-3 border border-gray-300 focus:outline-none focus:border-emerald-700"
rows="4" rows="4"
required required
></textarea> ></textarea>
@@ -598,7 +638,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-700 text-white hover:bg-emerald-700"
disabled={loading.submitting} disabled={loading.submitting}
> >
{loading.submitting ? "Submitting..." : "Submit Review"} {loading.submitting ? "Submitting..." : "Submit Review"}

View File

@@ -146,7 +146,7 @@ const SearchPage = () => {
<div className="flex space-x-2"> <div className="flex space-x-2">
<button <button
onClick={applyFilters} onClick={applyFilters}
className="w-full bg-emerald-500 text-white p-3 hover:bg-emerald-600 transition-colors" className="w-full bg-emerald-600 text-white p-3 hover:bg-emerald-700 transition-colors"
> >
Apply Filters Apply Filters
</button> </button>
@@ -187,7 +187,7 @@ const SearchPage = () => {
<h3 className="text-lg font-medium text-gray-800"> <h3 className="text-lg font-medium text-gray-800">
{listing.title} {listing.title}
</h3> </h3>
<p className="text-emerald-600 font-semibold"> <p className="text-emerald-700 font-semibold">
${Number(listing.price).toFixed(2)} ${Number(listing.price).toFixed(2)}
</p> </p>
</div> </div>
@@ -196,58 +196,9 @@ const SearchPage = () => {
</div> </div>
</div> </div>
</div> </div>
{/* Footer - Added here */}
<footer className="bg-gray-800 text-white py-6 mt-12"> <footer className="bg-gray-800 text-white py-6 mt-12">
<div className="container mx-auto px-4"> <div className="border-t border-gray-700 text-center text-sm text-gray-400">
<div className="flex flex-col md:flex-row justify-between items-center"> <p>© 2025 Campus Marketplace. All rights reserved.</p>
<div className="mb-4 md:mb-0">
<h3 className="text-lg font-semibold mb-2">Campus Marketplace</h3>
<p className="text-gray-400 text-sm">
Your trusted university trading platform
</p>
</div>
<div className="flex space-x-6">
<div>
<h4 className="font-medium mb-2">Quick Links</h4>
<ul className="text-sm text-gray-400">
<li className="mb-1">
<Link to="/" className="hover:text-white transition">
Home
</Link>
</li>
<li className="mb-1">
<Link to="/selling" className="hover:text-white transition">
Sell an Item
</Link>
</li>
<li className="mb-1">
<Link
to="/favorites"
className="hover:text-white transition"
>
My Favorites
</Link>
</li>
</ul>
</div>
<div>
<h4 className="font-medium mb-2">Contact</h4>
<ul className="text-sm text-gray-400">
<li className="mb-1">support@campusmarket.com</li>
<li className="mb-1">University of Calgary</li>
</ul>
</div>
</div>
</div>
<div className="border-t border-gray-700 mt-6 pt-6 text-center text-sm text-gray-400">
<p>
© {new Date().getFullYear()} Campus Marketplace. All rights
reserved.
</p>
</div>
</div> </div>
</footer> </footer>
</div> </div>

View File

@@ -1,95 +1,284 @@
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, 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 [originalProduct, setOriginalProduct] = useState(null);
const [editingProduct, setEditingProduct] = useState({ const [editingProduct, setEditingProduct] = useState({
name: "", name: "",
price: "", price: "",
description: "", description: "",
categories: [], category: "",
status: "Unsold",
images: [], images: [],
}); });
// Simulate fetching products from API/database on component mount function reloadPage() {
useEffect(() => { var doctTimestamp = new Date(performance.timing.domLoading).getTime();
// This would be replaced with a real API call var now = Date.now();
const fetchProducts = async () => { if (now > doctTimestamp) {
// Mock data location.reload();
const mockProducts = [ }
{ }
id: "1",
name: "Vintage Camera",
price: "299.99",
description: "A beautiful vintage film camera in excellent condition",
categories: ["Electronics", "Art & Collectibles"],
status: "Unsold",
images: ["/public/Pictures/Dell1.jpg"],
},
{
id: "2",
name: "Leather Jacket",
price: "149.50",
description: "Genuine leather jacket, worn only a few times",
categories: ["Clothing"],
status: "Unsold",
images: [],
},
];
setProducts(mockProducts); // 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();
}, []);
// Fetch products from API/database on component mount
useEffect(() => {
const fetchProducts = async () => {
try {
// Replace with your actual API endpoint
const response = await fetch(
"http://localhost:3030/api/product/myProduct",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userID: storedUser.ID,
}),
},
);
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();
}, []); });
// Handle creating or updating a product const handleEditProduct = (product) => {
const handleSaveProduct = () => { setOriginalProduct(product);
if (editingProduct.id) {
// Update existing product const categoryName = getCategoryNameById(product.CategoryID);
setProducts(
products.map((p) => (p.id === editingProduct.id ? editingProduct : p)), setEditingProduct({
); ...product,
} else { category: categoryName || "", // Single category string
// Create new product images: product.images || [],
const newProduct = { });
...editingProduct,
id: Date.now().toString(), // Generate a temporary ID setShowForm(true);
}; };
setProducts([...products, newProduct]);
// Upload images to server and get their paths
const uploadImages = async (images) => {
console.log(images);
const uploadedImagePaths = [];
// Filter out only File objects (new images to upload)
const filesToUpload = images.filter((img) => img instanceof File);
for (const file of filesToUpload) {
// Create a FormData object to send the file
const formData = new FormData();
formData.append("image", file);
try {
// Send the file to your upload endpoint
const response = await fetch("http://localhost:3030/api/upload", {
method: "POST",
body: formData,
});
if (!response.ok) {
throw new Error(`Failed to upload image: ${file.name}`);
} }
const result = await response.json();
// Assuming the server returns the path where the file was saved
uploadedImagePaths.push(`/public/uploads/${file.name}`);
} catch (error) {
console.error("Error uploading image:", error);
// If upload fails, still add the expected path (this is a fallback)
uploadedImagePaths.push(`/public/uploads/${file.name}`);
}
}
// Also include any existing image URLs that are strings, not File objects
const existingImages = images.filter((img) => typeof img === "string");
if (existingImages.length > 0) {
uploadedImagePaths.push(...existingImages);
}
return uploadedImagePaths;
};
// Handle saving product with updated image logic
const handleSaveProduct = async () => {
if (!editingProduct.category) {
alert("Please select a category");
return;
}
try {
let imagePaths = [];
// Handle image uploads and get their paths
if (editingProduct.images && editingProduct.images.length > 0) {
imagePaths = await uploadImages(editingProduct.images);
} else if (originalProduct?.image_url) {
// If no new images but there was an original image URL
imagePaths = [originalProduct.image_url];
}
const categoryID =
categoryMapping[editingProduct.category] ||
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 : [],
};
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({
name: "", name: "",
price: "", price: "",
description: "", description: "",
categories: [], category: "",
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 deleting product:", 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
@@ -98,13 +287,20 @@ const Selling = () => {
name: "", name: "",
price: "", price: "",
description: "", description: "",
categories: [], category: "",
status: "Unsold",
images: [], images: [],
}); });
setShowForm(true); setShowForm(true);
}; };
// Handle category change
const handleCategoryChange = (e) => {
setEditingProduct({
...editingProduct,
category: e.target.value,
});
};
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">
@@ -112,7 +308,7 @@ const Selling = () => {
{!showForm && ( {!showForm && (
<button <button
onClick={handleAddProduct} onClick={handleAddProduct}
className="bg-emerald-600 text-white px-4 py-2 hover:bg-emerald-700" className="bg-emerald-700 text-white px-4 py-2 hover:bg-emerald-700"
> >
+ Add New Product + Add New Product
</button> </button>
@@ -120,12 +316,226 @@ 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-700 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-600 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-600 focus:outline-none"
/>
</div>
{/* Category - Single Selection Dropdown */}
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Category
</label>
<select
value={editingProduct.category || ""}
onChange={handleCategoryChange}
className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-600 focus:outline-none"
required
>
<option value="" disabled>
Select a category
</option>
{categories.map((category, index) => (
<option key={index} value={category}>
{category}
</option>
))}
</select>
{!editingProduct.category && (
<p className="text-xs text-gray-500 mt-1">
Please select a 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-600 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-700 font-medium">
Click to upload images (will be saved to /public/uploads)
</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={
typeof img === "string"
? img
: 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 &&
!(editingProduct.images || []).length && (
<div className="mt-3">
<p className="text-sm text-gray-600 mb-2">Current image:</p>
<div className="relative w-20 h-20 border border-gray-200 overflow-hidden">
<img
src={editingProduct.image_url}
alt="Current product"
className="w-full h-full object-cover"
/>
</div>
</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>
<button
onClick={handleSaveProduct}
className="bg-emerald-700 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 ? (
@@ -135,7 +545,7 @@ const Selling = () => {
</p> </p>
<button <button
onClick={handleAddProduct} onClick={handleAddProduct}
className="bg-emerald-600 text-white px-4 py-2 hover:bg-emerald-700" className="bg-emerald-700 text-white px-4 py-2 hover:bg-emerald-700"
> >
Create Your First Listing Create Your First Listing
</button> </button>
@@ -143,15 +553,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,61 +573,62 @@ 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-700 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) => {
className="text-emerald-600 hover:text-emerald-800 font-medium" e.stopPropagation();
e.preventDefault();
handleEditProduct(product);
}}
className="text-emerald-700 hover:text-emerald-800 font-medium"
> >
Edit Edit
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</Link>
))} ))}
</div> </div>
)} )}
</> </>
)} )}
<footer className="bg-gray-800 text-white py-6 mt-12">
<div className="border-t border-gray-700 text-center text-sm text-gray-400">
<p>© 2025 Campus Marketplace. All rights reserved.</p>
</div>
</footer>
</div> </div>
); );
}; };

View File

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

View File

@@ -1,8 +1,209 @@
import { useState } from "react"; import { useState, useEffect } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Calendar, CreditCard, Trash2 } from "lucide-react";
import FloatingAlert from "../components/FloatingAlert"; // adjust path if needed
const Transactions = () => { const Transactions = () => {
return <div></div>; const [transactions, setTransactions] = useState([]);
const [showAlert, setShowAlert] = useState(false);
const storedUser = JSON.parse(sessionStorage.getItem("user"));
function reloadPage() {
const docTimestamp = new Date(performance.timing.domLoading).getTime();
const now = Date.now();
if (now > docTimestamp) {
location.reload();
}
}
useEffect(() => {
const fetchTransactions = async () => {
try {
const response = await fetch(
"http://localhost:3030/api/transaction/getTransactionsByUser",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userID: storedUser.ID }),
},
);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const { transactions: txData } = await response.json();
if (!Array.isArray(txData)) return;
console.log(txData);
setTransactions(
txData.map((tx) => ({
id: tx.TransactionID,
productId: tx.ProductID,
name: tx.ProductName,
price: tx.Price != null ? parseFloat(tx.Price) : null,
image: tx.Image_URL,
date: tx.Date,
status: tx.PaymentStatus,
})),
);
} catch (error) {
console.error("Failed to fetch transactions:", error);
}
};
fetchTransactions();
}, []);
const deleteTransaction = async (id) => {
try {
const res = await fetch(
"http://localhost:3030/api/transaction/deleteTransaction",
{
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ transactionID: id }),
},
);
const data = await res.json();
if (data.success) {
setTransactions((prev) => prev.filter((tx) => tx.id !== id));
reloadPage();
} else {
console.error("Delete failed:", data.message);
}
} catch (err) {
console.error("Error deleting transaction:", err);
}
};
const updateTransaction = async (id) => {
try {
const res = await fetch(
"http://localhost:3030/api/transaction/updateStatus",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ transactionID: id }),
},
);
const data = await res.json();
if (data) {
setShowAlert(true);
reloadPage();
}
} catch (err) {
console.error("Error deleting transaction:", err);
}
};
const formatDate = (dateString) => {
const d = new Date(dateString);
return d.toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
});
};
return (
<div className="max-w-6xl mx-auto">
{showAlert && (
<FloatingAlert
message="Status Updated"
onClose={() => setShowAlert(false)}
/>
)}
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold text-gray-800">My Transactions</h1>
</div>
{transactions.length === 0 ? (
<div className="bg-white border border-gray-200 p-8 text-center">
<CreditCard className="h-12 w-12 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-medium text-gray-700 mb-2">
No transactions found
</h3>
<p className="text-gray-500 mb-4">
Once transactions are created, theyll appear here.
</p>
<Link
to="/"
className="inline-block bg-emerald-600 hover:bg-emerald-700 text-white font-medium py-2 px-4"
>
Browse Listings
</Link>
</div>
) : (
<>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{transactions.map((tx) => (
<div
key={tx.id}
className="relative border-2 border-gray-200 overflow-hidden hover:shadow-md transition-shadow"
>
<div className="absolute bottom-2 right-2 flex gap-2 z-10">
<button
onClick={(e) => {
e.preventDefault();
updateTransaction(tx.id);
}}
className="text-emerald-600 hover:text-emerald-700 text-sm font-medium"
>
Complete
</button>
<button
onClick={(e) => {
e.preventDefault();
deleteTransaction(tx.id);
}}
className="text-red-500 hover:text-red-600"
>
<Trash2 size={20} />
</button>
</div>
<Link to={`/product/${tx.productId}`}>
<div className="h-48 bg-gray-200 flex items-center justify-center">
<img
src={tx.image}
alt={tx.name}
className="w-full h-full object-cover"
/>
</div>
<div className="p-4">
<h3 className="text-lg font-semibold text-gray-800">
{tx.name}
</h3>
{tx.price !== null && (
<p className="text-emerald-700 font-bold mt-1">
${tx.price.toFixed(2)}
</p>
)}
<div className="flex items-center text-gray-500 text-sm mt-2">
<Calendar className="mr-1" size={16} />
{formatDate(tx.date)}
</div>
<p className="text-gray-600 text-sm mt-1">
Status: <span className="font-medium">{tx.status}</span>
</p>
</div>
</Link>
</div>
))}
</div>
<div className="mt-6 text-sm text-gray-500">
Showing {transactions.length}{" "}
{transactions.length === 1 ? "transaction" : "transactions"}
</div>
</>
)}
<footer className="bg-gray-800 text-white py-6 mt-12">
<div className="border-t border-gray-700 text-center text-sm text-gray-400">
<p>© 2025 Campus Marketplace. All rights reserved.</p>
</div>
</footer>
</div>
);
}; };
export default Transactions; export default Transactions;

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

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

View File

@@ -2,8 +2,6 @@
SET SET
FOREIGN_KEY_CHECKS = 0; FOREIGN_KEY_CHECKS = 0;
TRUNCATE TABLE Product_Category;
TRUNCATE TABLE Favorites; TRUNCATE TABLE Favorites;
TRUNCATE TABLE History; TRUNCATE TABLE History;
@@ -44,7 +42,7 @@ VALUES
( (
1, 1,
'John Doe', 'John Doe',
'john.doe@example.com', 'john.doe@ucalgary.ca',
'U123456', 'U123456',
'hashedpassword1', 'hashedpassword1',
'555-123-4567', '555-123-4567',
@@ -53,7 +51,7 @@ VALUES
( (
2, 2,
'Jane Smith', 'Jane Smith',
'jane.smith@example.com', 'jane.smith@ucalgary.ca',
'U234567', 'U234567',
'hashedpassword2', 'hashedpassword2',
'555-234-5678', '555-234-5678',
@@ -67,40 +65,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'), ('Other'),
(2, 'Electronics'), ('Textbooks'),
(3, 'Furniture'), ('Electronics'),
(4, 'Clothing'), ('Furniture'),
(5, 'Sports Equipment'), ('Clothing'),
(6, 'Musical Instruments'), ('Sports Equipment'),
(7, 'Art Supplies'), ('Musical Instruments'),
(8, 'Kitchen Appliances'), ('Art Supplies'),
(9, 'Gaming'), ('Kitchen Appliances'),
(10, 'Bicycles'), ('Gaming'),
(11, 'Computer Accessories'), ('Bicycles'),
(12, 'Stationery'), ('Computer Accessories'),
(13, 'Fitness Equipment'), ('Stationery'),
(14, 'Winter Sports'), ('Fitness Equipment'),
(15, 'Lab Equipment'), ('Winter Sports'),
(16, 'Camping Gear'), ('Lab Equipment'),
(17, 'School Supplies'), ('Camping Gear'),
(18, 'Office Furniture'), ('School Supplies'),
(19, 'Books (Non-textbook)'), ('Office Furniture'),
(20, 'Math & Science Resources'), ('Books (-textbook)'),
(21, 'Engineering Tools'), ('Math & Science Resources'),
(22, 'Backpacks & Bags'), ('Engineering Tools'),
(23, 'Audio Equipment'), ('Backpacks & Bags'),
(24, 'Dorm Essentials'), ('Audio Equipment'),
(25, 'Smartphones & Tablets'), ('Dorm Essentials'),
(26, 'Winter Clothing'), ('Smartphones & Tablets'),
(27, 'Photography Equipment'), ('Winter Clothing'),
(28, 'Event Tickets'), ('Photography Equipment'),
(29, 'Software Licenses'), ('Event Tickets'),
(30, 'Transportation (Car Pool)'); ('Software Licenses');
-- Insert Products -- Insert Products
INSERT INTO INSERT INTO
@@ -319,85 +318,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 INTO
Product_Category (ProductID, CategoryID)
VALUES
(1, 1),
(1, 17),
(1, 20), -- Calculus book: Textbooks, School Supplies, Math Resources
(2, 2),
(2, 11),
(2, 25), -- Laptop: Electronics, Computer Accessories, Smartphones & Tablets
(3, 3),
(3, 18),
(3, 24), -- Desk: Furniture, Office Furniture, Dorm Essentials
(4, 4),
(4, 26), -- Hoodie: Clothing, Winter Clothing
(5, 5),
(5, 13), -- Basketball: Sports Equipment, Fitness Equipment
(6, 6),
(6, 23), -- Guitar: Musical Instruments, Audio Equipment
(7, 1),
(7, 15),
(7, 20), -- Physics book: Textbooks, Lab Equipment, Math & Science Resources
(8, 8),
(8, 24), -- Mini Fridge: Kitchen Appliances, Dorm Essentials
(9, 9),
(9, 2), -- PS5 Controller: Gaming, Electronics
(10, 10),
(10, 5),
(10, 13), -- Mountain Bike: Bicycles, Sports Equipment, Fitness Equipment
(11, 11),
(11, 2), -- Mouse: Computer Accessories, Electronics
(12, 15),
(12, 17), -- Lab Coat: Lab Equipment, School Supplies
(13, 12),
(13, 17),
(13, 20), -- Calculator: Stationery, School Supplies, Math & Science Resources
(14, 13),
(14, 5), -- Yoga Mat: Fitness Equipment, Sports Equipment
(15, 26),
(15, 4),
(15, 14), -- Winter Jacket: Winter Clothing, Clothing, Winter Sports
(16, 1),
(16, 17),
(16, 19), -- CS Book: Textbooks, School Supplies, Books (Non-textbook)
(17, 24),
(17, 2), -- Desk Lamp: Dorm Essentials, Electronics
(18, 12),
(18, 17),
(18, 20), -- Scientific Calculator: Stationery, School Supplies, Math & Science
(19, 23),
(19, 2),
(19, 24), -- Bluetooth Speaker: Audio Equipment, Electronics, Dorm Essentials
(20, 22),
(20, 17),
(20, 24);
-- Insert History records -- Insert History records
INSERT INTO INSERT INTO
@@ -427,7 +370,6 @@ VALUES
(1, 5), -- User 4 likes Basketball (1, 5), -- User 4 likes Basketball
(2, 8); (2, 8);
-- User 5 likes Mini Fridge
-- Insert Transactions -- Insert Transactions
INSERT INTO INSERT INTO
Transaction ( Transaction (
@@ -452,4 +394,5 @@ VALUES
1, 1,
'This is a great fake product! Totally recommend it.', 'This is a great fake product! Totally recommend it.',
5, 5,
); '2024-10-02 16:00:00'
)

View File

@@ -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
); );
@@ -36,9 +36,9 @@ CREATE TABLE Product (
StockQuantity INT, StockQuantity INT,
UserID INT, UserID INT,
Description TEXT, Description TEXT,
CategoryID INT NOT NULL, CategoryID INT,
Date DATETIME DEFAULT CURRENT_TIMESTAMP, Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID), FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID) FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID)
); );
@@ -46,7 +46,7 @@ CREATE TABLE Product (
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,19 +60,19 @@ CREATE TABLE Review (
AND Rating <= 5 AND Rating <= 5
), ),
Date DATETIME DEFAULT CURRENT_TIMESTAMP, Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID), FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
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)
CREATE TABLE Transaction ( CREATE TABLE Transaction (
TransactionID INT PRIMARY KEY, TransactionID INT AUTO_INCREMENT PRIMARY KEY,
UserID INT, UserID INT,
ProductID INT, ProductID INT,
Date DATETIME DEFAULT CURRENT_TIMESTAMP, Date DATETIME DEFAULT CURRENT_TIMESTAMP,
PaymentStatus VARCHAR(50), PaymentStatus VARCHAR(50),
FOREIGN KEY (UserID) REFERENCES User (UserID), FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE
); );
-- Recommendation Entity (Many-to-One with User, Many-to-One with Product) -- Recommendation Entity (Many-to-One with User, Many-to-One with Product)
@@ -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,20 +100,11 @@ 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)
); );
-- Product-Category Junction Table (Many-to-Many)
CREATE TABLE Product_Category (
ProductID INT,
CategoryID INT,
PRIMARY KEY (ProductID, CategoryID),
FOREIGN KEY (ProductID) REFERENCES Product (ProductID),
FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID)
);
-- Login Authentication table -- Login Authentication table
CREATE TABLE AuthVerification ( CREATE TABLE AuthVerification (
UserID INT AUTO_INCREMENT PRIMARY KEY, UserID INT AUTO_INCREMENT PRIMARY KEY,

View File

@@ -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,47 +14,109 @@ 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: print(f"Deleted existing recommendations for user {user_id}")
#Product ID starts form index 1 cursor.execute(f"DELETE FROM Recommendation WHERE UserID = {user_id}")
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()
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=0, exclude_list=None):
#print(results)
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:
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=5):
db_con = database()
cursor = db_con.cursor()
try:
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 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)
finally:
cursor.close()
db_con.close()
def has_user_history_or_recommendations(user_id):
db_con = database()
cursor = db_con.cursor()
try:
cursor.execute(f"SELECT COUNT(*) FROM History WHERE UserID = {user_id}" )
history_count = cursor.fetchone()[0]
cursor.execute(f"SELECT COUNT(*) FROM Recommendation WHERE UserID = {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()
select_clause = "SELECT p.ProductID" select_clause = "SELECT p.ProductID"
for category in categories: for category in categories:
category_id = category[0] category_id = category[0]
select_clause += f", MAX(CASE WHEN pc.CategoryID = {category_id} THEN 1 ELSE 0 END) AS `Cat_{category_id}`" select_clause += f", MAX(CASE WHEN p.CategoryID = {category_id} THEN 1 ELSE 0 END) AS `Cat_{category_id}`"
final_query = f""" final_query = f"""
{select_clause} {select_clause}
FROM Product p FROM Product p
LEFT JOIN Product_Category pc ON p.ProductID = pc.ProductID
LEFT JOIN Category c ON pc.CategoryID = c.CategoryID
GROUP BY p.ProductID; GROUP BY p.ProductID;
""" """
@@ -62,33 +124,39 @@ 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)
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
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]
select_clause += f", MAX(CASE WHEN pc.CategoryID = {category_id} THEN 1 ELSE 0 END) AS `Cat_{category_id}`" select_clause += f", MAX(CASE WHEN p.CategoryID = {category_id} THEN 1 ELSE 0 END) AS `Cat_{category_id}`"
final_query = f""" final_query = f"""
{select_clause} {select_clause}
FROM Product p FROM Product p
LEFT JOIN Product_Category pc ON p.ProductID = pc.ProductID WHERE p.ProductID IN (SELECT ProductID FROM History WHERE UserID = {user_id})
LEFT JOIN Category c ON pc.CategoryID = c.CategoryID
where p.ProductID in (select ProductID from History where UserID = {user_id})
GROUP BY p.ProductID; GROUP BY p.ProductID;
""" """
@@ -103,58 +171,71 @@ 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:
# Get all products and user history with their category vectors delete_user_recommendations(user_id)
all_products = get_all_products()
if not has_user_history_or_recommendations(user_id):
random_recs = get_random_products(top_n)
recommendation_upload(user_id, random_recs)
additional_random = get_random_products(5, random_recs)
recommendation_upload(user_id, additional_random)
return random_recs + additional_random
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) popular_recs = get_popular_products(top_n)
# Calculate similarity between all products and user history recommendation_upload(user_id, popular_recs)
user_profile = np.mean(user_history, axis=0) # Average user preferences
similarities = cosine_similarity([user_profile], all_products) additional_random = get_random_products(5, popular_recs)
# finds the indices of the top N products that have the highest recommendation_upload(user_id, additional_random)
# cosine similarity with the user's profile and sorted from most similar to least similar.
return popular_recs + additional_random
user_profile = np.mean(user_history, axis=0)
similarities = cosine_similarity([user_profile], all_product_features)
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 recommended_product_ids = [all_product_ids[i] for i in product_indices]
recommended_products = [all_products[i][0] for i in product_indices] # Product IDs print(recommended_product_ids)
# Upload the recommendations to the database recommendation_upload(user_id, recommended_product_ids)
history_upload(user_id, product_indices) # Pass the indices directly to history_upload
additional_random = get_random_products(5, recommended_product_ids)
recommendation_upload(user_id, additional_random)
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 random_products = get_random_products(top_n + 5)
return random_products
def history_upload(userID, anrr): def recommendation_upload(userID, products):
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 cursor.execute("INSERT INTO Recommendation (UserID, RecommendedProductID) VALUES (%s, %s)",
item_value = item + 1 (userID, product_id))
print(item_value)
# Use parameterized queries to prevent SQL injection
cursor.execute(f"INSERT INTO Recommendation (UserID, RecommendedProductID) VALUES ({userID}, {item_value});")
# 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
cursor.close() cursor.close()
db_con.close() db_con.close()