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
68 changed files with 3013 additions and 1542 deletions

View File

@@ -1,6 +1,9 @@
### Some 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```.
### Ground rules
<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
@@ -19,24 +22,19 @@
```
---
### Recommendation
1. `cd recommendation-engine` into the dir and then type command
### Recommendation system
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
1. python3 server.py #Start The Server
```
---
### Recommendation system
1. Install the dependencies
```Bash
pip install mysql.connector
```
### Database
1. MySql Version 9.2.0
2. To Create the DataBase use the command bellow:
```Bash
1. mysql -u root
2. \. PathToYour/Schema.sql
3. \. PathToYour/Init-Data.sql
2. \. PathTo/Schema.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) => {
const { userID, productID } = req.body;
console.log(userID);
try {
// Use parameterized query to prevent SQL injection
const [result] = await db.execute(
`INSERT INTO Favorites (UserID, ProductID) VALUES (?, ?)`,
[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) => {
const { userID } = req.body;
@@ -211,32 +341,61 @@ exports.getProductById = async (req, res) => {
}
};
// db_con.query(
// "SELECT ProductID FROM product WHERE ProductID = ?",
// [productID],
// (err, results) => {
// if (err) {
// console.error("Error checking product:", err);
// return res.json({ error: "Database error" });
// }
exports.getProductWithPagination = async (req, res) => {
const limit = +req.query.limit;
const page = +req.query.page;
// if (results.length === 0) {
// return res.json({ error: "Product does not exist" });
// }
// },
// );
const offset = (page - 1) * limit;
// db_con.query(
// "INSERT INTO Favorites (UserID, ProductID) VALUES (?, ?)",
// [userID, productID],
// (err, result) => {
// if (err) {
// console.error("Error adding favorite product:", err);
// return res.json({ error: "Could not add favorite product" });
// }
// res.json({
// success: true,
// message: "Product added to favorites successfully",
// });
// },
// );
try {
const [data, fields] = await db.execute(
`
SELECT
P.ProductID,
P.Name AS ProductName,
P.Price,
P.Date AS DateUploaded,
U.Name AS SellerName,
MIN(I.URL) AS ProductImage,
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

@@ -3,43 +3,7 @@ const db = require("../utils/database");
// TODO: Get the recommondaed product given the userID
exports.RecommondationByUserId = async (req, res) => {
const { id } = req.body;
try {
const [recommendation] = await db.execute(
"select * from Recommendation where UserID = ? limit 1", [id]
);
if (recommendation.length === 0) {
const [data] = await db.execute(
`
SELECT
P.ProductID,
P.Name AS ProductName,
P.Price,
P.Date AS DateUploaded,
U.Name AS SellerName,
I.URL AS ProductImage,
C.Name AS Category
FROM Product P
JOIN Image_URL I ON P.ProductID = I.ProductID
JOIN User U ON P.UserID = U.UserID
JOIN Category C ON P.CategoryID = C.CategoryID
JOIN (
SELECT ProductID
FROM Product
ORDER BY RAND()
LIMIT 5
) RandomProducts ON P.ProductID = RandomProducts.ProductID
WHERE I.URL IS NOT NULL;
`
);
console.log("Random products for new user:", data);
return res.json({
success: true,
message: "Random products fetched for new user",
data,
});
} else{
const [data, fields] = await db.execute(
`
WITH RankedImages AS (
@@ -79,7 +43,6 @@ exports.RecommondationByUserId = async (req, res) => {
message: "Products fetched successfully",
data,
});
}
} catch (error) {
console.error("Error finding products:", error);
return res.status(500).json({

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
const verificationCode = crypto.randomInt(100000, 999999).toString();
console.log(
`Generated verification code for ${email}: ${verificationCode}`,
`Generated verification code for ${email}: ${verificationCode}`
);
// Check if email already exists in verification table
const [results, fields] = await db.execute(
"SELECT * FROM AuthVerification WHERE Email = ?",
[email],
[email]
);
if (results.length > 0) {
@@ -27,7 +27,7 @@ exports.sendVerificationCode = async (req, res) => {
const [result] = await db.execute(
`UPDATE AuthVerification SET VerificationCode = ?, Authenticated = FALSE, Date = CURRENT_TIMESTAMP
WHERE Email = ?`,
[verificationCode, email],
[verificationCode, email]
);
// Send email and respond
@@ -37,7 +37,7 @@ exports.sendVerificationCode = async (req, res) => {
// Insert new record
const [result] = await db.execute(
"INSERT INTO AuthVerification (Email, VerificationCode, Authenticated) VALUES (?, ?, FALSE)",
[email, verificationCode],
[email, verificationCode]
);
// Send email and respond
await sendVerificationEmail(email, verificationCode);
@@ -62,7 +62,7 @@ exports.verifyCode = async (req, res) => {
// Check verification code
const [results, fields] = await db.execute(
"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) {
console.log(`Invalid or expired verification code for email ${email}`);
@@ -76,7 +76,7 @@ exports.verifyCode = async (req, res) => {
// Mark as authenticated
const [result] = await db.execute(
"UPDATE AuthVerification SET Authenticated = TRUE WHERE Email = ?",
[email],
[email]
);
res.json({
success: true,
@@ -95,7 +95,7 @@ exports.completeSignUp = async (req, res) => {
try {
const [results, fields] = await db.execute(
`SELECT * FROM AuthVerification WHERE Email = ? AND Authenticated = 1;`,
[data.email],
[data.email]
);
if (results.length === 0) {
@@ -105,20 +105,20 @@ exports.completeSignUp = async (req, res) => {
// Create the user
const [createResult] = await db.execute(
`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
const [insertResult] = await db.execute(
`INSERT INTO UserRole (UserID, Client, Admin)
VALUES (LAST_INSERT_ID(), ${data.client || true}, ${
data.admin || false
})`,
data.admin || false
})`
);
// Delete verification record
const [deleteResult] = await db.execute(
`DELETE FROM AuthVerification WHERE Email = '${data.email}'`,
`DELETE FROM AuthVerification WHERE Email = '${data.email}'`
);
res.json({
@@ -310,7 +310,7 @@ exports.deleteUser = async (req, res) => {
// Delete from UserRole first (assuming foreign key constraint)
const [result1] = await db.execute(
"DELETE FROM UserRole WHERE UserID = ?",
[userId],
[userId]
);
// Then delete from User table
@@ -328,3 +328,38 @@ exports.deleteUser = async (req, res) => {
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 review = require("./routes/review");
const categoryRouter = require("./routes/category");
const transactionRouter = require("./routes/transaction");
const { generateEmailTransporter } = require("./utils/mail");
const {
cleanupExpiredCodes,
@@ -42,10 +46,12 @@ app.use("/api/search", searchRouter);
app.use("/api/engine", recommendedRouter);
app.use("/api/history", history);
app.use("/api/review", review);
app.use("/api/transaction", transactionRouter);
app.use("/api/category", categoryRouter);
app.use("/api/transaction", transactionRouter);
// Set up a scheduler to run cleanup every hour
clean_up_time = 30*60*1000;
clean_up_time = 30 * 60 * 1000;
setInterval(cleanupExpiredCodes, clean_up_time);
app.listen(3030, () => {

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,
getProductById,
addProduct,
removeProduct,
removeAnyProduct,
getProductWithPagination,
myProduct,
updateProduct,
} = require("../controllers/product");
const router = express.Router();
@@ -20,8 +25,18 @@ router.post("/addFavorite", addFavorite);
router.post("/getFavorites", getFavorites);
router.post("/delFavorite", removeFavorite);
router.post("/delProduct", removeProduct);
router.post("/myProduct", myProduct);
router.post("/addProduct", addProduct);
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.put("/update/:productId", updateProduct);
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,
deleteUser,
doLogin,
isAdmin,
getUsersWithPagination,
} = require("../controllers/user");
const router = express.Router();
@@ -36,4 +38,10 @@ router.post("/update", updateUser);
//Delete A uses Data:
router.post("/delete", deleteUser);
//Check admin status
router.get("/isAdmin/:id", isAdmin);
//Fetch user with pagination
router.get("/getUserWithPagination", getUsersWithPagination);
module.exports = router;

View File

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

View File

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

View File

@@ -13,6 +13,7 @@
"lucide-react": "^0.477.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-icons": "^5.5.0",
"react-router-dom": "^7.2.0"
},
"devDependencies": {
@@ -4369,6 +4370,15 @@
"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": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",

View File

@@ -15,6 +15,7 @@
"lucide-react": "^0.477.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-icons": "^5.5.0",
"react-router-dom": "^7.2.0"
},
"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,
Route,
Navigate,
useLocation,
} from "react-router-dom";
import Navbar from "./components/Navbar";
import Home from "./pages/Home";
@@ -12,7 +13,10 @@ import Selling from "./pages/Selling";
import Transactions from "./pages/Transactions";
import Favorites from "./pages/Favorites";
import ProductDetail from "./pages/ProductDetail";
import SearchPage from "./pages/SearchPage"; // Make sure to import the SearchPage
import 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() {
// Authentication state - initialize from localStorage if available
@@ -30,7 +34,22 @@ function App() {
const [error, setError] = useState("");
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
const [verificationStep, setVerificationStep] = useState("initial"); // 'initial', 'code-sent', 'verifying'
@@ -53,8 +72,46 @@ function App() {
}, []);
useEffect(() => {
sendSessionDataToServer();
}, []);
if (isAuthenticated && user) {
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
const sendVerificationCode = async (userData) => {
@@ -154,7 +211,6 @@ function App() {
// Complete signup
const completeSignUp = async (userData) => {
try {
setIsLoading(true);
setError("");
console.log("Completing signup for:", userData.email);
@@ -182,6 +238,7 @@ function App() {
if (result.success) {
// Create user object from API response
const newUser = {
ID: result.userID || result.ID,
name: result.name || userData.name,
email: result.email || userData.email,
UCID: result.UCID || userData.ucid,
@@ -189,20 +246,25 @@ function App() {
// Set authenticated user
setUser(newUser);
setIsAuthenticated(true);
setIsSignUp(false);
//setIsAuthenticated(true);
// Save to localStorage to persist across refreshes
sessionStorage.setItem("isAuthenticated", "true");
sessionStorage.setItem("user", JSON.stringify(newUser));
// After successful signup, send session data to server
sendSessionDataToServer(); // Call it after signup
sendSessionDataToServer();
// Reset verification steps
setVerificationStep("initial");
setTempUserData(null);
console.log("Signup completed successfully");
// Generate recommendations for the new user
generateProductRecommendations();
return true;
} else {
setError(result.message || "Failed to complete signup");
@@ -231,12 +293,11 @@ function App() {
setError("Email and password are required");
setIsLoading(false);
return;
} else if (!formValues.email.endsWith("@ucalgary.ca")) {
setError("Please use your UCalgary email address (@ucalgary.ca)");
setIsLoading(false);
return;
}
// else if (!formValues.email.endsWith("@ucalgary.ca")) {
// setError("Please use your UCalgary email address (@ucalgary.ca)");
// setIsLoading(false);
// return;
// }
try {
if (isSignUp) {
// Handle Sign Up with verification
@@ -301,9 +362,11 @@ function App() {
sessionStorage.setItem("isAuthenticated", "true");
sessionStorage.setItem("user", JSON.stringify(userObj));
sessionStorage.getItem("user");
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 {
// Show error message for invalid credentials
setError("Invalid email or password");
@@ -337,11 +400,13 @@ function App() {
setUser(null);
setVerificationStep("initial");
setTempUserData(null);
setRecommendations([]);
setShowAdminDashboard(false);
// Clear localStorage
//
sessionStorage.removeItem("user");
sessionStorage.removeItem("isAuthenticated");
sessionStorage.removeItem("userRecommendations");
console.log("User logged out");
};
@@ -369,8 +434,6 @@ function App() {
try {
// Retrieve data from sessionStorage
const user = JSON.parse(sessionStorage.getItem("user"));
// const isAuthenticated =
// sessionStorage.getItem("isAuthenticated") === "true";
if (!user || !isAuthenticated) {
console.log("User is not authenticated");
@@ -384,8 +447,6 @@ function App() {
isAuthenticated,
};
console.log("Sending user data to the server:", requestData);
// Send data to Python server (replace with your actual server URL)
const response = await fetch("http://0.0.0.0:5000/api/user/session", {
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
const LoginComponent = () => (
<div className="flex h-screen bg-white">
@@ -470,7 +538,7 @@ function App() {
id="name"
name="name"
placeholder="Enter your name"
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-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}
/>
</div>
@@ -489,7 +557,7 @@ function App() {
id="ucid"
name="ucid"
placeholder="1234567"
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-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}
/>
</div>
@@ -507,7 +575,7 @@ function App() {
id="email"
name="email"
placeholder="your.email@ucalgary.ca"
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-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
/>
</div>
@@ -525,7 +593,7 @@ function App() {
id="phone"
name="phone"
placeholder="+1(123)456 7890"
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-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}
/>
</div>
@@ -544,7 +612,7 @@ function App() {
id="address"
name="address"
placeholder="Your address"
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-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}
/>
</div>
@@ -566,7 +634,7 @@ function App() {
? "Create a secure password"
: "Enter your password"
}
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-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
/>
</div>
@@ -575,7 +643,7 @@ function App() {
<button
type="submit"
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..."
@@ -602,7 +670,7 @@ function App() {
id="verificationCode"
name="verificationCode"
placeholder="Enter the 6-digit code"
className="w-full px-4 py-2 border border-gray-300 bg-white text-gray-800 focus:outline-none focus:border-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
/>
<p className="mt-1 text-xs text-gray-500">
@@ -614,7 +682,7 @@ function App() {
<button
type="submit"
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"}
</button>
@@ -632,7 +700,7 @@ function App() {
type="button"
onClick={handleResendCode}
disabled={isLoading}
className="text-sm text-green-500 hover:text-green-700"
className="text-sm text-emerald-600 hover:text-emerald-700"
>
Resend code
</button>
@@ -650,7 +718,7 @@ function App() {
<button
onClick={toggleAuthMode}
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"}
</button>
@@ -674,93 +742,117 @@ function App() {
return (
<Router>
<div className="min-h-screen bg-gray-50">
{/* Only show navbar when authenticated */}
{isAuthenticated && (
<Navbar onLogout={handleLogout} userName={user?.name} />
)}
<Routes>
{/* Public routes */}
<Route
path="/login"
element={isAuthenticated ? <Navigate to="/" /> : <LoginComponent />}
/>
{/* Protected routes */}
<Route
path="/"
element={
<ProtectedRoute>
<div className="container mx-auto px-4 py-6">
<Home />
</div>
</ProtectedRoute>
}
/>
<Route
path="/product/:id"
element={
<ProtectedRoute>
<ProductDetail />
</ProtectedRoute>
}
/>
<Route
path="/search"
element={
<ProtectedRoute>
<div className="container mx-auto px-4 py-6">
<SearchPage />
</div>
</ProtectedRoute>
}
/>
<Route
path="/settings"
element={
<ProtectedRoute>
<div className="container mx-auto px-4 py-6">
<Settings />
</div>
</ProtectedRoute>
}
/>
<Route
path="/selling"
element={
<ProtectedRoute>
<div className="container mx-auto px-4 py-6">
<Selling />
</div>
</ProtectedRoute>
}
/>
<Route
path="/transactions"
element={
<ProtectedRoute>
<div className="container mx-auto px-4 py-6">
<Transactions />
</div>
</ProtectedRoute>
}
/>
<Route
path="/favorites"
element={
<ProtectedRoute>
<div className="container mx-auto px-4 py-6">
<Favorites />
</div>
</ProtectedRoute>
}
/>
{/* Redirect to login for any unmatched routes */}
<Route
path="*"
element={<Navigate to={isAuthenticated ? "/" : "/login"} />}
/>
</Routes>
</div>
{/* 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">
{/* Show loading overlay when generating recommendations */}
{isGeneratingRecommendations && <LoadingOverlay />}
{/* Only show navbar when authenticated */}
{isAuthenticated && (
<Navbar
isAdmin={isAdmin}
onLogout={handleLogout}
userName={user?.name}
handleShowAdminDashboard={handleShowAdminDashboard}
/>
)}
<Routes>
{/* Public routes */}
<Route
path="/login"
element={
isAuthenticated ? <Navigate to="/" /> : <LoginComponent />
}
/>
{/* Protected routes */}
<Route
path="/"
element={
<ProtectedRoute>
<div className="container mx-auto px-4 py-6">
<Home recommendations={recommendations} />
</div>
</ProtectedRoute>
}
/>
<Route
path="/product/:id"
element={
<ProtectedRoute>
<ProductDetail />
</ProtectedRoute>
}
/>
<Route
path="/search"
element={
<ProtectedRoute>
<div className="container mx-auto px-4 py-6">
<SearchPage />
</div>
</ProtectedRoute>
}
/>
<Route
path="/settings"
element={
<ProtectedRoute>
<div className="container mx-auto px-4 py-6">
<Settings />
</div>
</ProtectedRoute>
}
/>
<Route
path="/selling"
element={
<ProtectedRoute>
<div className="container mx-auto px-4 py-6">
<Selling />
</div>
</ProtectedRoute>
}
/>
<Route
path="/transactions"
element={
<ProtectedRoute>
<div className="container mx-auto px-4 py-6">
<Transactions />
</div>
</ProtectedRoute>
}
/>
<Route
path="/favorites"
element={
<ProtectedRoute>
<div className="container mx-auto px-4 py-6">
<Favorites />
</div>
</ProtectedRoute>
}
/>
{/* Redirect to login for any unmatched routes */}
<Route
path="*"
element={<Navigate to={isAuthenticated ? "/" : "/login"} />}
/>
</Routes>
</div>
)}
</Router>
);
}

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]);
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}
</div>
);

View File

@@ -3,7 +3,7 @@ import { Link, useNavigate } from "react-router-dom";
import UserDropdown from "./UserDropdown";
import { Search, Heart } from "lucide-react";
const Navbar = ({ onLogout, userName }) => {
const Navbar = ({ onLogout, userName, isAdmin, handleShowAdminDashboard }) => {
const [searchQuery, setSearchQuery] = useState("");
const navigate = useNavigate();
@@ -35,7 +35,7 @@ const Navbar = ({ onLogout, userName }) => {
alt="Campus Plug"
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
</span>
</Link>
@@ -76,7 +76,12 @@ const Navbar = ({ onLogout, userName }) => {
</Link>
{/* User Profile */}
<UserDropdown onLogout={onLogout} userName={userName} />
<UserDropdown
isAdmin={isAdmin}
onLogout={onLogout}
userName={userName}
handleShowAdminDashboard={handleShowAdminDashboard}
/>
</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 { Link, useNavigate } from "react-router-dom";
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 dropdownRef = useRef(null);
const navigate = useNavigate();
@@ -49,7 +55,7 @@ const UserDropdown = ({ onLogout, userName }) => {
onClick={toggleDropdown}
>
<div className="h-8 w-8 rounded-full bg-emerald-100 flex items-center justify-center">
<User className="h-5 w-5 text-emerald-600" />
<User className="h-5 w-5 text-emerald-700" />
</div>
</button>
@@ -89,6 +95,20 @@ const UserDropdown = ({ onLogout, userName }) => {
Settings
</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
className="flex w-full items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
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>
<Link
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
</Link>
@@ -161,7 +161,7 @@ const Favorites = () => {
</button>
</div>
<p className="text-emerald-600 font-bold mt-1">
<p className="text-emerald-700 font-bold mt-1">
${product.price.toFixed(2)}
</p>
@@ -200,56 +200,8 @@ const Favorites = () => {
)}
<footer className="bg-gray-800 text-white py-6 mt-12">
<div className="container mx-auto px-4">
<div className="flex flex-col md:flex-row justify-between items-center">
<div className="mb-4 md:mb-0">
<h3 className="text-lg font-semibold mb-2">Campus Marketplace</h3>
<p className="text-gray-400 text-sm">
Your trusted university trading platform
</p>
</div>
<div className="flex space-x-6">
<div>
<h4 className="font-medium mb-2">Quick Links</h4>
<ul className="text-sm text-gray-400">
<li className="mb-1">
<Link to="/" className="hover:text-white transition">
Home
</Link>
</li>
<li className="mb-1">
<Link to="/selling" className="hover:text-white transition">
Sell an Item
</Link>
</li>
<li className="mb-1">
<Link
to="/favorites"
className="hover:text-white transition"
>
My Favorites
</Link>
</li>
</ul>
</div>
<div>
<h4 className="font-medium mb-2">Contact</h4>
<ul className="text-sm text-gray-400">
<li className="mb-1">support@campusmarket.com</li>
<li className="mb-1">University of Calgary</li>
</ul>
</div>
</div>
</div>
<div className="border-t border-gray-700 mt-6 pt-6 text-center text-sm text-gray-400">
<p>
© {new Date().getFullYear()} Campus Marketplace. All rights
reserved.
</p>
</div>
<div className="border-t border-gray-700 text-center text-sm text-gray-400">
<p>© 2025 Campus Marketplace. All rights reserved.</p>
</div>
</footer>
</div>

View File

@@ -1,12 +1,6 @@
import { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
import {
Tag,
ChevronLeft,
ChevronRight,
Bookmark,
BookmarkCheck,
} from "lucide-react";
import { useState, useEffect, useRef } from "react";
import { Tag, ChevronLeft, ChevronRight, Bookmark, Loader } from "lucide-react";
import FloatingAlert from "../components/FloatingAlert"; // adjust path if needed
@@ -14,38 +8,50 @@ const Home = () => {
const navigate = useNavigate();
const [listings, setListings] = useState([]);
const [recommended, setRecommended] = useState([]);
const [history, sethistory] = useState([]);
const [history, setHistory] = useState([]);
const [error, setError] = useState(null);
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.
const storedUser = JSON.parse(sessionStorage.getItem("user"));
const toggleFavorite = async (id) => {
const response = await fetch(
"http://localhost:3030/api/product/addFavorite",
{
method: "POST",
headers: {
"Content-Type": "application/json",
try {
const response = await fetch(
"http://localhost:3030/api/product/addFavorite",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
userID: storedUser.ID,
productID: id,
}),
},
body: JSON.stringify({
userID: storedUser.ID,
productID: id,
}),
},
);
const data = await response.json();
if (data.success) {
setShowAlert(true);
);
const data = await response.json();
if (data.success) {
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 response = await fetch(
"http://localhost:3030/api/history/addHistory",
{
try {
await fetch("http://localhost:3030/api/history/addHistory", {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -54,23 +60,23 @@ const Home = () => {
userID: storedUser.ID,
productID: id,
}),
},
);
});
} catch (error) {
console.error("Error adding to history:", error);
}
};
function reloadPage() {
var doctTimestamp = new Date(performance.timing.domLoading).getTime();
var now = Date.now();
var tenSec = 10 * 1000;
if (now > doctTimestamp + tenSec) {
location.reload();
}
}
reloadPage();
// Fetch recommended products
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 {
recommendationsFetched.current = true; // Mark as fetched before the API call
const response = await fetch(
"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();
if (data.success) {
setRecommended(
data.data.map((product) => ({
id: product.ProductID,
title: product.ProductName, // Use the alias from SQL
title: product.ProductName,
price: product.Price,
category: product.Category, // Ensure this gets the category name
image: product.ProductImage, // Use the alias for image URL
seller: product.SellerName, // Fetch seller name properly
datePosted: product.DateUploaded, // Use the actual date
isFavorite: false, // Default state
category: product.Category,
image: product.ProductImage,
seller: product.SellerName,
datePosted: product.DateUploaded,
isFavorite: false,
})),
);
reloadPage();
} else {
throw new Error(data.message || "Error fetching products");
throw new Error(data.message || "Error fetching recommendations");
}
} catch (error) {
console.error("Error fetching products:", error);
console.error("Error fetching recommendations:", error);
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(() => {
const fetchProducts = async () => {
setIsLoading((prev) => ({ ...prev, listings: true }));
try {
const response = await fetch(
"http://localhost:3030/api/product/getProduct",
@@ -120,18 +132,17 @@ const Home = () => {
if (!response.ok) throw new Error("Failed to fetch products");
const data = await response.json();
if (data.success) {
setListings(
data.data.map((product) => ({
id: product.ProductID,
title: product.ProductName, // Use the alias from SQL
title: product.ProductName,
price: product.Price,
category: product.Category, // Ensure this gets the category name
image: product.ProductImage, // Use the alias for image URL
seller: product.SellerName, // Fetch seller name properly
datePosted: product.DateUploaded, // Use the actual date
isFavorite: false, // Default state
category: product.Category,
image: product.ProductImage,
seller: product.SellerName,
datePosted: product.DateUploaded,
isFavorite: false,
})),
);
} else {
@@ -140,15 +151,24 @@ const Home = () => {
} catch (error) {
console.error("Error fetching products:", error);
setError(error.message);
} finally {
setIsLoading((prev) => ({ ...prev, listings: false }));
}
};
fetchProducts();
}, []);
// Fetch user history
useEffect(() => {
const fetchrecomProducts = async () => {
// Get the user's data from localStorage
const fetchUserHistory = async () => {
// Skip if already fetched or no user data
if (historyFetched.current || !storedUser || !storedUser.ID) return;
setIsLoading((prev) => ({ ...prev, history: true }));
try {
historyFetched.current = true; // Mark as fetched before the API call
const response = await fetch(
"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();
if (data.success) {
sethistory(
setHistory(
data.data.map((product) => ({
id: product.ProductID,
title: product.ProductName, // Use the alias from SQL
title: product.ProductName,
price: product.Price,
category: product.Category, // Ensure this gets the category name
image: product.ProductImage, // Use the alias for image URL
seller: product.SellerName, // Fetch seller name properly
datePosted: product.DateUploaded, // Use the actual date
category: product.Category,
image: product.ProductImage,
seller: product.SellerName,
datePosted: product.DateUploaded,
})),
);
} else {
throw new Error(data.message || "Error fetching products");
throw new Error(data.message || "Error fetching history");
}
} catch (error) {
console.error("Error fetching products:", error);
console.error("Error fetching history:", error);
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 = () => {
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 (
<div className="flex flex-col min-h-screen">
<div className="flex-grow">
{/* Hero Section with School Background */}
<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">
<img
src="../public/Ucalgary.png"
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>
{/* Content */}
<div className="max-w-2xl mx-auto text-center relative z-1">
<h1 className="text-3xl font-bold text-white mb-4">
Buy and Sell on Campus
@@ -217,348 +353,61 @@ const Home = () => {
</p>
<button
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
</button>
</div>
</div>
{/* Recent Listings */}
{/* Floating Alert */}
{showAlert && (
<FloatingAlert
message="Product added to favorites!"
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">
Recommendation
Recommended For You
</h2>
</ScrollableProductList>
<div className="relative">
{/* Left Button - Overlaid on products */}
<button
onClick={() =>
document
.getElementById("RecomContainer")
.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">
{/* Recent Listings Section */}
<ScrollableProductList
containerId="listingsContainer"
products={listings}
isLoading={isLoading.listings}
addToHistory={true}
>
<h2 className="text-xl font-semibold text-gray-800 mb-4">
Recent Listings
</h2>
<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>
</ScrollableProductList>
{/* History Section */}
{showAlert && (
<FloatingAlert
message="Product added to favorites!"
onClose={() => setShowAlert(false)}
/>
{(history.length > 0 || isLoading.history) && (
<ScrollableProductList
containerId="HistoryContainer"
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 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">
<div className="container mx-auto px-4">
<div className="flex flex-col md:flex-row justify-between items-center">
<div className="mb-4 md:mb-0">
<h3 className="text-lg font-semibold mb-2">Campus Marketplace</h3>
<p className="text-gray-400 text-sm">
Your trusted university trading platform
</p>
</div>
<div className="flex space-x-6">
<div>
<h4 className="font-medium mb-2">Quick Links</h4>
<ul className="text-sm text-gray-400">
<li className="mb-1">
<Link to="/" className="hover:text-white transition">
Home
</Link>
</li>
<li className="mb-1">
<Link to="/selling" className="hover:text-white transition">
Sell an Item
</Link>
</li>
<li className="mb-1">
<Link
to="/favorites"
className="hover:text-white transition"
>
My Favorites
</Link>
</li>
</ul>
</div>
<div>
<h4 className="font-medium mb-2">Contact</h4>
<ul className="text-sm text-gray-400">
<li className="mb-1">support@campusmarket.com</li>
<li className="mb-1">University of Calgary</li>
</ul>
</div>
</div>
</div>
<div className="border-t border-gray-700 mt-6 pt-6 text-center text-sm text-gray-400">
<p>
© {new Date().getFullYear()} Campus Marketplace. All rights
reserved.
</p>
</div>
<div className="border-t border-gray-700 text-center text-sm text-gray-400">
<p>© 2025 Campus Marketplace. All rights reserved.</p>
</div>
</footer>
</div>

View File

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

View File

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

View File

@@ -1,95 +1,284 @@
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 = () => {
// State to store user's products
const [products, setProducts] = useState([]);
// State to control when editing form is shown
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({
name: "",
price: "",
description: "",
categories: [],
status: "Unsold",
category: "",
images: [],
});
// Simulate fetching products from API/database on component mount
useEffect(() => {
// This would be replaced with a real API call
const fetchProducts = async () => {
// Mock data
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: [],
},
];
function reloadPage() {
var doctTimestamp = new Date(performance.timing.domLoading).getTime();
var now = Date.now();
if (now > doctTimestamp) {
location.reload();
}
}
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();
}, []);
});
// Handle creating or updating a product
const handleSaveProduct = () => {
if (editingProduct.id) {
// Update existing product
setProducts(
products.map((p) => (p.id === editingProduct.id ? editingProduct : p)),
);
} else {
// Create new product
const newProduct = {
...editingProduct,
id: Date.now().toString(), // Generate a temporary ID
};
setProducts([...products, newProduct]);
const handleEditProduct = (product) => {
setOriginalProduct(product);
const categoryName = getCategoryNameById(product.CategoryID);
setEditingProduct({
...product,
category: categoryName || "", // Single category string
images: product.images || [],
});
setShowForm(true);
};
// Upload images to server and get their paths
const uploadImages = async (images) => {
console.log(images);
const uploadedImagePaths = [];
// Filter out only File objects (new images to upload)
const filesToUpload = images.filter((img) => img instanceof File);
for (const file of filesToUpload) {
// Create a FormData object to send the file
const formData = new FormData();
formData.append("image", file);
try {
// Send the file to your upload endpoint
const response = await fetch("http://localhost:3030/api/upload", {
method: "POST",
body: formData,
});
if (!response.ok) {
throw new Error(`Failed to upload image: ${file.name}`);
}
const result = await response.json();
// Assuming the server returns the path where the file was saved
uploadedImagePaths.push(`/public/uploads/${file.name}`);
} catch (error) {
console.error("Error uploading image:", error);
// If upload fails, still add the expected path (this is a fallback)
uploadedImagePaths.push(`/public/uploads/${file.name}`);
}
}
// Reset form and hide it
setShowForm(false);
setEditingProduct({
name: "",
price: "",
description: "",
categories: [],
status: "Unsold",
images: [],
});
// 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
setShowForm(false);
setEditingProduct({
name: "",
price: "",
description: "",
category: "",
images: [],
});
setOriginalProduct(null); // reset original as well
reloadPage();
} catch (error) {
console.error("Error saving product:", error);
alert(`Error saving product: ${error.message}`);
}
};
// Handle product deletion
const handleDeleteProduct = (productId) => {
if (window.confirm("Are you sure you want to delete this product?")) {
setProducts(products.filter((p) => p.id !== productId));
const handleDeleteProduct = async (productId) => {
try {
// Replace with your actual API endpoint
const response = await fetch(
"http://localhost:3030/api/product/delProduct",
{
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
}
};
// Handle editing a product
const handleEditProduct = (product) => {
setEditingProduct({
...product,
images: product.images || [], // Ensure images array exists
});
setShowForm(true);
// 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
@@ -98,13 +287,20 @@ const Selling = () => {
name: "",
price: "",
description: "",
categories: [],
status: "Unsold",
category: "",
images: [],
});
setShowForm(true);
};
// Handle category change
const handleCategoryChange = (e) => {
setEditingProduct({
...editingProduct,
category: e.target.value,
});
};
return (
<div className="container mx-auto p-4 max-w-6xl">
<div className="flex justify-between items-center mb-6">
@@ -112,7 +308,7 @@ const Selling = () => {
{!showForm && (
<button
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
</button>
@@ -120,12 +316,226 @@ const Selling = () => {
</div>
{showForm ? (
<ProductForm
editingProduct={editingProduct}
setEditingProduct={setEditingProduct}
onSave={handleSaveProduct}
onCancel={() => setShowForm(false)}
/>
<div className="bg-white border border-gray-200 shadow-md p-6">
{/* Back Button */}
<button
onClick={() => 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 ? (
@@ -135,7 +545,7 @@ const Selling = () => {
</p>
<button
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
</button>
@@ -143,80 +553,82 @@ const Selling = () => {
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{products.map((product) => (
<div
key={product.id}
className="border-2 border-gray-200 overflow-hidden hover:shadow-md transition-shadow"
<Link
key={product.ProductID}
to={`/product/${product.ProductID}`}
>
<div className="h-48 bg-gray-200 flex items-center justify-center">
{product.images && product.images.length > 0 ? (
<img
src={product.images[0] || ""}
alt={product.name}
className="w-full h-full object-cover"
/>
) : (
<div className="text-gray-400">No image</div>
)}
</div>
<div className="p-4">
<div className="flex justify-between items-start">
<h3 className="text-lg font-semibold text-gray-800">
{product.name}
</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 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">
{product.image_url && product.image_url.length > 0 ? (
<img
src={product.image_url || ""}
alt={product.Name}
className="w-full h-full object-cover"
/>
) : (
<div className="text-gray-400">No image</div>
)}
</div>
<p className="text-emerald-600 font-bold mt-1">
${product.price}
</p>
{product.categories && product.categories.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{product.categories.map((category) => (
<span
key={category}
className="text-xs bg-gray-100 text-gray-600 px-2 py-1 "
>
{category}
</span>
))}
<div className="p-4">
<div className="flex justify-between items-start">
<h3 className="text-lg font-semibold text-gray-800">
{product.Name}
</h3>
</div>
)}
<p className="text-gray-500 text-sm mt-2 line-clamp-2">
{product.description}
</p>
<p className="text-emerald-700 font-bold mt-1">
${product.Price}
</p>
<div className="mt-4 flex justify-end gap-2">
<button
onClick={() => handleDeleteProduct(product.id)}
className="text-red-600 hover:text-red-800"
>
Delete
</button>
<button
onClick={() => handleEditProduct(product)}
className="text-emerald-600 hover:text-emerald-800 font-medium"
>
Edit
</button>
{product.CategoryID && (
<div className="mt-2 flex flex-wrap gap-1">
<span className="text-xs bg-gray-100 text-gray-600 px-2 py-1">
{getCategoryNameById(product.CategoryID) ||
product.CategoryID}
</span>
</div>
)}
<p className="text-gray-500 text-sm mt-2 line-clamp-2">
{product.Description}
</p>
<div className="mt-4 flex justify-end gap-2">
<button
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
handleDeleteProduct(product.ProductID);
}}
className="text-red-600 hover:text-red-800"
>
Delete
</button>
<button
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
handleEditProduct(product);
}}
className="text-emerald-700 hover:text-emerald-800 font-medium"
>
Edit
</button>
</div>
</div>
</div>
</div>
</Link>
))}
</div>
)}
</>
)}
<footer className="bg-gray-800 text-white py-6 mt-12">
<div className="border-t border-gray-700 text-center text-sm text-gray-400">
<p>© 2025 Campus Marketplace. All rights reserved.</p>
</div>
</footer>
</div>
);
};

View File

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

View File

@@ -1,8 +1,209 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { Calendar, CreditCard, Trash2 } from "lucide-react";
import FloatingAlert from "../components/FloatingAlert"; // adjust path if needed
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;

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
FOREIGN_KEY_CHECKS = 0;
TRUNCATE TABLE Product_Category;
TRUNCATE TABLE Favorites;
TRUNCATE TABLE History;
@@ -44,7 +42,7 @@ VALUES
(
1,
'John Doe',
'john.doe@example.com',
'john.doe@ucalgary.ca',
'U123456',
'hashedpassword1',
'555-123-4567',
@@ -53,7 +51,7 @@ VALUES
(
2,
'Jane Smith',
'jane.smith@example.com',
'jane.smith@ucalgary.ca',
'U234567',
'hashedpassword2',
'555-234-5678',
@@ -67,40 +65,41 @@ VALUES
(1, TRUE, TRUE),
(2, TRUE, FALSE);
-- Insert Categories
-- Insert Categories
INSERT INTO
Category (CategoryID, Name)
Category (Name)
VALUES
(1, 'Textbooks'),
(2, 'Electronics'),
(3, 'Furniture'),
(4, 'Clothing'),
(5, 'Sports Equipment'),
(6, 'Musical Instruments'),
(7, 'Art Supplies'),
(8, 'Kitchen Appliances'),
(9, 'Gaming'),
(10, 'Bicycles'),
(11, 'Computer Accessories'),
(12, 'Stationery'),
(13, 'Fitness Equipment'),
(14, 'Winter Sports'),
(15, 'Lab Equipment'),
(16, 'Camping Gear'),
(17, 'School Supplies'),
(18, 'Office Furniture'),
(19, 'Books (Non-textbook)'),
(20, 'Math & Science Resources'),
(21, 'Engineering Tools'),
(22, 'Backpacks & Bags'),
(23, 'Audio Equipment'),
(24, 'Dorm Essentials'),
(25, 'Smartphones & Tablets'),
(26, 'Winter Clothing'),
(27, 'Photography Equipment'),
(28, 'Event Tickets'),
(29, 'Software Licenses'),
(30, 'Transportation (Car Pool)');
('Other'),
('Textbooks'),
('Electronics'),
('Furniture'),
('Clothing'),
('Sports Equipment'),
('Musical Instruments'),
('Art Supplies'),
('Kitchen Appliances'),
('Gaming'),
('Bicycles'),
('Computer Accessories'),
('Stationery'),
('Fitness Equipment'),
('Winter Sports'),
('Lab Equipment'),
('Camping Gear'),
('School Supplies'),
('Office Furniture'),
('Books (-textbook)'),
('Math & Science Resources'),
('Engineering Tools'),
('Backpacks & Bags'),
('Audio Equipment'),
('Dorm Essentials'),
('Smartphones & Tablets'),
('Winter Clothing'),
('Photography Equipment'),
('Event Tickets'),
('Software Licenses');
-- Insert Products
INSERT INTO
@@ -319,85 +318,29 @@ VALUES
INSERT INTO
Image_URL (URL, ProductID)
VALUES
('/Pictures/Dell1.jpg', 1),
('/Pictures/Dell2.jpg', 1),
('/Pictures/Dell3.jpg', 1),
('/Pictures/HP-Laptop1.jpg', 2),
('/Pictures/HP-Laptop1.jpg', 2),
('/Pictures/Dorm-Desk.jpg', 3),
('/Pictures/University-Hoodie.jpg', 4),
('/Pictures/Basketball.jpg', 5),
('/Pictures/Acoustic-Guitar.jpg', 6),
('/Pictures/Physics-Textbook.jpg', 7),
('/Pictures/Mini-Fridge.jpg', 8),
('/Pictures/Controller.jpg', 9),
('/Pictures/Mountain-Bike.jpg', 10),
('/Pictures/Wireless-Mouse.jpg', 11),
('/Pictures/Lab-Coat.jpg', 12),
('/Pictures/Calculator.jpg', 13),
('/Pictures/Yoga-Mat.jpg', 14),
('/Pictures/Winter-Jacket.jpg', 15),
('/Pictures/CS-Textbook.jpg', 16),
('/Pictures/Desk-Lamp.jpg', 17),
('/Pictures/HP-Calculator.jpg', 18),
('/Pictures/Bluetooth-Speaker.jpg', 19),
('/Pictures/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);
('/Uploads/Dell1.jpg', 1),
('/Uploads/Dell2.jpg', 1),
('/Uploads/Dell3.jpg', 1),
('/Uploads/HP-Laptop1.jpg', 2),
('/Uploads/HP-Laptop1.jpg', 2),
('/Uploads/Dorm-Desk.jpg', 3),
('/Uploads/University-Hoodie.jpg', 4),
('/Uploads/Basketball.jpg', 5),
('/Uploads/Acoustic-Guitar.jpg', 6),
('/Uploads/Physics-Textbook.jpg', 7),
('/Uploads/Mini-Fridge.jpg', 8),
('/Uploads/Controller.jpg', 9),
('/Uploads/Mountain-Bike.jpg', 10),
('/Uploads/Wireless-Mouse.jpg', 11),
('/Uploads/Lab-Coat.jpg', 12),
('/Uploads/Calculator.jpg', 13),
('/Uploads/Yoga-Mat.jpg', 14),
('/Uploads/Winter-Jacket.jpg', 15),
('/Uploads/CS-Textbook.jpg', 16),
('/Uploads/Desk-Lamp.jpg', 17),
('/Uploads/HP-Calculator.jpg', 18),
('/Uploads/Bluetooth-Speaker.jpg', 19),
('/Uploads/Backpack.jpg', 20);
-- Insert History records
INSERT INTO
@@ -427,7 +370,6 @@ VALUES
(1, 5), -- User 4 likes Basketball
(2, 8);
-- User 5 likes Mini Fridge
-- Insert Transactions
INSERT INTO
Transaction (
@@ -452,4 +394,5 @@ VALUES
1,
'This is a great fake product! Totally recommend it.',
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)
CREATE TABLE Category (
CategoryID INT PRIMARY KEY,
CategoryID INT AUTO_INCREMENT PRIMARY KEY,
Name VARCHAR(255) NOT NULL
);
@@ -36,9 +36,9 @@ CREATE TABLE Product (
StockQuantity INT,
UserID INT,
Description TEXT,
CategoryID INT NOT NULL,
CategoryID INT,
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)
);
@@ -46,7 +46,7 @@ CREATE TABLE Product (
CREATE TABLE Image_URL (
URL VARCHAR(255),
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)
@@ -60,19 +60,19 @@ CREATE TABLE Review (
AND Rating <= 5
),
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID),
FOREIGN KEY (ProductID) REFERENCES Product (ProductID)
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,
TransactionID INT AUTO_INCREMENT PRIMARY KEY,
UserID INT,
ProductID INT,
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
PaymentStatus VARCHAR(50),
FOREIGN KEY (UserID) REFERENCES User (UserID),
FOREIGN KEY (ProductID) REFERENCES Product (ProductID)
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)
@@ -81,8 +81,8 @@ CREATE TABLE Recommendation (
UserID INT,
RecommendedProductID INT,
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID),
FOREIGN KEY (RecommendedProductID) REFERENCES Product (ProductID)
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)
@@ -91,8 +91,8 @@ CREATE TABLE History (
UserID INT,
ProductID INT,
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (UserID) REFERENCES User (UserID),
FOREIGN KEY (ProductID) REFERENCES Product (ProductID)
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)
@@ -100,20 +100,11 @@ CREATE TABLE Favorites (
FavoriteID INT AUTO_INCREMENT PRIMARY KEY,
UserID INT,
ProductID INT,
FOREIGN KEY (UserID) REFERENCES User (UserID),
FOREIGN KEY (ProductID) REFERENCES Product (ProductID),
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),
FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID)
);
-- Login Authentication table
CREATE TABLE AuthVerification (
UserID INT AUTO_INCREMENT PRIMARY KEY,

View File

@@ -3,7 +3,7 @@ import mysql.connector
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
import logging
from unittest import result
import random
def database():
db_connection = mysql.connector.connect(
@@ -14,147 +14,228 @@ def database():
)
return db_connection
def get_popular_products():
pass
def delete_user_recommendation(userID, Array):
def delete_user_recommendations(user_id):
db_con = database()
cursor = db_con.cursor()
try:
for item in Array:
#Product ID starts form index 1
item_value = item + 1
print(item_value)
# Use parameterized queries to prevent SQL injection
cursor.execute(f"INTO Recommendation (UserID, RecommendedProductID) VALUES ({userID}, {item_value});")
print(f"Deleted existing recommendations for user {user_id}")
cursor.execute(f"DELETE FROM Recommendation WHERE UserID = {user_id}")
db_con.commit()
logging.info(f"Deleted existing recommendations for user {user_id}")
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()
#print(results)
except:
pass
def get_all_products():
def get_random_products(count=0, exclude_list=None):
db_con = database()
cursor = db_con.cursor()
cursor.execute("SELECT CategoryID FROM Category")
categories = cursor.fetchall()
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}")
select_clause = "SELECT p.ProductID"
for category in categories:
category_id = category[0]
select_clause += f", MAX(CASE WHEN pc.CategoryID = {category_id} THEN 1 ELSE 0 END) AS `Cat_{category_id}`"
random_products = [row[0] for row in cursor.fetchall()]
return random_products
final_query = f"""
{select_clause}
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;
"""
except Exception as e:
logging.error(f"Error getting random products: {str(e)}")
return []
finally:
cursor.close()
db_con.close()
cursor.execute(final_query)
results = cursor.fetchall()
def get_popular_products(count=5):
db_con = database()
cursor = db_con.cursor()
final = []
for row in results:
text_list = list(row)
text_list.pop(0)
final.append(text_list)
try:
cursor.execute("""
SELECT ProductID, COUNT(*) as count
FROM History
GROUP BY ProductID
ORDER BY count DESC
LIMIT %s
""", (count,))
cursor.close()
db_con.close()
return final
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")
categories = cursor.fetchall()
select_clause = "SELECT p.ProductID"
for category in categories:
category_id = category[0]
select_clause += f", MAX(CASE WHEN p.CategoryID = {category_id} THEN 1 ELSE 0 END) AS `Cat_{category_id}`"
final_query = f"""
{select_clause}
FROM Product p
GROUP BY p.ProductID;
"""
cursor.execute(final_query)
results = cursor.fetchall()
final = []
product_ids = []
for row in results:
text_list = list(row)
product_id = text_list.pop(0)
final.append(text_list)
product_ids.append(product_id)
cursor.close()
db_con.close()
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):
db_con = database()
cursor = db_con.cursor()
cursor.execute("SELECT CategoryID FROM Category")
categories = cursor.fetchall()
select_clause = "SELECT p.ProductID"
for category in categories:
category_id = category[0] # get the uid of the catefory and then append that to the new column
select_clause += f", MAX(CASE WHEN pc.CategoryID = {category_id} THEN 1 ELSE 0 END) AS `Cat_{category_id}`"
final_query = f"""
{select_clause}
FROM Product p
LEFT JOIN Product_Category pc ON p.ProductID = pc.ProductID
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;
"""
cursor.execute(final_query)
results = cursor.fetchall()
final = []
for row in results:
text_list = list(row)
text_list.pop(0)
final.append(text_list)
cursor.close()
db_con.close()
return final
def get_recommendations(user_id, top_n=10):
try:
# Get all products and user history with their category vectors
all_products = get_all_products()
cursor.execute("SELECT CategoryID FROM Category")
categories = cursor.fetchall()
select_clause = "SELECT p.ProductID"
for category in categories:
category_id = category[0]
select_clause += f", MAX(CASE WHEN p.CategoryID = {category_id} THEN 1 ELSE 0 END) AS `Cat_{category_id}`"
final_query = f"""
{select_clause}
FROM Product p
WHERE p.ProductID IN (SELECT ProductID FROM History WHERE UserID = {user_id})
GROUP BY p.ProductID;
"""
cursor.execute(final_query)
results = cursor.fetchall()
final = []
for row in results:
text_list = list(row)
text_list.pop(0)
final.append(text_list)
cursor.close()
db_con.close()
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=5):
try:
delete_user_recommendations(user_id)
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)
# if not user_history:
# #Cold start: return popular products
# return get_popular_products(top_n)
# Calculate similarity between all products and user history
user_profile = np.mean(user_history, axis=0) # Average user preferences
similarities = cosine_similarity([user_profile], all_products)
# finds the indices of the top N products that have the highest
# cosine similarity with the user's profile and sorted from most similar to least similar.
if not user_history:
popular_recs = get_popular_products(top_n)
recommendation_upload(user_id, popular_recs)
additional_random = get_random_products(5, popular_recs)
recommendation_upload(user_id, additional_random)
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]
print("product", product_indices)
# Get the recommended product IDs
recommended_products = [all_products[i][0] for i in product_indices] # Product IDs
recommended_product_ids = [all_product_ids[i] for i in product_indices]
print(recommended_product_ids)
# Upload the recommendations to the database
history_upload(user_id, product_indices) # Pass the indices directly to history_upload
recommendation_upload(user_id, recommended_product_ids)
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:
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()
cursor = db_con.cursor()
try:
for item in anrr:
#Product ID starts form index 1
item_value = item + 1
print(item_value)
# Use parameterized queries to prevent SQL injection
cursor.execute(f"INSERT INTO Recommendation (UserID, RecommendedProductID) VALUES ({userID}, {item_value});")
for product_id in products:
cursor.execute("INSERT INTO Recommendation (UserID, RecommendedProductID) VALUES (%s, %s)",
(userID, product_id))
# Commit the changes
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:
print(f"Error: {e}")
logging.error(f"Error uploading recommendations: {str(e)}")
db_con.rollback()
finally:
# Close the cursor and connection
cursor.close()
db_con.close()