Merge branch 'main' into mann-Branch
This commit is contained in:
24
README.md
24
README.md
@@ -1,25 +1,28 @@
|
|||||||
### Some ground rules
|
### Some ground rules
|
||||||
1. Add both node_modules from Slient and Server to your ```gitignore``` file
|
1. Add both node_modules from Slient and Server to your ```gitignore``` file
|
||||||
2. Make a brach with the following naming conventionp, refix it with your name ```Your-Name Branch-Name```.
|
2. Make a brach with the following naming conventionp, prefix it with your name ```Your-Name Branch-Name```.
|
||||||
---
|
---
|
||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
1. `cd frontend` into the dir and then type command
|
1. `cd frontend` into the dir and then type command
|
||||||
```Bash
|
```Bash
|
||||||
#Install the needed lib with the command bellow
|
1. npm install #Installs the needed packages
|
||||||
npm install
|
2. npm run dev #Start The Server
|
||||||
#Start The Server
|
|
||||||
npm run dev
|
|
||||||
```
|
```
|
||||||
---
|
---
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
1. `cd backend` into the dir and then type command
|
1. `cd backend` into the dir and then type command
|
||||||
```Bash
|
```Bash
|
||||||
#Install the needed lib with the command bellow
|
1. npm install #Installs the needed packages
|
||||||
npm install
|
2. npm run dev #Start The Server
|
||||||
#Start The Server
|
```
|
||||||
npm run dev
|
---
|
||||||
|
|
||||||
|
### Recommendation
|
||||||
|
1. `cd recommendation-engine` into the dir and then type command
|
||||||
|
```Bash
|
||||||
|
1. python3 server.py #Start The Server
|
||||||
```
|
```
|
||||||
---
|
---
|
||||||
### Recommendation system
|
### Recommendation system
|
||||||
@@ -34,7 +37,6 @@
|
|||||||
2. To Create the DataBase use the command bellow:
|
2. To Create the DataBase use the command bellow:
|
||||||
```Bash
|
```Bash
|
||||||
1. mysql -u root
|
1. mysql -u root
|
||||||
2. use Marketplace;
|
2. \. PathToYour/Schema.sql
|
||||||
3. \. PathToYour/Schema.sql
|
|
||||||
3. \. PathToYour/Init-Data.sql
|
3. \. PathToYour/Init-Data.sql
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,5 +1,51 @@
|
|||||||
const db = require("../utils/database");
|
const db = require("../utils/database");
|
||||||
|
|
||||||
|
exports.getAllCategoriesWithPagination = async (req, res) => {
|
||||||
|
const limit = +req.query?.limit;
|
||||||
|
const page = +req.query?.page;
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
try {
|
||||||
|
const [data, _] = await db.execute(
|
||||||
|
"SELECT * FROM Category C ORDER BY C.CategoryID ASC LIMIT ? OFFSET ?",
|
||||||
|
[limit.toString(), offset.toString()],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [result] = await db.execute("SELECT COUNT(*) AS count FROM Category");
|
||||||
|
const { count: total } = result[0];
|
||||||
|
return res.json({ data, total });
|
||||||
|
} catch (error) {
|
||||||
|
res.json({ error: "Cannot fetch categories from database!" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.addCategory = async (req, res) => {
|
||||||
|
const { name } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [result] = await db.execute(
|
||||||
|
"INSERT INTO Category (Name) VALUES (?)",
|
||||||
|
[name],
|
||||||
|
);
|
||||||
|
res.json({ message: "Adding new category successfully!" });
|
||||||
|
} catch (error) {
|
||||||
|
res.json({ error: "Cannot add new category!" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.removeCategory = async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [result] = await db.execute(
|
||||||
|
`DELETE FROM Category WHERE CategoryID = ?`,
|
||||||
|
[id],
|
||||||
|
);
|
||||||
|
res.json({ message: "Delete category successfully!" });
|
||||||
|
} catch (error) {
|
||||||
|
res.json({ error: "Cannot remove category from database!" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
exports.getAllCategory = async (req, res) => {
|
exports.getAllCategory = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [data, fields] = await db.execute(`SELECT * FROM Category`);
|
const [data, fields] = await db.execute(`SELECT * FROM Category`);
|
||||||
|
|||||||
@@ -345,3 +345,62 @@ exports.getProductById = async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.getProductWithPagination = async (req, res) => {
|
||||||
|
const limit = +req.query.limit;
|
||||||
|
const page = +req.query.page;
|
||||||
|
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
|
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!" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
40
backend/controllers/transaction.js
Normal file
40
backend/controllers/transaction.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
const db = require("../utils/database");
|
||||||
|
|
||||||
|
exports.getTransactionWithPagination = async (req, res) => {
|
||||||
|
const limit = +req.query?.limit;
|
||||||
|
const page = +req.query?.page;
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
try {
|
||||||
|
const [data, _] = await db.execute(
|
||||||
|
`SELECT T.TransactionID, DATE_FORMAT(T.Date, '%b-%d-%Y %h:%i %p') as Date, T.PaymentStatus, U.Name as UserName, P.Name as ProductName
|
||||||
|
FROM Transaction T
|
||||||
|
LEFT JOIN User U ON T.UserID = U.UserID
|
||||||
|
LEFT JOIN Product P ON T.ProductID = P.ProductID
|
||||||
|
ORDER BY T.TransactionID ASC LIMIT ? OFFSET ?`,
|
||||||
|
[limit.toString(), offset.toString()]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [result] = await db.execute(
|
||||||
|
"SELECT COUNT(*) AS count FROM Transaction"
|
||||||
|
);
|
||||||
|
const { count: total } = result[0];
|
||||||
|
return res.json({ data, total });
|
||||||
|
} catch (error) {
|
||||||
|
res.json({ error: "Cannot fetch transactions from database!" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.removeTransation = async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
try {
|
||||||
|
const [result] = await db.execute(
|
||||||
|
"DELETE FROM Transaction WHERE TransactionID = ?;",
|
||||||
|
[id.toString()]
|
||||||
|
);
|
||||||
|
return res.json({ message: "Remove transaction successfully!" });
|
||||||
|
} catch (error) {
|
||||||
|
return res
|
||||||
|
.status(500)
|
||||||
|
.json({ error: "Cannot remove transactions from database!" });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -13,13 +13,13 @@ exports.sendVerificationCode = async (req, res) => {
|
|||||||
// Generate a random 6-digit code
|
// Generate a random 6-digit code
|
||||||
const verificationCode = crypto.randomInt(100000, 999999).toString();
|
const verificationCode = crypto.randomInt(100000, 999999).toString();
|
||||||
console.log(
|
console.log(
|
||||||
`Generated verification code for ${email}: ${verificationCode}`,
|
`Generated verification code for ${email}: ${verificationCode}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if email already exists in verification table
|
// Check if email already exists in verification table
|
||||||
const [results, fields] = await db.execute(
|
const [results, fields] = await db.execute(
|
||||||
"SELECT * FROM AuthVerification WHERE Email = ?",
|
"SELECT * FROM AuthVerification WHERE Email = ?",
|
||||||
[email],
|
[email]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (results.length > 0) {
|
if (results.length > 0) {
|
||||||
@@ -27,7 +27,7 @@ exports.sendVerificationCode = async (req, res) => {
|
|||||||
const [result] = await db.execute(
|
const [result] = await db.execute(
|
||||||
`UPDATE AuthVerification SET VerificationCode = ?, Authenticated = FALSE, Date = CURRENT_TIMESTAMP
|
`UPDATE AuthVerification SET VerificationCode = ?, Authenticated = FALSE, Date = CURRENT_TIMESTAMP
|
||||||
WHERE Email = ?`,
|
WHERE Email = ?`,
|
||||||
[verificationCode, email],
|
[verificationCode, email]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Send email and respond
|
// Send email and respond
|
||||||
@@ -37,7 +37,7 @@ exports.sendVerificationCode = async (req, res) => {
|
|||||||
// Insert new record
|
// Insert new record
|
||||||
const [result] = await db.execute(
|
const [result] = await db.execute(
|
||||||
"INSERT INTO AuthVerification (Email, VerificationCode, Authenticated) VALUES (?, ?, FALSE)",
|
"INSERT INTO AuthVerification (Email, VerificationCode, Authenticated) VALUES (?, ?, FALSE)",
|
||||||
[email, verificationCode],
|
[email, verificationCode]
|
||||||
);
|
);
|
||||||
// Send email and respond
|
// Send email and respond
|
||||||
await sendVerificationEmail(email, verificationCode);
|
await sendVerificationEmail(email, verificationCode);
|
||||||
@@ -62,7 +62,7 @@ exports.verifyCode = async (req, res) => {
|
|||||||
// Check verification code
|
// Check verification code
|
||||||
const [results, fields] = await db.execute(
|
const [results, fields] = await db.execute(
|
||||||
"SELECT * FROM AuthVerification WHERE Email = ? AND VerificationCode = ? AND Authenticated = 0 AND Date > DATE_SUB(NOW(), INTERVAL 15 MINUTE)",
|
"SELECT * FROM AuthVerification WHERE Email = ? AND VerificationCode = ? AND Authenticated = 0 AND Date > DATE_SUB(NOW(), INTERVAL 15 MINUTE)",
|
||||||
[email, code],
|
[email, code]
|
||||||
);
|
);
|
||||||
if (results.length === 0) {
|
if (results.length === 0) {
|
||||||
console.log(`Invalid or expired verification code for email ${email}`);
|
console.log(`Invalid or expired verification code for email ${email}`);
|
||||||
@@ -76,7 +76,7 @@ exports.verifyCode = async (req, res) => {
|
|||||||
// Mark as authenticated
|
// Mark as authenticated
|
||||||
const [result] = await db.execute(
|
const [result] = await db.execute(
|
||||||
"UPDATE AuthVerification SET Authenticated = TRUE WHERE Email = ?",
|
"UPDATE AuthVerification SET Authenticated = TRUE WHERE Email = ?",
|
||||||
[email],
|
[email]
|
||||||
);
|
);
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -95,7 +95,7 @@ exports.completeSignUp = async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const [results, fields] = await db.execute(
|
const [results, fields] = await db.execute(
|
||||||
`SELECT * FROM AuthVerification WHERE Email = ? AND Authenticated = 1;`,
|
`SELECT * FROM AuthVerification WHERE Email = ? AND Authenticated = 1;`,
|
||||||
[data.email],
|
[data.email]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (results.length === 0) {
|
if (results.length === 0) {
|
||||||
@@ -105,20 +105,20 @@ exports.completeSignUp = async (req, res) => {
|
|||||||
// Create the user
|
// Create the user
|
||||||
const [createResult] = await db.execute(
|
const [createResult] = await db.execute(
|
||||||
`INSERT INTO User (Name, Email, UCID, Password, Phone, Address)
|
`INSERT INTO User (Name, Email, UCID, Password, Phone, Address)
|
||||||
VALUES ('${data.name}', '${data.email}', '${data.UCID}', '${data.password}', '${data.phone}', '${data.address}')`,
|
VALUES ('${data.name}', '${data.email}', '${data.UCID}', '${data.password}', '${data.phone}', '${data.address}')`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Insert role using the user's ID
|
// Insert role using the user's ID
|
||||||
const [insertResult] = await db.execute(
|
const [insertResult] = await db.execute(
|
||||||
`INSERT INTO UserRole (UserID, Client, Admin)
|
`INSERT INTO UserRole (UserID, Client, Admin)
|
||||||
VALUES (LAST_INSERT_ID(), ${data.client || true}, ${
|
VALUES (LAST_INSERT_ID(), ${data.client || true}, ${
|
||||||
data.admin || false
|
data.admin || false
|
||||||
})`,
|
})`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Delete verification record
|
// Delete verification record
|
||||||
const [deleteResult] = await db.execute(
|
const [deleteResult] = await db.execute(
|
||||||
`DELETE FROM AuthVerification WHERE Email = '${data.email}'`,
|
`DELETE FROM AuthVerification WHERE Email = '${data.email}'`
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -310,7 +310,7 @@ exports.deleteUser = async (req, res) => {
|
|||||||
// Delete from UserRole first (assuming foreign key constraint)
|
// Delete from UserRole first (assuming foreign key constraint)
|
||||||
const [result1] = await db.execute(
|
const [result1] = await db.execute(
|
||||||
"DELETE FROM UserRole WHERE UserID = ?",
|
"DELETE FROM UserRole WHERE UserID = ?",
|
||||||
[userId],
|
[userId]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Then delete from User table
|
// Then delete from User table
|
||||||
@@ -328,3 +328,38 @@ exports.deleteUser = async (req, res) => {
|
|||||||
return res.status(500).json({ error: "Could not delete user!" });
|
return res.status(500).json({ error: "Could not delete user!" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.getUsersWithPagination = async (req, res) => {
|
||||||
|
const limit = +req.query.limit;
|
||||||
|
const page = +req.query.page;
|
||||||
|
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
try {
|
||||||
|
const [users, fields] = await db.execute(
|
||||||
|
"SELECT * FROM User LIMIT ? OFFSET ?",
|
||||||
|
[limit.toString(), offset.toString()]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [result] = await db.execute("SELECT COUNT(*) AS count FROM User");
|
||||||
|
const { count: total } = result[0];
|
||||||
|
|
||||||
|
res.json({ users, total });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Errors: ", error);
|
||||||
|
return res.status(500).json({ error: "\nCould not fetch users!" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.isAdmin = async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
try {
|
||||||
|
const [result] = await db.execute(
|
||||||
|
"SELECT R.Admin FROM marketplace.userrole R WHERE R.UserID = ?",
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
const { Admin } = result[0];
|
||||||
|
res.json({ isAdmin: Admin });
|
||||||
|
} catch (error) {
|
||||||
|
res.json({ error: "Cannot verify admin status!" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ const searchRouter = require("./routes/search");
|
|||||||
const recommendedRouter = require("./routes/recommendation");
|
const recommendedRouter = require("./routes/recommendation");
|
||||||
const history = require("./routes/history");
|
const history = require("./routes/history");
|
||||||
const review = require("./routes/review");
|
const review = require("./routes/review");
|
||||||
const category = require("./routes/category");
|
const categoryRouter = require("./routes/category");
|
||||||
|
const transactionRouter = require("./routes/transaction");
|
||||||
|
|
||||||
const { generateEmailTransporter } = require("./utils/mail");
|
const { generateEmailTransporter } = require("./utils/mail");
|
||||||
const {
|
const {
|
||||||
@@ -43,7 +44,9 @@ app.use("/api/search", searchRouter);
|
|||||||
app.use("/api/engine", recommendedRouter);
|
app.use("/api/engine", recommendedRouter);
|
||||||
app.use("/api/history", history);
|
app.use("/api/history", history);
|
||||||
app.use("/api/review", review);
|
app.use("/api/review", review);
|
||||||
app.use("/api/category", category);
|
app.use("/api/category", categoryRouter);
|
||||||
|
app.use("/api/transaction", transactionRouter);
|
||||||
|
app.use("/api/category", categoryRouter);
|
||||||
|
|
||||||
// Set up a scheduler to run cleanup every hour
|
// Set up a scheduler to run cleanup every hour
|
||||||
clean_up_time = 30 * 60 * 1000;
|
clean_up_time = 30 * 60 * 1000;
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const { getAllCategory } = require("../controllers/category");
|
const {
|
||||||
|
getAllCategoriesWithPagination,
|
||||||
|
addCategory,
|
||||||
|
removeCategory,
|
||||||
|
getAllCategory,
|
||||||
|
} = require("../controllers/category");
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get("/getCategories", getAllCategoriesWithPagination);
|
||||||
|
router.post("/addCategory", addCategory);
|
||||||
|
router.delete("/:id", removeCategory);
|
||||||
router.get("/", getAllCategory);
|
router.get("/", getAllCategory);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ const {
|
|||||||
getAllProducts,
|
getAllProducts,
|
||||||
getProductById,
|
getProductById,
|
||||||
addProduct,
|
addProduct,
|
||||||
myProduct,
|
|
||||||
removeProduct,
|
removeProduct,
|
||||||
|
removeAnyProduct,
|
||||||
|
getProductWithPagination,
|
||||||
|
myProduct,
|
||||||
updateProduct,
|
updateProduct,
|
||||||
} = require("../controllers/product");
|
} = require("../controllers/product");
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@@ -27,6 +29,12 @@ router.post("/delProduct", removeProduct);
|
|||||||
router.post("/myProduct", myProduct);
|
router.post("/myProduct", myProduct);
|
||||||
router.post("/addProduct", addProduct);
|
router.post("/addProduct", addProduct);
|
||||||
router.get("/getProduct", getAllProducts);
|
router.get("/getProduct", getAllProducts);
|
||||||
|
|
||||||
|
//Remove product
|
||||||
|
router.delete("/any/:id", removeAnyProduct);
|
||||||
|
//Get products with pagination
|
||||||
|
router.get("/getProductWithPagination", getProductWithPagination);
|
||||||
|
|
||||||
router.get("/:id", getProductById); // Simplified route
|
router.get("/:id", getProductById); // Simplified route
|
||||||
|
|
||||||
router.put("/update/:productId", updateProduct);
|
router.put("/update/:productId", updateProduct);
|
||||||
|
|||||||
12
backend/routes/transaction.js
Normal file
12
backend/routes/transaction.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const {
|
||||||
|
getTransactionWithPagination,
|
||||||
|
removeTransation,
|
||||||
|
} = require("../controllers/transaction");
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get("/getTransactions", getTransactionWithPagination);
|
||||||
|
router.delete("/:id", removeTransation);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -8,6 +8,8 @@ const {
|
|||||||
updateUser,
|
updateUser,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
doLogin,
|
doLogin,
|
||||||
|
isAdmin,
|
||||||
|
getUsersWithPagination,
|
||||||
} = require("../controllers/user");
|
} = require("../controllers/user");
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@@ -36,4 +38,10 @@ router.post("/update", updateUser);
|
|||||||
//Delete A uses Data:
|
//Delete A uses Data:
|
||||||
router.post("/delete", deleteUser);
|
router.post("/delete", deleteUser);
|
||||||
|
|
||||||
|
//Check admin status
|
||||||
|
router.get("/isAdmin/:id", isAdmin);
|
||||||
|
|
||||||
|
//Fetch user with pagination
|
||||||
|
router.get("/getUserWithPagination", getUsersWithPagination);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
10
frontend/package-lock.json
generated
10
frontend/package-lock.json
generated
@@ -13,6 +13,7 @@
|
|||||||
"lucide-react": "^0.477.0",
|
"lucide-react": "^0.477.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"react-icons": "^5.5.0",
|
||||||
"react-router-dom": "^7.2.0"
|
"react-router-dom": "^7.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -4369,6 +4370,15 @@
|
|||||||
"react": "^19.0.0"
|
"react": "^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-icons": {
|
||||||
|
"version": "5.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
|
||||||
|
"integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"lucide-react": "^0.477.0",
|
"lucide-react": "^0.477.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"react-icons": "^5.5.0",
|
||||||
"react-router-dom": "^7.2.0"
|
"react-router-dom": "^7.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
Routes,
|
Routes,
|
||||||
Route,
|
Route,
|
||||||
Navigate,
|
Navigate,
|
||||||
|
useLocation,
|
||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
import Navbar from "./components/Navbar";
|
import Navbar from "./components/Navbar";
|
||||||
import Home from "./pages/Home";
|
import Home from "./pages/Home";
|
||||||
@@ -13,6 +14,9 @@ import Transactions from "./pages/Transactions";
|
|||||||
import Favorites from "./pages/Favorites";
|
import Favorites from "./pages/Favorites";
|
||||||
import ProductDetail from "./pages/ProductDetail";
|
import ProductDetail from "./pages/ProductDetail";
|
||||||
import SearchPage from "./pages/SearchPage";
|
import SearchPage from "./pages/SearchPage";
|
||||||
|
import Dashboard from "./pages/Dashboard"; // The single consolidated dashboard component
|
||||||
|
import DashboardNav from "./components/DashboardNav";
|
||||||
|
import { verifyIsAdmin } from "./api/admin";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
// Authentication state - initialize from localStorage if available
|
// Authentication state - initialize from localStorage if available
|
||||||
@@ -35,6 +39,18 @@ function App() {
|
|||||||
useState(false);
|
useState(false);
|
||||||
const [recommendations, setRecommendations] = useState([]);
|
const [recommendations, setRecommendations] = useState([]);
|
||||||
|
|
||||||
|
// Admin state
|
||||||
|
const [isAdmin, setIsAdmin] = useState(false);
|
||||||
|
const [showAdminDashboard, setShowAdminDashboard] = useState(false);
|
||||||
|
|
||||||
|
// Check URL to determine if we're in admin mode
|
||||||
|
useEffect(() => {
|
||||||
|
// If URL contains /admin, set showAdminDashboard to true
|
||||||
|
if (window.location.pathname.includes("/admin")) {
|
||||||
|
setShowAdminDashboard(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
// New verification states
|
// New verification states
|
||||||
const [verificationStep, setVerificationStep] = useState("initial"); // 'initial', 'code-sent', 'verifying'
|
const [verificationStep, setVerificationStep] = useState("initial"); // 'initial', 'code-sent', 'verifying'
|
||||||
const [tempUserData, setTempUserData] = useState(null);
|
const [tempUserData, setTempUserData] = useState(null);
|
||||||
@@ -120,6 +136,28 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const userInfo = sessionStorage.getItem("user")
|
||||||
|
? JSON.parse(sessionStorage.getItem("user"))
|
||||||
|
: "";
|
||||||
|
const id = userInfo?.ID;
|
||||||
|
verifyIsAdmin(id).then((data) => {
|
||||||
|
setIsAdmin(data.isAdmin);
|
||||||
|
});
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
const handleShowAdminDashboard = () => {
|
||||||
|
setShowAdminDashboard(true);
|
||||||
|
// Update URL without reloading page
|
||||||
|
window.history.pushState({}, "", "/admin");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseAdminDashboard = () => {
|
||||||
|
setShowAdminDashboard(false);
|
||||||
|
// Update URL without reloading page
|
||||||
|
window.history.pushState({}, "", "/");
|
||||||
|
};
|
||||||
|
|
||||||
// Send verification code
|
// Send verification code
|
||||||
const sendVerificationCode = async (userData) => {
|
const sendVerificationCode = async (userData) => {
|
||||||
try {
|
try {
|
||||||
@@ -409,6 +447,7 @@ function App() {
|
|||||||
setVerificationStep("initial");
|
setVerificationStep("initial");
|
||||||
setTempUserData(null);
|
setTempUserData(null);
|
||||||
setRecommendations([]);
|
setRecommendations([]);
|
||||||
|
setShowAdminDashboard(false);
|
||||||
|
|
||||||
// Clear localStorage
|
// Clear localStorage
|
||||||
sessionStorage.removeItem("user");
|
sessionStorage.removeItem("user");
|
||||||
@@ -749,96 +788,117 @@ function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<div className="min-h-screen bg-gray-50">
|
{/* If admin dashboard should be shown */}
|
||||||
{/* Show loading overlay when generating recommendations */}
|
{showAdminDashboard ? (
|
||||||
{isGeneratingRecommendations && <LoadingOverlay />}
|
<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 */}
|
{/* Only show navbar when authenticated */}
|
||||||
{isAuthenticated && (
|
{isAuthenticated && (
|
||||||
<Navbar onLogout={handleLogout} userName={user?.name} />
|
<Navbar
|
||||||
)}
|
isAdmin={isAdmin}
|
||||||
<Routes>
|
onLogout={handleLogout}
|
||||||
{/* Public routes */}
|
userName={user?.name}
|
||||||
<Route
|
handleShowAdminDashboard={handleShowAdminDashboard}
|
||||||
path="/login"
|
/>
|
||||||
element={isAuthenticated ? <Navigate to="/" /> : <LoginComponent />}
|
)}
|
||||||
/>
|
<Routes>
|
||||||
{/* Protected routes */}
|
{/* Public routes */}
|
||||||
<Route
|
<Route
|
||||||
path="/"
|
path="/login"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute>
|
isAuthenticated ? <Navigate to="/" /> : <LoginComponent />
|
||||||
<div className="container mx-auto px-4 py-6">
|
}
|
||||||
<Home recommendations={recommendations} />
|
/>
|
||||||
</div>
|
{/* Protected routes */}
|
||||||
</ProtectedRoute>
|
<Route
|
||||||
}
|
path="/"
|
||||||
/>
|
element={
|
||||||
<Route
|
<ProtectedRoute>
|
||||||
path="/product/:id"
|
<div className="container mx-auto px-4 py-6">
|
||||||
element={
|
<Home recommendations={recommendations} />
|
||||||
<ProtectedRoute>
|
</div>
|
||||||
<ProductDetail />
|
</ProtectedRoute>
|
||||||
</ProtectedRoute>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Route
|
||||||
<Route
|
path="/product/:id"
|
||||||
path="/search"
|
element={
|
||||||
element={
|
<ProtectedRoute>
|
||||||
<ProtectedRoute>
|
<ProductDetail />
|
||||||
<div className="container mx-auto px-4 py-6">
|
</ProtectedRoute>
|
||||||
<SearchPage />
|
}
|
||||||
</div>
|
/>
|
||||||
</ProtectedRoute>
|
<Route
|
||||||
}
|
path="/search"
|
||||||
/>
|
element={
|
||||||
<Route
|
<ProtectedRoute>
|
||||||
path="/settings"
|
<div className="container mx-auto px-4 py-6">
|
||||||
element={
|
<SearchPage />
|
||||||
<ProtectedRoute>
|
</div>
|
||||||
<div className="container mx-auto px-4 py-6">
|
</ProtectedRoute>
|
||||||
<Settings />
|
}
|
||||||
</div>
|
/>
|
||||||
</ProtectedRoute>
|
<Route
|
||||||
}
|
path="/settings"
|
||||||
/>
|
element={
|
||||||
<Route
|
<ProtectedRoute>
|
||||||
path="/selling"
|
<div className="container mx-auto px-4 py-6">
|
||||||
element={
|
<Settings />
|
||||||
<ProtectedRoute>
|
</div>
|
||||||
<div className="container mx-auto px-4 py-6">
|
</ProtectedRoute>
|
||||||
<Selling />
|
}
|
||||||
</div>
|
/>
|
||||||
</ProtectedRoute>
|
<Route
|
||||||
}
|
path="/selling"
|
||||||
/>
|
element={
|
||||||
<Route
|
<ProtectedRoute>
|
||||||
path="/transactions"
|
<div className="container mx-auto px-4 py-6">
|
||||||
element={
|
<Selling />
|
||||||
<ProtectedRoute>
|
</div>
|
||||||
<div className="container mx-auto px-4 py-6">
|
</ProtectedRoute>
|
||||||
<Transactions />
|
}
|
||||||
</div>
|
/>
|
||||||
</ProtectedRoute>
|
<Route
|
||||||
}
|
path="/transactions"
|
||||||
/>
|
element={
|
||||||
<Route
|
<ProtectedRoute>
|
||||||
path="/favorites"
|
<div className="container mx-auto px-4 py-6">
|
||||||
element={
|
<Transactions />
|
||||||
<ProtectedRoute>
|
</div>
|
||||||
<div className="container mx-auto px-4 py-6">
|
</ProtectedRoute>
|
||||||
<Favorites />
|
}
|
||||||
</div>
|
/>
|
||||||
</ProtectedRoute>
|
<Route
|
||||||
}
|
path="/favorites"
|
||||||
/>
|
element={
|
||||||
{/* Redirect to login for any unmatched routes */}
|
<ProtectedRoute>
|
||||||
<Route
|
<div className="container mx-auto px-4 py-6">
|
||||||
path="*"
|
<Favorites />
|
||||||
element={<Navigate to={isAuthenticated ? "/" : "/login"} />}
|
</div>
|
||||||
/>
|
</ProtectedRoute>
|
||||||
</Routes>
|
}
|
||||||
</div>
|
/>
|
||||||
|
{/* Redirect to login for any unmatched routes */}
|
||||||
|
<Route
|
||||||
|
path="*"
|
||||||
|
element={<Navigate to={isAuthenticated ? "/" : "/login"} />}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
119
frontend/src/api/admin.js
Normal file
119
frontend/src/api/admin.js
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
// api.js
|
||||||
|
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const client = axios.create({
|
||||||
|
baseURL: "http://localhost:3030/api",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Users
|
||||||
|
export const getUsers = async (page, limit = 10) => {
|
||||||
|
try {
|
||||||
|
const { data } = await client.get(
|
||||||
|
`/user/getUserWithPagination?page=${page}&limit=${limit}`,
|
||||||
|
);
|
||||||
|
return { users: data.users, total: data.total };
|
||||||
|
} catch (error) {
|
||||||
|
return handleError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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, 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}`);
|
||||||
|
return { message: data.message };
|
||||||
|
} catch (error) {
|
||||||
|
return handleError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Transactions
|
||||||
|
export const getTransactions = async (page, limit = 10) => {
|
||||||
|
try {
|
||||||
|
const { data } = await client.get(
|
||||||
|
`/transaction/getTransactions?limit=${limit}&page=${page}`,
|
||||||
|
);
|
||||||
|
return { transactions: data.data, total: data.total };
|
||||||
|
} catch (error) {
|
||||||
|
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 { error: error.message || error };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Optional: export client if you want to use it elsewhere
|
||||||
|
export default client;
|
||||||
65
frontend/src/components/CategoryForm.jsx
Normal file
65
frontend/src/components/CategoryForm.jsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { MdAddBox } from "react-icons/md";
|
||||||
|
import { addCategory } from "../api/admin";
|
||||||
|
|
||||||
|
export default function CategoryForm({ visible, onAddCategory }) {
|
||||||
|
const [category, setCategory] = useState("");
|
||||||
|
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!category.trim()) {
|
||||||
|
document.getElementById("noti").innerHTML = "Category name is missing!";
|
||||||
|
document
|
||||||
|
.getElementById("noti")
|
||||||
|
.classList.add("bg-red-200", "text-red-500");
|
||||||
|
document.getElementById("noti").classList.remove("opacity-0");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addCategory(category)
|
||||||
|
.then((message) => {
|
||||||
|
document
|
||||||
|
.getElementById("noti")
|
||||||
|
.classList.remove("opacity-0", "bg-red-200", "text-red-500");
|
||||||
|
document
|
||||||
|
.getElementById("noti")
|
||||||
|
.classList.add("bg-green-200", "text-green-800");
|
||||||
|
document.getElementById("noti").innerHTML = `${message.message}`;
|
||||||
|
setCategory("");
|
||||||
|
onAddCategory();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = ({ target }) => {
|
||||||
|
setCategory(target.value);
|
||||||
|
if (target.value.trim())
|
||||||
|
document.getElementById("noti").classList.add("opacity-0");
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!visible) return;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} action="" className="flex p-2 items-center">
|
||||||
|
<label htmlFor="category" className="text-green-700">
|
||||||
|
Category:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="border border-green-700 ml-2 rounded-sm focus:bg-green-100 text-green-900"
|
||||||
|
name="category"
|
||||||
|
id="category"
|
||||||
|
onChange={handleChange}
|
||||||
|
value={category}
|
||||||
|
/>
|
||||||
|
<button type="submit" className="text-2xl pl-1 text-green-700">
|
||||||
|
<MdAddBox className="text-3xl" />
|
||||||
|
</button>
|
||||||
|
<p
|
||||||
|
id="noti"
|
||||||
|
className="text-red-500 bg-red-200 px-2 rounded-sm opacity-0 mx-2"
|
||||||
|
></p>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
15
frontend/src/components/DashboardNav.jsx
Normal file
15
frontend/src/components/DashboardNav.jsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
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">
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import { Link, useNavigate } from "react-router-dom";
|
|||||||
import UserDropdown from "./UserDropdown";
|
import UserDropdown from "./UserDropdown";
|
||||||
import { Search, Heart } from "lucide-react";
|
import { Search, Heart } from "lucide-react";
|
||||||
|
|
||||||
const Navbar = ({ onLogout, userName }) => {
|
const Navbar = ({ onLogout, userName, isAdmin, handleShowAdminDashboard }) => {
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -76,7 +76,12 @@ const Navbar = ({ onLogout, userName }) => {
|
|||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* User Profile */}
|
{/* User Profile */}
|
||||||
<UserDropdown onLogout={onLogout} userName={userName} />
|
<UserDropdown
|
||||||
|
isAdmin={isAdmin}
|
||||||
|
onLogout={onLogout}
|
||||||
|
userName={userName}
|
||||||
|
handleShowAdminDashboard={handleShowAdminDashboard}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
99
frontend/src/components/Pagination.jsx
Normal file
99
frontend/src/components/Pagination.jsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { NavLink } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function Pagination({ pageNum, onChange }) {
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
|
||||||
|
const pages = [];
|
||||||
|
for (let i = 1; i <= pageNum; i++) {
|
||||||
|
pages.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClick = (page) => {
|
||||||
|
setCurrentPage(page);
|
||||||
|
onChange(page);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTogglePage = (type) => {
|
||||||
|
let current = currentPage;
|
||||||
|
if (type == "next")
|
||||||
|
current = current + 1 <= pageNum ? current + 1 : current;
|
||||||
|
else current = current - 1 >= 1 ? current - 1 : current;
|
||||||
|
setCurrentPage(current);
|
||||||
|
onChange(current);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<nav aria-label="Page navigation" className="flex justify-end">
|
||||||
|
<ul className="flex items-center -space-x-px h-8 text-sm mt-4 pr-0 font-bold">
|
||||||
|
<li>
|
||||||
|
<NavLink
|
||||||
|
onClick={() => {
|
||||||
|
handleTogglePage("previous");
|
||||||
|
}}
|
||||||
|
className=" flex items-center justify-center px-3 h-8 ms-0 leading-tight border-e-0 border-gray-300 rounded-s-lg hover:bg-gray-100 text-white bg-green-700 border border-gray-300 hover:bg-green-600 hover:text-white"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Previous</span>
|
||||||
|
<svg
|
||||||
|
className="w-2.5 h-2.5 rtl:rotate-180"
|
||||||
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 6 10"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
d="M5 1 1 5l4 4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
{pages.map((page) => (
|
||||||
|
<li key={page}>
|
||||||
|
<NavLink
|
||||||
|
className={`${
|
||||||
|
currentPage == page ? "bg-green-600" : "bg-green-700"
|
||||||
|
} +
|
||||||
|
" flex items-center justify-center px-3 h-8 leading-tight text-white border border-gray-300 hover:bg-green-600 hover:text-white"`}
|
||||||
|
onClick={() => {
|
||||||
|
handleClick(page);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
<li>
|
||||||
|
<NavLink
|
||||||
|
onClick={() => {
|
||||||
|
handleTogglePage("next");
|
||||||
|
}}
|
||||||
|
className="flex items-center justify-center px-3 h-8 leading-tight border border-gray-300 rounded-e-lg text-white bg-green-700 border border-gray-300 hover:bg-green-600 hover:text-white"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Next</span>
|
||||||
|
<svg
|
||||||
|
className="w-2.5 h-2.5 rtl:rotate-180"
|
||||||
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 6 10"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
d="m1 9 4-4-4-4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { User, Settings, ShoppingBag, DollarSign, LogOut } from "lucide-react";
|
import { User, Settings, ShoppingBag, DollarSign, LogOut } from "lucide-react";
|
||||||
|
import { RiAdminLine } from "react-icons/ri";
|
||||||
|
|
||||||
const UserDropdown = ({ onLogout, userName }) => {
|
const UserDropdown = ({
|
||||||
|
onLogout,
|
||||||
|
userName,
|
||||||
|
isAdmin,
|
||||||
|
handleShowAdminDashboard,
|
||||||
|
}) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const dropdownRef = useRef(null);
|
const dropdownRef = useRef(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -89,6 +95,20 @@ const UserDropdown = ({ onLogout, userName }) => {
|
|||||||
Settings
|
Settings
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
{isAdmin ? (
|
||||||
|
<Link
|
||||||
|
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||||
|
onClick={() => {
|
||||||
|
handleShowAdminDashboard();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RiAdminLine className="h-4 w-4 mr-2 text-gray-500" />
|
||||||
|
Admin
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="flex w-full items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
className="flex w-full items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
|
|||||||
362
frontend/src/pages/Dashboard.jsx
Normal file
362
frontend/src/pages/Dashboard.jsx
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
import { useEffect, useState, useCallback } 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 { FaHome } from "react-icons/fa";
|
||||||
|
import Pagination from "../components/Pagination";
|
||||||
|
import CategoryForm from "../components/CategoryForm";
|
||||||
|
import DashboardNav from "../components/DashboardNav";
|
||||||
|
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-green-500 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 fetchItems = useCallback(
|
||||||
|
(page = 1, limit = 10) => {
|
||||||
|
setLoading(true);
|
||||||
|
fetchDataFn(page, limit)
|
||||||
|
.then((res) => {
|
||||||
|
const data =
|
||||||
|
res.users || res.products || res.transactions || res.data || [];
|
||||||
|
setItems(data);
|
||||||
|
setTotal(res.total);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error fetching data:", 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-green-600">{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-green-600">
|
||||||
|
<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-green-500 rounded-md px-4 py-2 text-white text-sm hover:bg-green-600 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">
|
||||||
|
<div className="mb-8 flex justify-between items-center w-full">
|
||||||
|
<h1 className="text-2xl md:text-3xl font-bold text-gray-800">
|
||||||
|
Admin Dashboard
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Tabs */}
|
||||||
|
<div className="md:hidden w-full mb-4">
|
||||||
|
<select
|
||||||
|
className="w-full rounded-md border-gray-300 shadow-sm focus:border-green-500 focus:ring focus:ring-green-500 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-green-700 bg-white border-l border-t border-r border-gray-200 border-b-0"
|
||||||
|
: "text-gray-600 hover:text-green-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>
|
||||||
|
);
|
||||||
|
}
|
||||||
124
frontend/src/schema.sql
Normal file
124
frontend/src/schema.sql
Normal 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
|
||||||
|
);
|
||||||
@@ -67,40 +67,42 @@ VALUES
|
|||||||
(1, TRUE, TRUE),
|
(1, TRUE, TRUE),
|
||||||
(2, TRUE, FALSE);
|
(2, TRUE, FALSE);
|
||||||
|
|
||||||
|
-- Insert Categories
|
||||||
-- Insert Categories
|
-- Insert Categories
|
||||||
INSERT INTO
|
INSERT INTO
|
||||||
Category (CategoryID, Name)
|
Category (Name)
|
||||||
VALUES
|
VALUES
|
||||||
(1, 'Textbooks'),
|
('Textbooks'),
|
||||||
(2, 'Electronics'),
|
('Electronics'),
|
||||||
(3, 'Furniture'),
|
('Furniture'),
|
||||||
(4, 'Clothing'),
|
('Clothing'),
|
||||||
(5, 'Sports Equipment'),
|
('Sports Equipment'),
|
||||||
(6, 'Musical Instruments'),
|
('Musical Instruments'),
|
||||||
(7, 'Art Supplies'),
|
('Art Supplies'),
|
||||||
(8, 'Kitchen Appliances'),
|
('Kitchen Appliances'),
|
||||||
(9, 'Gaming'),
|
('Gaming'),
|
||||||
(10, 'Bicycles'),
|
('Bicycles'),
|
||||||
(11, 'Computer Accessories'),
|
('Computer Accessories'),
|
||||||
(12, 'Stationery'),
|
('Stationery'),
|
||||||
(13, 'Fitness Equipment'),
|
('Fitness Equipment'),
|
||||||
(14, 'Winter Sports'),
|
('Winter Sports'),
|
||||||
(15, 'Lab Equipment'),
|
('Lab Equipment'),
|
||||||
(16, 'Camping Gear'),
|
('Camping Gear'),
|
||||||
(17, 'School Supplies'),
|
('School Supplies'),
|
||||||
(18, 'Office Furniture'),
|
('Office Furniture'),
|
||||||
(19, 'Books (Non-textbook)'),
|
('Books (-textbook)'),
|
||||||
(20, 'Math & Science Resources'),
|
('Math & Science Resources'),
|
||||||
(21, 'Engineering Tools'),
|
('Engineering Tools'),
|
||||||
(22, 'Backpacks & Bags'),
|
('Backpacks & Bags'),
|
||||||
(23, 'Audio Equipment'),
|
('Audio Equipment'),
|
||||||
(24, 'Dorm Essentials'),
|
('Dorm Essentials'),
|
||||||
(25, 'Smartphones & Tablets'),
|
('Smartphones & Tablets'),
|
||||||
(26, 'Winter Clothing'),
|
('Winter Clothing'),
|
||||||
(27, 'Photography Equipment'),
|
('Photography Equipment'),
|
||||||
(28, 'Event Tickets'),
|
('Event Tickets'),
|
||||||
(29, 'Software Licenses'),
|
('Software Licenses'),
|
||||||
(30, 'Transportation (Car Pool)');
|
('Transportation (Car Pool)'),
|
||||||
|
('Other');
|
||||||
|
|
||||||
-- Insert Products
|
-- Insert Products
|
||||||
INSERT INTO
|
INSERT INTO
|
||||||
|
|||||||
@@ -24,11 +24,10 @@ CREATE TABLE UserRole (
|
|||||||
|
|
||||||
-- Category Entity (must be created before Product or else error)
|
-- Category Entity (must be created before Product or else error)
|
||||||
CREATE TABLE Category (
|
CREATE TABLE Category (
|
||||||
CategoryID INT PRIMARY KEY,
|
CategoryID INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
Name VARCHAR(255) NOT NULL
|
Name VARCHAR(255) NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Product Entity
|
|
||||||
CREATE TABLE Product (
|
CREATE TABLE Product (
|
||||||
ProductID INT AUTO_INCREMENT PRIMARY KEY,
|
ProductID INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
Name VARCHAR(255) NOT NULL,
|
Name VARCHAR(255) NOT NULL,
|
||||||
@@ -42,11 +41,25 @@ CREATE TABLE Product (
|
|||||||
FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID)
|
FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- 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,
|
||||||
|
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID)
|
||||||
|
);
|
||||||
|
|
||||||
-- Fixed Image_URL table
|
-- Fixed Image_URL table
|
||||||
CREATE TABLE Image_URL (
|
CREATE TABLE Image_URL (
|
||||||
URL VARCHAR(255),
|
URL VARCHAR(255),
|
||||||
ProductID INT,
|
ProductID INT,
|
||||||
FOREIGN KEY (ProductID) REFERENCES Product (ProductID)
|
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Fixed Review Entity (Many-to-One with User, Many-to-One with Product)
|
-- Fixed Review Entity (Many-to-One with User, Many-to-One with Product)
|
||||||
@@ -60,8 +73,8 @@ CREATE TABLE Review (
|
|||||||
AND Rating <= 5
|
AND Rating <= 5
|
||||||
),
|
),
|
||||||
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (UserID) REFERENCES User (UserID),
|
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE SET NULL,
|
||||||
FOREIGN KEY (ProductID) REFERENCES Product (ProductID)
|
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Transaction Entity (Many-to-One with User, Many-to-One with Product)
|
-- Transaction Entity (Many-to-One with User, Many-to-One with Product)
|
||||||
@@ -71,8 +84,8 @@ CREATE TABLE Transaction (
|
|||||||
ProductID INT,
|
ProductID INT,
|
||||||
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
PaymentStatus VARCHAR(50),
|
PaymentStatus VARCHAR(50),
|
||||||
FOREIGN KEY (UserID) REFERENCES User (UserID),
|
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (ProductID) REFERENCES Product (ProductID)
|
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE SET NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Recommendation Entity (Many-to-One with User, Many-to-One with Product)
|
-- Recommendation Entity (Many-to-One with User, Many-to-One with Product)
|
||||||
@@ -81,8 +94,8 @@ CREATE TABLE Recommendation (
|
|||||||
UserID INT,
|
UserID INT,
|
||||||
RecommendedProductID INT,
|
RecommendedProductID INT,
|
||||||
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (UserID) REFERENCES User (UserID),
|
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (RecommendedProductID) REFERENCES Product (ProductID)
|
FOREIGN KEY (RecommendedProductID) REFERENCES Product (ProductID) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
-- History Entity (Many-to-One with User, Many-to-One with Product)
|
-- History Entity (Many-to-One with User, Many-to-One with Product)
|
||||||
@@ -91,8 +104,8 @@ CREATE TABLE History (
|
|||||||
UserID INT,
|
UserID INT,
|
||||||
ProductID INT,
|
ProductID INT,
|
||||||
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (UserID) REFERENCES User (UserID),
|
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (ProductID) REFERENCES Product (ProductID)
|
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Favorites Entity (Many-to-One with User, Many-to-One with Product)
|
-- Favorites Entity (Many-to-One with User, Many-to-One with Product)
|
||||||
@@ -100,8 +113,8 @@ CREATE TABLE Favorites (
|
|||||||
FavoriteID INT AUTO_INCREMENT PRIMARY KEY,
|
FavoriteID INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
UserID INT,
|
UserID INT,
|
||||||
ProductID INT,
|
ProductID INT,
|
||||||
FOREIGN KEY (UserID) REFERENCES User (UserID),
|
FOREIGN KEY (UserID) REFERENCES User (UserID) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (ProductID) REFERENCES Product (ProductID),
|
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE,
|
||||||
UNIQUE (UserID, ProductID)
|
UNIQUE (UserID, ProductID)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -110,8 +123,8 @@ CREATE TABLE Product_Category (
|
|||||||
ProductID INT,
|
ProductID INT,
|
||||||
CategoryID INT,
|
CategoryID INT,
|
||||||
PRIMARY KEY (ProductID, CategoryID),
|
PRIMARY KEY (ProductID, CategoryID),
|
||||||
FOREIGN KEY (ProductID) REFERENCES Product (ProductID),
|
FOREIGN KEY (ProductID) REFERENCES Product (ProductID) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID)
|
FOREIGN KEY (CategoryID) REFERENCES Category (CategoryID) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Login Authentication table
|
-- Login Authentication table
|
||||||
|
|||||||
Reference in New Issue
Block a user