Merge branch 'main' into aaqil
43
README.md
@@ -1,29 +1,42 @@
|
|||||||
### 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 `name-some branch name`.
|
2. Make a brach with the following naming conventionp, prefix it with your name ```Your-Name Branch-Name```.
|
||||||
|
---
|
||||||
|
|
||||||
### `frontend`
|
### Frontend
|
||||||
- Use React Js and vite as the node manger
|
1. `cd frontend` into the dir and then type command
|
||||||
```Bash
|
```Bash
|
||||||
npm install
|
1. npm install #Installs the needed packages
|
||||||
|
2. npm run dev #Start The Server
|
||||||
```
|
```
|
||||||
2. **Start The Server**, `cd frontend` into the dir and then type command
|
---
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
1. `cd backend` into the dir and then type command
|
||||||
```Bash
|
```Bash
|
||||||
npm run dev
|
1. npm install #Installs the needed packages
|
||||||
|
2. npm run dev #Start The Server
|
||||||
```
|
```
|
||||||
### `backend`
|
---
|
||||||
1. Install the needed lib with the command bellow
|
|
||||||
|
### Recommendation
|
||||||
|
1. `cd recommendation-engine` into the dir and then type command
|
||||||
```Bash
|
```Bash
|
||||||
npm install
|
1. python3 server.py #Start The Server
|
||||||
```
|
```
|
||||||
2. **Start The Server**, `cd backend` into the dir and then type command
|
---
|
||||||
|
### Recommendation system
|
||||||
|
1. Install the dependencies
|
||||||
```Bash
|
```Bash
|
||||||
npm run dev
|
pip install mysql.connector
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### Database
|
### Database
|
||||||
1. To Create the DB use the command bellow
|
1. MySql Version 9.2.0
|
||||||
|
2. To Create the DataBase use the command bellow:
|
||||||
```Bash
|
```Bash
|
||||||
python3 ./SQL_code/init-db.py
|
1. mysql -u root
|
||||||
|
2. \. PathToYour/Schema.sql
|
||||||
|
3. \. PathToYour/Init-Data.sql
|
||||||
```
|
```
|
||||||
- MySql Version 9.2.0
|
|
||||||
|
|||||||
90
backend/controllers/history.js
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
const db = require("../utils/database");
|
||||||
|
|
||||||
|
exports.HistoryByUserId = async (req, res) => {
|
||||||
|
const { id } = req.body;
|
||||||
|
try {
|
||||||
|
const [data] = await db.execute(
|
||||||
|
`
|
||||||
|
WITH RankedImages AS (
|
||||||
|
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,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY P.ProductID ORDER BY I.URL) AS RowNum
|
||||||
|
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 History H ON H.ProductID = P.ProductID
|
||||||
|
WHERE H.UserID = ?
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
ProductID,
|
||||||
|
ProductName,
|
||||||
|
Price,
|
||||||
|
DateUploaded,
|
||||||
|
SellerName,
|
||||||
|
ProductImage,
|
||||||
|
Category
|
||||||
|
FROM RankedImages
|
||||||
|
WHERE RowNum = 1;
|
||||||
|
`,
|
||||||
|
[id],
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: "Products fetched successfully",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error finding products:", error);
|
||||||
|
return res.status(500).json({
|
||||||
|
found: false,
|
||||||
|
error: "Database error occurred",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.AddHistory = 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 History (UserID, ProductID) VALUES (?, ?)`,
|
||||||
|
[userID, productID],
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: "Product added to history successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error adding favorite product:", error);
|
||||||
|
return res.json({ error: "Could not add favorite product" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.DelHistory = async (req, res) => {
|
||||||
|
const { userID, productID } = req.body;
|
||||||
|
console.log(userID);
|
||||||
|
try {
|
||||||
|
// Use parameterized query to prevent SQL injection
|
||||||
|
const [result] = await db.execute(`DELETE FROM History WHERE UserID=?`, [
|
||||||
|
userID,
|
||||||
|
]);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: "Product deleted from History successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error adding favorite product:", error);
|
||||||
|
return res.json({ error: "Could not add favorite product" });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,13 +1,45 @@
|
|||||||
const db = require("../utils/database");
|
const db = require("../utils/database");
|
||||||
|
|
||||||
exports.addToFavorite = async (req, res) => {
|
exports.addProduct = async (req, res) => {
|
||||||
const { userID, productsID } = req.body;
|
const { userID, name, price, qty, description, category, images } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [result] = await db.execute(
|
||||||
|
`INSERT INTO Product (Name, Price, StockQuantity, UserID, Description, CategoryID) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||||
|
[name, price, qty, userID, description, category],
|
||||||
|
);
|
||||||
|
|
||||||
|
const productID = result.insertId;
|
||||||
|
if (images && images.length > 0) {
|
||||||
|
const imageInsertPromises = images.map((imagePath) =>
|
||||||
|
db.execute(`INSERT INTO Image_URL (URL, ProductID) VALUES (?, ?)`, [
|
||||||
|
imagePath,
|
||||||
|
productID,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(imageInsertPromises); //perallel
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: "Product and images added successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error adding product or images:", error);
|
||||||
|
console.log(error);
|
||||||
|
return res.json({ error: "Could not add product or images" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.addFavorite = async (req, res) => {
|
||||||
|
const { userID, productID } = req.body;
|
||||||
|
console.log(userID);
|
||||||
try {
|
try {
|
||||||
// Use parameterized query to prevent SQL injection
|
// Use parameterized query to prevent SQL injection
|
||||||
const [result] = await db.execute(
|
const [result] = await db.execute(
|
||||||
"INSERT INTO Favorites (UserID, ProductID) VALUES (?, ?)",
|
`INSERT INTO Favorites (UserID, ProductID) VALUES (?, ?)`,
|
||||||
[userID, productsID],
|
[userID, productID],
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -20,13 +52,93 @@ exports.addToFavorite = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.removeFavorite = async (req, res) => {
|
||||||
|
const { userID, productID } = req.body;
|
||||||
|
console.log(userID);
|
||||||
|
try {
|
||||||
|
// Use parameterized query to prevent SQL injection
|
||||||
|
const [result] = await db.execute(
|
||||||
|
`DELETE FROM Favorites WHERE UserID = ? AND ProductID = ?`,
|
||||||
|
[userID, productID],
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: "Product removed from favorites successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error removing favorite product:", error);
|
||||||
|
return res.json({ error: "Could not remove favorite product" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.getFavorites = async (req, res) => {
|
||||||
|
const { userID } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [favorites] = 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 Favorites f
|
||||||
|
JOIN Product p ON f.ProductID = p.ProductID
|
||||||
|
JOIN User u ON p.UserID = u.UserID
|
||||||
|
LEFT JOIN Image_URL i ON p.ProductID = i.ProductID
|
||||||
|
WHERE f.UserID = ?
|
||||||
|
GROUP BY
|
||||||
|
p.ProductID,
|
||||||
|
p.Name,
|
||||||
|
p.Description,
|
||||||
|
p.Price,
|
||||||
|
p.CategoryID,
|
||||||
|
p.UserID,
|
||||||
|
p.Date,
|
||||||
|
u.Name;
|
||||||
|
`,
|
||||||
|
[userID],
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
favorites: favorites,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error retrieving favorites:", error);
|
||||||
|
res.status(500).json({ error: "Could not retrieve favorite products" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Get all products along with their image URLs
|
// Get all products along with their image URLs
|
||||||
exports.getAllProducts = async (req, res) => {
|
exports.getAllProducts = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [data, fields] = await db.execute(`
|
const [data, fields] = await db.execute(`
|
||||||
SELECT p.*, i.URL
|
SELECT
|
||||||
FROM Product p
|
P.ProductID,
|
||||||
LEFT JOIN Image_URL i ON p.ProductID = i.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
|
||||||
|
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
|
||||||
|
GROUP BY
|
||||||
|
P.ProductID,
|
||||||
|
P.Name,
|
||||||
|
P.Price,
|
||||||
|
P.Date,
|
||||||
|
U.Name,
|
||||||
|
C.Name;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -43,44 +155,58 @@ exports.getAllProducts = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get a single product by ID along with image URLs
|
|
||||||
exports.getProductById = async (req, res) => {
|
exports.getProductById = async (req, res) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
console.log(id);
|
console.log("Received Product ID:", id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [data] = await db.execute(
|
const [data] = await db.execute(
|
||||||
`
|
`
|
||||||
SELECT p.*, i.URL AS image_url
|
SELECT p.*,U.Name AS SellerName,U.Email as SellerEmail,U.Phone as SellerPhone, i.URL AS image_url
|
||||||
FROM Product p
|
FROM Product p
|
||||||
LEFT JOIN Image_URL i ON p.ProductID = i.ProductID
|
LEFT JOIN Image_URL i ON p.ProductID = i.ProductID
|
||||||
|
JOIN User U ON p.UserID = U.UserID
|
||||||
WHERE p.ProductID = ?
|
WHERE p.ProductID = ?
|
||||||
`,
|
`,
|
||||||
[id],
|
[id],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Log raw data for debugging
|
||||||
|
console.log("Raw Database Result:", data);
|
||||||
|
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
|
console.log("No product found with ID:", id);
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Product not found",
|
message: "Product not found",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assuming that `data` contains product information and the image URLs
|
// Collect all image URLs
|
||||||
|
const images = data
|
||||||
|
.map((row) => row.image_url)
|
||||||
|
.filter((url) => url !== null);
|
||||||
|
|
||||||
|
// Create product object with all details from first row and collected images
|
||||||
const product = {
|
const product = {
|
||||||
...data[0], // First product found in the query
|
...data[0], // Base product details
|
||||||
images: data.map((image) => image.image_url), // Collect all image URLs into an array
|
images: images, // Collected image URLs
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Log processed product for debugging
|
||||||
|
console.log("Processed Product:", product);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: "Product fetched successfully",
|
message: "Product fetched successfully",
|
||||||
data: product,
|
data: product,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching product:", error);
|
console.error("Full Error Details:", error);
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "Database error occurred",
|
message: "Database error occurred",
|
||||||
|
error: error.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
53
backend/controllers/recommendation.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
const db = require("../utils/database");
|
||||||
|
|
||||||
|
// TODO: Get the recommondaed product given the userID
|
||||||
|
exports.RecommondationByUserId = async (req, res) => {
|
||||||
|
const { id } = req.body;
|
||||||
|
try {
|
||||||
|
const [data, fields] = await db.execute(
|
||||||
|
`
|
||||||
|
WITH RankedImages AS (
|
||||||
|
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,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY P.ProductID ORDER BY I.URL) AS RowNum
|
||||||
|
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 Recommendation R ON P.ProductID = R.RecommendedProductID
|
||||||
|
WHERE R.UserID = ?
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
ProductID,
|
||||||
|
ProductName,
|
||||||
|
Price,
|
||||||
|
DateUploaded,
|
||||||
|
SellerName,
|
||||||
|
ProductImage,
|
||||||
|
Category
|
||||||
|
FROM RankedImages
|
||||||
|
WHERE RowNum = 1;
|
||||||
|
`,
|
||||||
|
[id],
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(data);
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: "Products fetched successfully",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error finding products:", error);
|
||||||
|
return res.status(500).json({
|
||||||
|
found: false,
|
||||||
|
error: "Database error occurred",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
302
backend/controllers/review.js
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
const db = require("../utils/database");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get reviews for a specific product
|
||||||
|
* Returns both reviews for the product and reviews by the product owner for other products
|
||||||
|
*/
|
||||||
|
exports.getReviews = async (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
console.log("Received Product ID:", id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First query: Get reviews for this specific product
|
||||||
|
const [productReviews] = await db.execute(
|
||||||
|
`SELECT
|
||||||
|
R.ReviewID,
|
||||||
|
R.UserID,
|
||||||
|
R.ProductID,
|
||||||
|
R.Comment,
|
||||||
|
R.Rating,
|
||||||
|
R.Date AS ReviewDate,
|
||||||
|
U.Name AS ReviewerName,
|
||||||
|
P.Name AS ProductName,
|
||||||
|
'product' AS ReviewType
|
||||||
|
FROM Review R
|
||||||
|
JOIN User U ON R.UserID = U.UserID
|
||||||
|
JOIN Product P ON R.ProductID = P.ProductID
|
||||||
|
WHERE R.ProductID = ?`,
|
||||||
|
[id],
|
||||||
|
);
|
||||||
|
|
||||||
|
// // Second query: Get reviews written by the product owner for other products
|
||||||
|
// const [sellerReviews] = await db.execute(
|
||||||
|
// `SELECT
|
||||||
|
// R.ReviewID,
|
||||||
|
// R.UserID,
|
||||||
|
// R.ProductID,
|
||||||
|
// R.Comment,
|
||||||
|
// R.Rating,
|
||||||
|
// R.Date AS ReviewDate,
|
||||||
|
// U.Name AS ReviewerName,
|
||||||
|
// P.Name AS ProductName,
|
||||||
|
// 'seller' AS ReviewType
|
||||||
|
// FROM Review R
|
||||||
|
// JOIN User U ON R.UserID = U.UserID
|
||||||
|
// JOIN Product P ON R.ProductID = P.ProductID
|
||||||
|
// WHERE R.UserID = (
|
||||||
|
// SELECT UserID
|
||||||
|
// FROM Product
|
||||||
|
// WHERE ProductID = ?
|
||||||
|
// )
|
||||||
|
// AND R.ProductID != ?`,
|
||||||
|
// [id, id],
|
||||||
|
// );
|
||||||
|
|
||||||
|
// Combine the results
|
||||||
|
const combinedReviews = [...productReviews];
|
||||||
|
|
||||||
|
// Log data for debugging
|
||||||
|
console.log("Combined Reviews:", combinedReviews);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: "Reviews fetched successfully",
|
||||||
|
data: combinedReviews,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Full Error Details:", error);
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Database error occurred",
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit a new review for a product
|
||||||
|
*/
|
||||||
|
exports.submitReview = async (req, res) => {
|
||||||
|
const { productId, userId, rating, comment } = req.body;
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!productId || !userId || !rating || !comment) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: "Missing required fields",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate rating is between 1 and 5
|
||||||
|
if (rating < 1 || rating > 5) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: "Rating must be between 1 and 5",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if user has already reviewed this product
|
||||||
|
const [existingReview] = await db.execute(
|
||||||
|
`SELECT ReviewID FROM Review WHERE ProductID = ? AND UserID = ?`,
|
||||||
|
[productId, userId],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingReview.length > 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: "You have already reviewed this product",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user is trying to review their own product
|
||||||
|
const [productOwner] = await db.execute(
|
||||||
|
`SELECT UserID FROM Product WHERE ProductID = ?`,
|
||||||
|
[productId],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (productOwner.length > 0 && productOwner[0].UserID === userId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: "You cannot review your own product",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the review into the database
|
||||||
|
const [result] = await db.execute(
|
||||||
|
`INSERT INTO Review (
|
||||||
|
ProductID,
|
||||||
|
UserID,
|
||||||
|
Rating,
|
||||||
|
Comment,
|
||||||
|
Date
|
||||||
|
) VALUES (?, ?, ?, ?, NOW())`,
|
||||||
|
[productId, userId, rating, comment],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get the inserted review id
|
||||||
|
const reviewId = result.insertId;
|
||||||
|
|
||||||
|
// Fetch the newly created review to return to client
|
||||||
|
const [newReview] = await db.execute(
|
||||||
|
`SELECT
|
||||||
|
R.ReviewID,
|
||||||
|
R.ProductID,
|
||||||
|
R.UserID,
|
||||||
|
R.Rating,
|
||||||
|
R.Comment,
|
||||||
|
R.Date AS ReviewDate,
|
||||||
|
U.Name AS ReviewerName,
|
||||||
|
P.Name AS ProductName
|
||||||
|
FROM Review R
|
||||||
|
JOIN User U ON R.UserID = U.UserID
|
||||||
|
JOIN Product P ON R.ProductID = P.ProductID
|
||||||
|
WHERE R.ReviewID = ?`,
|
||||||
|
[reviewId],
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true, // Fixed from false to true
|
||||||
|
message: "Review submitted successfully",
|
||||||
|
data: newReview[0],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error submitting review:", error);
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Database error occurred",
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Update an existing review
|
||||||
|
// */
|
||||||
|
// exports.updateReview = async (req, res) => {
|
||||||
|
// const { reviewId } = req.params;
|
||||||
|
// const { rating, comment } = req.body;
|
||||||
|
// const userId = req.body.userId; // Assuming you have middleware that validates the user
|
||||||
|
|
||||||
|
// // Validate required fields
|
||||||
|
// if (!reviewId || !rating || !comment) {
|
||||||
|
// return res.status(400).json({
|
||||||
|
// success: false,
|
||||||
|
// message: "Missing required fields",
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Validate rating is between 1 and 5
|
||||||
|
// if (rating < 1 || rating > 5) {
|
||||||
|
// return res.status(400).json({
|
||||||
|
// success: false,
|
||||||
|
// message: "Rating must be between 1 and 5",
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// // Check if review exists and belongs to the user
|
||||||
|
// const [existingReview] = await db.execute(
|
||||||
|
// `SELECT ReviewID, UserID FROM Review WHERE ReviewID = ?`,
|
||||||
|
// [reviewId],
|
||||||
|
// );
|
||||||
|
|
||||||
|
// if (existingReview.length === 0) {
|
||||||
|
// return res.status(404).json({
|
||||||
|
// success: false,
|
||||||
|
// message: "Review not found",
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (existingReview[0].UserID !== userId) {
|
||||||
|
// return res.status(403).json({
|
||||||
|
// success: false,
|
||||||
|
// message: "You can only update your own reviews",
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Update the review
|
||||||
|
// await db.execute(
|
||||||
|
// `UPDATE Review
|
||||||
|
// SET Rating = ?, Comment = ?, Date = NOW()
|
||||||
|
// WHERE ReviewID = ?`,
|
||||||
|
// [rating, comment, reviewId],
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // Fetch the updated review
|
||||||
|
// const [updatedReview] = await db.execute(
|
||||||
|
// `SELECT
|
||||||
|
// R.ReviewID,
|
||||||
|
// R.ProductID,
|
||||||
|
// R.UserID,
|
||||||
|
// R.Rating,
|
||||||
|
// R.Comment,
|
||||||
|
// R.Date AS ReviewDate,
|
||||||
|
// U.Name AS ReviewerName,
|
||||||
|
// P.Name AS ProductName
|
||||||
|
// FROM Review R
|
||||||
|
// JOIN User U ON R.UserID = U.UserID
|
||||||
|
// JOIN Product P ON R.ProductID = P.ProductID
|
||||||
|
// WHERE R.ReviewID = ?`,
|
||||||
|
// [reviewId],
|
||||||
|
// );
|
||||||
|
|
||||||
|
// res.json({
|
||||||
|
// success: true,
|
||||||
|
// message: "Review updated successfully",
|
||||||
|
// data: updatedReview[0],
|
||||||
|
// });
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error("Error updating review:", error);
|
||||||
|
// return res.status(500).json({
|
||||||
|
// success: false,
|
||||||
|
// message: "Database error occurred",
|
||||||
|
// error: error.message,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Delete a review
|
||||||
|
// */
|
||||||
|
// exports.deleteReview = async (req, res) => {
|
||||||
|
// const { reviewId } = req.params;
|
||||||
|
// const userId = req.body.userId; // Assuming you have middleware that validates the user
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// // Check if review exists and belongs to the user
|
||||||
|
// const [existingReview] = await db.execute(
|
||||||
|
// `SELECT ReviewID, UserID FROM Review WHERE ReviewID = ?`,
|
||||||
|
// [reviewId],
|
||||||
|
// );
|
||||||
|
|
||||||
|
// if (existingReview.length === 0) {
|
||||||
|
// return res.status(404).json({
|
||||||
|
// success: false,
|
||||||
|
// message: "Review not found",
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (existingReview[0].UserID !== userId) {
|
||||||
|
// return res.status(403).json({
|
||||||
|
// success: false,
|
||||||
|
// message: "You can only delete your own reviews",
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Delete the review
|
||||||
|
// await db.execute(`DELETE FROM Review WHERE ReviewID = ?`, [reviewId]);
|
||||||
|
|
||||||
|
// res.json({
|
||||||
|
// success: true,
|
||||||
|
// message: "Review deleted successfully",
|
||||||
|
// });
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error("Error deleting review:", error);
|
||||||
|
// return res.status(500).json({
|
||||||
|
// success: false,
|
||||||
|
// message: "Database error occurred",
|
||||||
|
// error: error.message,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// };
|
||||||
164
backend/controllers/search.js
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
const db = require("../utils/database");
|
||||||
|
|
||||||
|
exports.searchProductsByName = async (req, res) => {
|
||||||
|
const { name } = req.query;
|
||||||
|
|
||||||
|
if (name.length === 0) {
|
||||||
|
console.log("Searching for products with no name", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Searching for products with name:", name);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Modify SQL to return all products when no search term is provided
|
||||||
|
const sql = `
|
||||||
|
SELECT p.*, i.URL as image
|
||||||
|
FROM Product p
|
||||||
|
LEFT JOIN Image_URL i ON p.ProductID = i.ProductID
|
||||||
|
${name ? "WHERE p.Name LIKE ?" : ""}
|
||||||
|
ORDER BY p.ProductID
|
||||||
|
`;
|
||||||
|
|
||||||
|
const params = name ? [`%${name}%`] : [];
|
||||||
|
console.log("Executing SQL:", sql);
|
||||||
|
console.log("With parameters:", params);
|
||||||
|
|
||||||
|
const [data] = await db.execute(sql, params);
|
||||||
|
|
||||||
|
console.log("Raw Database Result:", data);
|
||||||
|
|
||||||
|
if (data.length === 0) {
|
||||||
|
console.log("No products found matching:", name);
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: "No products found matching your search",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group products by ProductID to handle multiple images per product
|
||||||
|
const productsMap = new Map();
|
||||||
|
|
||||||
|
data.forEach((row) => {
|
||||||
|
if (!productsMap.has(row.ProductID)) {
|
||||||
|
const product = {
|
||||||
|
ProductID: row.ProductID,
|
||||||
|
Name: row.Name,
|
||||||
|
Description: row.Description,
|
||||||
|
Price: row.Price,
|
||||||
|
images: row.image,
|
||||||
|
};
|
||||||
|
productsMap.set(row.ProductID, product);
|
||||||
|
} else if (row.image_url) {
|
||||||
|
productsMap.get(row.ProductID).images.push(row.image_url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const products = Array.from(productsMap.values());
|
||||||
|
|
||||||
|
console.log("Processed Products:", products);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: "Products fetched successfully",
|
||||||
|
data: products,
|
||||||
|
count: products.length,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Database Error:", error);
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: "Database error occurred",
|
||||||
|
error: error.message || "Unknown database error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// exports.searchProductsByName = async (req, res) => {
|
||||||
|
// const { name } = req.query;
|
||||||
|
|
||||||
|
// // Add better validation and error handling
|
||||||
|
// if (!name || typeof name !== "string") {
|
||||||
|
// return res.status(400).json({
|
||||||
|
// success: false,
|
||||||
|
// message: "Valid search term is required",
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// console.log("Searching for products with name:", name);
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// // Log the SQL query and parameters for debugging
|
||||||
|
// const sql = `
|
||||||
|
// SELECT p.*, i.URL AS image_url
|
||||||
|
// FROM Product p
|
||||||
|
// LEFT JOIN Image_URL i ON p.ProductID = i.ProductID
|
||||||
|
// WHERE p.Name LIKE ?
|
||||||
|
// `;
|
||||||
|
// const params = [`%${name}%`];
|
||||||
|
// console.log("Executing SQL:", sql);
|
||||||
|
// console.log("With parameters:", params);
|
||||||
|
|
||||||
|
// const [data] = await db.execute(sql, params);
|
||||||
|
|
||||||
|
// // Log raw data for debugging
|
||||||
|
// console.log("Raw Database Result:", data);
|
||||||
|
|
||||||
|
// if (data.length === 0) {
|
||||||
|
// console.log("No products found matching:", name);
|
||||||
|
// return res.status(404).json({
|
||||||
|
// success: false,
|
||||||
|
// message: "No products found matching your search",
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Group products by ProductID to handle multiple images per product
|
||||||
|
// const productsMap = new Map();
|
||||||
|
|
||||||
|
// data.forEach((row) => {
|
||||||
|
// if (!productsMap.has(row.ProductID)) {
|
||||||
|
// // Create a clean object without circular references
|
||||||
|
// const product = {
|
||||||
|
// ProductID: row.ProductID,
|
||||||
|
// Name: row.Name,
|
||||||
|
// Description: row.Description,
|
||||||
|
// Price: row.Price,
|
||||||
|
// // Add any other product fields you need
|
||||||
|
// images: row.image_url ? [row.image_url] : [],
|
||||||
|
// };
|
||||||
|
// productsMap.set(row.ProductID, product);
|
||||||
|
// } else if (row.image_url) {
|
||||||
|
// // Add additional image to existing product
|
||||||
|
// productsMap.get(row.ProductID).images.push(row.image_url);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // Convert map to array of products
|
||||||
|
// const products = Array.from(productsMap.values());
|
||||||
|
|
||||||
|
// // Log processed products for debugging
|
||||||
|
// console.log("Processed Products:", products);
|
||||||
|
|
||||||
|
// res.json({
|
||||||
|
// success: true,
|
||||||
|
// message: "Products fetched successfully",
|
||||||
|
// data: products,
|
||||||
|
// count: products.length,
|
||||||
|
// });
|
||||||
|
// } catch (error) {
|
||||||
|
// // Enhanced error logging
|
||||||
|
// console.error("Database Error Details:", {
|
||||||
|
// message: error.message,
|
||||||
|
// code: error.code,
|
||||||
|
// errno: error.errno,
|
||||||
|
// sqlState: error.sqlState,
|
||||||
|
// sqlMessage: error.sqlMessage,
|
||||||
|
// sql: error.sql,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return res.status(500).json({
|
||||||
|
// success: false,
|
||||||
|
// message: "Database error occurred",
|
||||||
|
// error: error.message || "Unknown database error",
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// };
|
||||||
@@ -134,6 +134,62 @@ exports.completeSignUp = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.doLogin = async (req, res) => {
|
||||||
|
const { email, password } = req.body;
|
||||||
|
|
||||||
|
// Input validation
|
||||||
|
if (!email || !password) {
|
||||||
|
return res.status(400).json({
|
||||||
|
found: false,
|
||||||
|
error: "Email and password are required",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Query to find user with matching email
|
||||||
|
const query = "SELECT * FROM User WHERE email = ?";
|
||||||
|
const [data, fields] = await db.execute(query, [email]);
|
||||||
|
|
||||||
|
// Check if user was found
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
const user = data[0];
|
||||||
|
|
||||||
|
// Verify password match
|
||||||
|
if (user.Password === password) {
|
||||||
|
// Consider using bcrypt for secure password comparison
|
||||||
|
// Return user data without password
|
||||||
|
return res.json({
|
||||||
|
found: true,
|
||||||
|
userID: user.UserID,
|
||||||
|
name: user.Name,
|
||||||
|
email: user.Email,
|
||||||
|
UCID: user.UCID,
|
||||||
|
phone: user.Phone,
|
||||||
|
address: user.Address,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Password doesn't match
|
||||||
|
return res.json({
|
||||||
|
found: false,
|
||||||
|
error: "Invalid email or password",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// User not found
|
||||||
|
return res.json({
|
||||||
|
found: false,
|
||||||
|
error: "Invalid email or password",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error logging in:", error);
|
||||||
|
return res.status(500).json({
|
||||||
|
found: false,
|
||||||
|
error: "Database error occurred",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
exports.getAllUser = async (req, res) => {
|
exports.getAllUser = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [users, fields] = await db.execute("SELECT * FROM User;");
|
const [users, fields] = await db.execute("SELECT * FROM User;");
|
||||||
@@ -174,6 +230,7 @@ exports.findUserByEmail = async (req, res) => {
|
|||||||
UCID: user.UCID,
|
UCID: user.UCID,
|
||||||
phone: user.Phone,
|
phone: user.Phone,
|
||||||
address: user.Address,
|
address: user.Address,
|
||||||
|
password: user.Password,
|
||||||
// Include any other fields your user might have
|
// Include any other fields your user might have
|
||||||
// Make sure the field names match exactly with your database column names
|
// Make sure the field names match exactly with your database column names
|
||||||
});
|
});
|
||||||
@@ -201,7 +258,7 @@ exports.updateUser = async (req, res) => {
|
|||||||
const phone = req.body?.phone;
|
const phone = req.body?.phone;
|
||||||
const UCID = req.body?.UCID;
|
const UCID = req.body?.UCID;
|
||||||
const address = req.body?.address;
|
const address = req.body?.address;
|
||||||
|
const password = req.body?.password;
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return res.status(400).json({ error: "User ID is required" });
|
return res.status(400).json({ error: "User ID is required" });
|
||||||
}
|
}
|
||||||
@@ -213,7 +270,7 @@ exports.updateUser = async (req, res) => {
|
|||||||
if (phone) updateData.phone = phone;
|
if (phone) updateData.phone = phone;
|
||||||
if (UCID) updateData.UCID = UCID;
|
if (UCID) updateData.UCID = UCID;
|
||||||
if (address) updateData.address = address;
|
if (address) updateData.address = address;
|
||||||
|
if (password) updateData.password = password;
|
||||||
if (Object.keys(updateData).length === 0) {
|
if (Object.keys(updateData).length === 0) {
|
||||||
return res.status(400).json({ error: "No valid fields to update" });
|
return res.status(400).json({ error: "No valid fields to update" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const cors = require("cors");
|
const cors = require("cors");
|
||||||
//Get the db connection
|
|
||||||
const db = require("./utils/database");
|
const db = require("./utils/database");
|
||||||
|
|
||||||
const userRouter = require("./routes/user");
|
const userRouter = require("./routes/user");
|
||||||
const productRouter = require("./routes/product");
|
const productRouter = require("./routes/product");
|
||||||
|
const searchRouter = require("./routes/search");
|
||||||
|
const recommendedRouter = require("./routes/recommendation");
|
||||||
|
const history = require("./routes/history");
|
||||||
|
const review = require("./routes/review");
|
||||||
|
|
||||||
const { generateEmailTransporter } = require("./utils/mail");
|
const { generateEmailTransporter } = require("./utils/mail");
|
||||||
const {
|
const {
|
||||||
cleanupExpiredCodes,
|
cleanupExpiredCodes,
|
||||||
@@ -28,15 +33,20 @@ transporter
|
|||||||
console.error("Email connection failed:", error);
|
console.error("Email connection failed:", error);
|
||||||
});
|
});
|
||||||
|
|
||||||
//Check database connection
|
|
||||||
checkDatabaseConnection(db);
|
checkDatabaseConnection(db);
|
||||||
|
|
||||||
//Routes
|
//Routes
|
||||||
app.use("/api/user", userRouter); //prefix with /api/user
|
app.use("/api/user", userRouter);
|
||||||
app.use("/api/product", productRouter); //prefix with /api/product
|
app.use("/api/product", productRouter);
|
||||||
|
app.use("/api/search", searchRouter);
|
||||||
|
app.use("/api/engine", recommendedRouter);
|
||||||
|
app.use("/api/history", history);
|
||||||
|
app.use("/api/review", review);
|
||||||
|
|
||||||
|
|
||||||
// Set up a scheduler to run cleanup every hour
|
// Set up a scheduler to run cleanup every hour
|
||||||
setInterval(cleanupExpiredCodes, 60 * 60 * 1000);
|
clean_up_time = 30*60*1000;
|
||||||
|
setInterval(cleanupExpiredCodes, clean_up_time);
|
||||||
|
|
||||||
app.listen(3030, () => {
|
app.listen(3030, () => {
|
||||||
console.log(`Running Backend on http://localhost:3030/`);
|
console.log(`Running Backend on http://localhost:3030/`);
|
||||||
|
|||||||
14
backend/routes/history.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// routes/product.js
|
||||||
|
const express = require("express");
|
||||||
|
const {
|
||||||
|
HistoryByUserId,
|
||||||
|
DelHistory,
|
||||||
|
AddHistory,
|
||||||
|
} = require("../controllers/history");
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post("/getHistory", HistoryByUserId);
|
||||||
|
router.post("/delHistory", DelHistory);
|
||||||
|
router.post("/addHistory", AddHistory);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -1,16 +1,27 @@
|
|||||||
|
// routes/product.js
|
||||||
const express = require("express");
|
const express = require("express");
|
||||||
const {
|
const {
|
||||||
addToFavorite,
|
addFavorite,
|
||||||
|
getFavorites,
|
||||||
|
removeFavorite,
|
||||||
getAllProducts,
|
getAllProducts,
|
||||||
getProductById,
|
getProductById,
|
||||||
|
addProduct,
|
||||||
} = require("../controllers/product");
|
} = require("../controllers/product");
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.post("/add_fav_product", addToFavorite);
|
// Add detailed logging middleware
|
||||||
|
router.use((req, res, next) => {
|
||||||
|
console.log(`Incoming ${req.method} request to ${req.path}`);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
router.get("/get_product", getAllProducts);
|
router.post("/addFavorite", addFavorite);
|
||||||
|
router.post("/getFavorites", getFavorites);
|
||||||
|
router.post("/delFavorite", removeFavorite);
|
||||||
|
|
||||||
router.post("/get_productID", getProductById);
|
router.post("/addProduct", addProduct);
|
||||||
|
router.get("/getProduct", getAllProducts);
|
||||||
|
router.get("/:id", getProductById); // Simplified route
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
8
backend/routes/recommendation.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// routes/product.js
|
||||||
|
const express = require("express");
|
||||||
|
const { RecommondationByUserId } = require("../controllers/recommendation");
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post("/recommended", RecommondationByUserId);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
9
backend/routes/review.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// routes/product.js
|
||||||
|
const express = require("express");
|
||||||
|
const { getReviews, submitReview } = require("../controllers/review");
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get("/:id", getReviews);
|
||||||
|
router.post("/addReview", submitReview);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
14
backend/routes/search.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// routes/product.js
|
||||||
|
const express = require("express");
|
||||||
|
const { searchProductsByName } = require("../controllers/search");
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Add detailed logging middleware
|
||||||
|
router.use((req, res, next) => {
|
||||||
|
console.log(`Incoming ${req.method} request to ${req.path}`);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/getProduct", searchProductsByName);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -7,6 +7,7 @@ const {
|
|||||||
findUserByEmail,
|
findUserByEmail,
|
||||||
updateUser,
|
updateUser,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
|
doLogin,
|
||||||
} = require("../controllers/user");
|
} = require("../controllers/user");
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@@ -26,6 +27,9 @@ router.get("/fetch_all_users", getAllUser);
|
|||||||
//Fetch One user Data with all fields:
|
//Fetch One user Data with all fields:
|
||||||
router.post("/find_user", findUserByEmail);
|
router.post("/find_user", findUserByEmail);
|
||||||
|
|
||||||
|
//Fetch One user Data with all fields:
|
||||||
|
router.post("/do_login", doLogin);
|
||||||
|
|
||||||
//Update A uses Data:
|
//Update A uses Data:
|
||||||
router.post("/update", updateUser);
|
router.post("/update", updateUser);
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
const mysql = require("mysql2");
|
const mysql = require("mysql2");
|
||||||
|
|
||||||
//Create a pool of connections to allow multiple query happen at the same time
|
|
||||||
const pool = mysql.createPool({
|
const pool = mysql.createPool({
|
||||||
host: "localhost",
|
host: "localhost",
|
||||||
user: "root",
|
user: "root",
|
||||||
database: "marketplace",
|
database: "Marketplace",
|
||||||
});
|
});
|
||||||
|
|
||||||
//Export a promise for promise-based query
|
|
||||||
module.exports = pool.promise();
|
module.exports = pool.promise();
|
||||||
|
|||||||
115
frontend/package-lock.json
generated
@@ -9,6 +9,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.0.9",
|
"@tailwindcss/vite": "^4.0.9",
|
||||||
|
"axios": "^1.8.4",
|
||||||
"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",
|
||||||
@@ -1770,6 +1771,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/autoprefixer": {
|
"node_modules/autoprefixer": {
|
||||||
"version": "10.4.20",
|
"version": "10.4.20",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
|
||||||
@@ -1824,6 +1831,17 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.8.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
|
||||||
|
"integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.6",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@@ -1898,7 +1916,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
@@ -1993,6 +2010,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -2153,6 +2182,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||||
@@ -2182,7 +2220,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.1",
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
@@ -2283,7 +2320,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -2293,7 +2329,6 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -2331,7 +2366,6 @@
|
|||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0"
|
"es-errors": "^1.3.0"
|
||||||
@@ -2344,7 +2378,6 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
@@ -2732,6 +2765,26 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||||
|
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/for-each": {
|
"node_modules/for-each": {
|
||||||
"version": "0.3.5",
|
"version": "0.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||||
@@ -2748,6 +2801,21 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fraction.js": {
|
"node_modules/fraction.js": {
|
||||||
"version": "4.3.7",
|
"version": "4.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
||||||
@@ -2780,7 +2848,6 @@
|
|||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
@@ -2831,7 +2898,6 @@
|
|||||||
"version": "1.2.7",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz",
|
||||||
"integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==",
|
"integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.1",
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
@@ -2856,7 +2922,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dunder-proto": "^1.0.1",
|
"dunder-proto": "^1.0.1",
|
||||||
@@ -2931,7 +2996,6 @@
|
|||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -3002,7 +3066,6 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -3015,7 +3078,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"has-symbols": "^1.0.3"
|
"has-symbols": "^1.0.3"
|
||||||
@@ -3031,7 +3093,6 @@
|
|||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
@@ -3895,12 +3956,32 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
@@ -4251,6 +4332,12 @@
|
|||||||
"react-is": "^16.13.1"
|
"react-is": "^16.13.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.0.9",
|
"@tailwindcss/vite": "^4.0.9",
|
||||||
|
"axios": "^1.8.4",
|
||||||
"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",
|
||||||
|
|||||||
BIN
frontend/public/Pictures/Acoustic-Guitar.jpg
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
frontend/public/Pictures/Backpack.jpg
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
frontend/public/Pictures/Basketball.jpg
Normal file
|
After Width: | Height: | Size: 774 KiB |
BIN
frontend/public/Pictures/Bluetooth-Speaker.jpg
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
frontend/public/Pictures/CS-Textbook.jpg
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
frontend/public/Pictures/Calculator.jpg
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
frontend/public/Pictures/Calculus-Textbook.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
frontend/public/Pictures/Calculus-Textbook2.jpg
Normal file
|
After Width: | Height: | Size: 301 KiB |
BIN
frontend/public/Pictures/Calculus-Textbook3.jpg
Normal file
|
After Width: | Height: | Size: 421 KiB |
BIN
frontend/public/Pictures/Controller.jpg
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
BIN
frontend/public/Pictures/Desk-Lamp.jpg
Normal file
|
After Width: | Height: | Size: 236 KiB |
BIN
frontend/public/Pictures/Dorm-Desk.jpg
Normal file
|
After Width: | Height: | Size: 197 KiB |
BIN
frontend/public/Pictures/HP-Calculator.jpg
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
frontend/public/Pictures/HP-Laptop1.jpg
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
frontend/public/Pictures/HP-Laptop2.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
frontend/public/Pictures/Lab-Coat.jpg
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
frontend/public/Pictures/Mini-Fridge.jpg
Normal file
|
After Width: | Height: | Size: 201 KiB |
BIN
frontend/public/Pictures/Mountain-Bike.jpg
Normal file
|
After Width: | Height: | Size: 577 KiB |
BIN
frontend/public/Pictures/Physics-Textbook.jpg
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
frontend/public/Pictures/University-Hoodie.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
frontend/public/Pictures/Winter-Jacket.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
frontend/public/Pictures/Wireless-Mouse.jpg
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
frontend/public/Pictures/Yoga-Mat.jpg
Normal file
|
After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 23 KiB |
@@ -12,7 +12,7 @@ import Selling from "./pages/Selling";
|
|||||||
import Transactions from "./pages/Transactions";
|
import Transactions from "./pages/Transactions";
|
||||||
import Favorites from "./pages/Favorites";
|
import Favorites from "./pages/Favorites";
|
||||||
import ProductDetail from "./pages/ProductDetail";
|
import ProductDetail from "./pages/ProductDetail";
|
||||||
import ItemForm from "./pages/MyListings";
|
import SearchPage from "./pages/SearchPage"; // Make sure to import the SearchPage
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
// Authentication state - initialize from localStorage if available
|
// Authentication state - initialize from localStorage if available
|
||||||
@@ -30,6 +30,8 @@ function App() {
|
|||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const [userId, setUserId] = useState(null);
|
||||||
|
|
||||||
// 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);
|
||||||
@@ -50,6 +52,10 @@ function App() {
|
|||||||
return () => window.removeEventListener("resize", handleResize);
|
return () => window.removeEventListener("resize", handleResize);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
sendSessionDataToServer();
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Send verification code
|
// Send verification code
|
||||||
const sendVerificationCode = async (userData) => {
|
const sendVerificationCode = async (userData) => {
|
||||||
try {
|
try {
|
||||||
@@ -189,6 +195,9 @@ function App() {
|
|||||||
sessionStorage.setItem("isAuthenticated", "true");
|
sessionStorage.setItem("isAuthenticated", "true");
|
||||||
sessionStorage.setItem("user", JSON.stringify(newUser));
|
sessionStorage.setItem("user", JSON.stringify(newUser));
|
||||||
|
|
||||||
|
// After successful signup, send session data to server
|
||||||
|
sendSessionDataToServer(); // Call it after signup
|
||||||
|
|
||||||
// Reset verification steps
|
// Reset verification steps
|
||||||
setVerificationStep("initial");
|
setVerificationStep("initial");
|
||||||
setTempUserData(null);
|
setTempUserData(null);
|
||||||
@@ -240,7 +249,7 @@ function App() {
|
|||||||
UCID: formValues.ucid,
|
UCID: formValues.ucid,
|
||||||
phone: formValues.phone,
|
phone: formValues.phone,
|
||||||
password: formValues.password, // This will be needed for the final signup
|
password: formValues.password, // This will be needed for the final signup
|
||||||
address: "NOT_GIVEN",
|
address: formValues.address, // Add this line
|
||||||
client: 1,
|
client: 1,
|
||||||
admin: 0,
|
admin: 0,
|
||||||
};
|
};
|
||||||
@@ -256,7 +265,7 @@ function App() {
|
|||||||
|
|
||||||
// Make API call to localhost:3030/find_user
|
// Make API call to localhost:3030/find_user
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
"http://localhost:3030/api/user/find_user",
|
"http://localhost:3030/api/user/do_login",
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -356,6 +365,48 @@ function App() {
|
|||||||
setError("");
|
setError("");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sendSessionDataToServer = async () => {
|
||||||
|
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");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare the data to send
|
||||||
|
const requestData = {
|
||||||
|
userId: user.ID, // or user.ID depending on your user structure
|
||||||
|
email: user.email,
|
||||||
|
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",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestData),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check the response
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
console.log("Server response:", result);
|
||||||
|
} else {
|
||||||
|
console.error("Failed to send session data to the server");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error sending session data:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Login component
|
// Login component
|
||||||
const LoginComponent = () => (
|
const LoginComponent = () => (
|
||||||
<div className="flex h-screen bg-white">
|
<div className="flex h-screen bg-white">
|
||||||
@@ -480,6 +531,25 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isSignUp && (
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="address"
|
||||||
|
className="block mb-1 text-sm font-medium text-gray-800"
|
||||||
|
>
|
||||||
|
Address
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
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"
|
||||||
|
required={isSignUp}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="password"
|
htmlFor="password"
|
||||||
@@ -634,6 +704,16 @@ function App() {
|
|||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/search"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<div className="container mx-auto px-4 py-6">
|
||||||
|
<SearchPage />
|
||||||
|
</div>
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/settings"
|
path="/settings"
|
||||||
element={
|
element={
|
||||||
@@ -654,27 +734,6 @@ function App() {
|
|||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{/* Add new selling routes */}
|
|
||||||
<Route
|
|
||||||
path="/selling/create"
|
|
||||||
element={
|
|
||||||
<ProtectedRoute>
|
|
||||||
<div className="container mx-auto px-4 py-6">
|
|
||||||
<ItemForm />
|
|
||||||
</div>
|
|
||||||
</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/selling/edit/:id"
|
|
||||||
element={
|
|
||||||
<ProtectedRoute>
|
|
||||||
<div className="container mx-auto px-4 py-6">
|
|
||||||
<ItemForm />
|
|
||||||
</div>
|
|
||||||
</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path="/transactions"
|
path="/transactions"
|
||||||
element={
|
element={
|
||||||
|
|||||||
19
frontend/src/components/FloatingAlert.jsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// components/FloatingAlert.jsx
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
const FloatingAlert = ({ message, onClose, duration = 3000 }) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
onClose();
|
||||||
|
}, duration);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [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">
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default FloatingAlert;
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
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 }) => {
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleSearchChange = (e) => {
|
const handleSearchChange = (e) => {
|
||||||
setSearchQuery(e.target.value);
|
setSearchQuery(e.target.value);
|
||||||
@@ -12,8 +13,14 @@ const Navbar = ({ onLogout, userName }) => {
|
|||||||
|
|
||||||
const handleSearchSubmit = (e) => {
|
const handleSearchSubmit = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
console.log("Searching for:", searchQuery);
|
|
||||||
// TODO: Implement search functionality
|
// if (!searchQuery.trim()) return;
|
||||||
|
|
||||||
|
// Navigate to search page with query
|
||||||
|
navigate({
|
||||||
|
pathname: "/search",
|
||||||
|
search: `?name=${encodeURIComponent(searchQuery)}`,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -28,7 +35,7 @@ const Navbar = ({ onLogout, userName }) => {
|
|||||||
alt="Campus Plug"
|
alt="Campus Plug"
|
||||||
className="h-8 px-2"
|
className="h-8 px-2"
|
||||||
/>
|
/>
|
||||||
<span className="hidden md:block text-green-600 font-bold text-xl">
|
<span className="hidden md:block text-emerald-600 font-bold text-xl">
|
||||||
Campus Plug
|
Campus Plug
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -40,14 +47,20 @@ const Navbar = ({ onLogout, userName }) => {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search for books, electronics, furniture..."
|
placeholder="Search for anything..."
|
||||||
className="w-full p-2 pl-10 pr-4 border border-gray-300 focus:outline-none focus:border-green-500 focus:ring-1 focus:ring-green-500"
|
className="w-full p-2 pl-10 pr-4 border border-gray-300 focus:outline-none focus:border-[#ed7f30]-500 focus:ring-1 focus:ring-[#ed7f30]-500"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
<Search className="h-5 w-5 text-gray-400" />
|
<Search className="h-5 w-5 text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="absolute inset-y-0 right-0 flex items-center px-3 text-gray-500 hover:text-[#ed7f30]-500"
|
||||||
|
>
|
||||||
|
Search
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,10 +70,11 @@ const Navbar = ({ onLogout, userName }) => {
|
|||||||
{/* Favorites Button */}
|
{/* Favorites Button */}
|
||||||
<Link
|
<Link
|
||||||
to="/favorites"
|
to="/favorites"
|
||||||
className="p-2 text-gray-600 hover:text-green-600"
|
className="p-2 text-gray-600 hover:text-[#ed7f30]-600"
|
||||||
>
|
>
|
||||||
<Heart className="h-6 w-6" />
|
<Heart className="h-6 w-6" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* User Profile */}
|
{/* User Profile */}
|
||||||
<UserDropdown onLogout={onLogout} userName={userName} />
|
<UserDropdown onLogout={onLogout} userName={userName} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
410
frontend/src/components/ProductForm.jsx
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
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;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
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";
|
||||||
|
|
||||||
const UserDropdown = ({ onLogout, userName }) => {
|
const UserDropdown = ({ onLogout, userName }) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
@@ -8,7 +8,7 @@ const UserDropdown = ({ onLogout, userName }) => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// Use passed userName or fallback to default
|
// Use passed userName or fallback to default
|
||||||
const displayName = userName || 'User';
|
const displayName = userName || "User";
|
||||||
|
|
||||||
const toggleDropdown = () => {
|
const toggleDropdown = () => {
|
||||||
setIsOpen(!isOpen);
|
setIsOpen(!isOpen);
|
||||||
@@ -22,9 +22,9 @@ const UserDropdown = ({ onLogout, userName }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ const UserDropdown = ({ onLogout, userName }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Navigate to login page (this may be redundant as App.jsx should handle redirection)
|
// Navigate to login page (this may be redundant as App.jsx should handle redirection)
|
||||||
navigate('/login');
|
navigate("/login");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -48,8 +48,8 @@ const UserDropdown = ({ onLogout, userName }) => {
|
|||||||
className="flex items-center focus:outline-none"
|
className="flex items-center focus:outline-none"
|
||||||
onClick={toggleDropdown}
|
onClick={toggleDropdown}
|
||||||
>
|
>
|
||||||
<div className="h-8 w-8 rounded-full bg-green-100 flex items-center justify-center">
|
<div className="h-8 w-8 rounded-full bg-emerald-100 flex items-center justify-center">
|
||||||
<User className="h-5 w-5 text-green-600" />
|
<User className="h-5 w-5 text-emerald-600" />
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|||||||
@@ -1,151 +1,190 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from "react-router-dom";
|
||||||
import { Heart, Tag, Trash2, Filter, ChevronDown } from 'lucide-react';
|
import { Heart, Trash2 } from "lucide-react";
|
||||||
|
|
||||||
const Favorites = () => {
|
const Favorites = () => {
|
||||||
const [favorites, setFavorites] = useState([
|
const [favorites, setFavorites] = useState([]);
|
||||||
{
|
const [sortBy, setSortBy] = useState("dateAdded");
|
||||||
id: 0,
|
const storedUser = JSON.parse(sessionStorage.getItem("user"));
|
||||||
title: 'Dell XPS 16 Laptop',
|
|
||||||
price: 850,
|
|
||||||
category: 'Electronics',
|
|
||||||
image: '/image1.avif',
|
|
||||||
condition: 'Like New',
|
|
||||||
seller: 'Michael T.',
|
|
||||||
datePosted: '5d ago',
|
|
||||||
dateAdded: '2023-03-08',
|
|
||||||
},
|
|
||||||
|
|
||||||
]);
|
function reloadPage() {
|
||||||
|
const docTimestamp = new Date(performance.timing.domLoading).getTime();
|
||||||
|
const now = Date.now();
|
||||||
|
if (now > docTimestamp) {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [showFilters, setShowFilters] = useState(false);
|
const mapCategory = (id) => {
|
||||||
const [sortBy, setSortBy] = useState('dateAdded');
|
return id || "Other";
|
||||||
const [filterCategory, setFilterCategory] = useState('All');
|
|
||||||
|
|
||||||
// Function to remove item from favorites
|
|
||||||
const removeFromFavorites = (id) => {
|
|
||||||
setFavorites(favorites.filter(item => item.id !== id));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Available categories for filtering
|
const removeFromFavorites = async (itemID) => {
|
||||||
const categories = ['All', 'Electronics', 'Textbooks', 'Furniture', 'Kitchen', 'Other'];
|
const response = await fetch(
|
||||||
|
"http://localhost:3030/api/product/delFavorite",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
userID: storedUser.ID,
|
||||||
|
productID: itemID,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Sort favorites based on selected sort option
|
const data = await response.json();
|
||||||
const sortedFavorites = [...favorites].sort((a, b) => {
|
if (data.success) {
|
||||||
if (sortBy === 'dateAdded') {
|
reloadPage();
|
||||||
return new Date(b.dateAdded) - new Date(a.dateAdded);
|
|
||||||
} else if (sortBy === 'priceHigh') {
|
|
||||||
return b.price - a.price;
|
|
||||||
} else if (sortBy === 'priceLow') {
|
|
||||||
return a.price - b.price;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error("Failed to remove from favorites");
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchFavorites = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
"http://localhost:3030/api/product/getFavorites",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ userID: storedUser.ID }),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const favoritesData = data.favorites;
|
||||||
|
|
||||||
|
if (!Array.isArray(favoritesData)) {
|
||||||
|
console.error("Expected an array but got:", favoritesData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const transformed = favoritesData.map((item) => ({
|
||||||
|
id: item.ProductID,
|
||||||
|
name: item.Name,
|
||||||
|
price: parseFloat(item.Price),
|
||||||
|
categories: [mapCategory(item.Category)],
|
||||||
|
image: item.image_url || "/default-image.jpg",
|
||||||
|
description: item.Description || "",
|
||||||
|
seller: item.SellerName,
|
||||||
|
datePosted: formatDatePosted(item.Date),
|
||||||
|
dateAdded: item.Date || new Date().toISOString(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
setFavorites(transformed);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch favorites:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchFavorites();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const formatDatePosted = (dateString) => {
|
||||||
|
const postedDate = new Date(dateString);
|
||||||
|
const today = new Date();
|
||||||
|
const diffInMs = today - postedDate;
|
||||||
|
const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24));
|
||||||
|
return `${diffInDays}d ago`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortedFavorites = [...favorites].sort((a, b) => {
|
||||||
|
if (sortBy === "dateAdded")
|
||||||
|
return new Date(b.dateAdded) - new Date(a.dateAdded);
|
||||||
|
if (sortBy === "priceHigh") return b.price - a.price;
|
||||||
|
if (sortBy === "priceLow") return a.price - b.price;
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Filter favorites based on selected category
|
|
||||||
const filteredFavorites = filterCategory === 'All'
|
|
||||||
? sortedFavorites
|
|
||||||
: sortedFavorites.filter(item => item.category === filterCategory);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-6xl mx-auto">
|
<div className="max-w-6xl mx-auto">
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<h1 className="text-2xl font-bold text-gray-800">My Favorites</h1>
|
<h1 className="text-2xl font-bold text-gray-800">My Favorites</h1>
|
||||||
<button
|
|
||||||
className="flex items-center text-gray-600 hover:text-gray-800"
|
|
||||||
onClick={() => setShowFilters(!showFilters)}
|
|
||||||
>
|
|
||||||
<Filter className="h-5 w-5 mr-1" />
|
|
||||||
<span>Filter & Sort</span>
|
|
||||||
<ChevronDown className={`h-4 w-4 ml-1 transition-transform ${showFilters ? 'rotate-180' : ''}`} />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filters and Sorting */}
|
{sortedFavorites.length === 0 ? (
|
||||||
{showFilters && (
|
|
||||||
<div className="bg-white border border-gray-200 p-4 mb-6">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Sort by
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={sortBy}
|
|
||||||
onChange={(e) => setSortBy(e.target.value)}
|
|
||||||
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500"
|
|
||||||
>
|
|
||||||
<option value="dateAdded">Recently Added</option>
|
|
||||||
<option value="priceHigh">Price (High to Low)</option>
|
|
||||||
<option value="priceLow">Price (Low to High)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Category
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={filterCategory}
|
|
||||||
onChange={(e) => setFilterCategory(e.target.value)}
|
|
||||||
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500"
|
|
||||||
>
|
|
||||||
{categories.map((category) => (
|
|
||||||
<option key={category} value={category}>{category}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Favorites List */}
|
|
||||||
{filteredFavorites.length === 0 ? (
|
|
||||||
<div className="bg-white border border-gray-200 p-8 text-center">
|
<div className="bg-white border border-gray-200 p-8 text-center">
|
||||||
<Heart className="h-12 w-12 text-gray-300 mx-auto mb-4" />
|
<Heart className="h-12 w-12 text-gray-300 mx-auto mb-4" />
|
||||||
<h3 className="text-xl font-medium text-gray-700 mb-2">No favorites yet</h3>
|
<h3 className="text-xl font-medium text-gray-700 mb-2">
|
||||||
|
No favorites yet
|
||||||
|
</h3>
|
||||||
<p className="text-gray-500 mb-4">
|
<p className="text-gray-500 mb-4">
|
||||||
Items you save will appear here. Start browsing to add items to your favorites.
|
Items you save will appear here. Start browsing to add items to your
|
||||||
|
favorites.
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/"
|
||||||
className="inline-block bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-4"
|
className="inline-block bg-emerald-500 hover:bg-emerald-600 text-white font-medium py-2 px-4"
|
||||||
>
|
>
|
||||||
Browse Listings
|
Browse Listings
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{filteredFavorites.map((item) => (
|
{sortedFavorites.map((product) => (
|
||||||
<div key={item.id} className="bg-white border border-gray-200 hover:shadow-md transition-shadow relative">
|
<div
|
||||||
<button
|
key={product.id}
|
||||||
onClick={() => removeFromFavorites(item.id)}
|
className="border-2 border-gray-200 overflow-hidden hover:shadow-md transition-shadow"
|
||||||
className="absolute top-2 right-2 p-1 bg-white rounded-full shadow-sm text-red-500 hover:bg-red-50"
|
|
||||||
title="Remove from favorites"
|
|
||||||
>
|
>
|
||||||
<Trash2 className="h-5 w-5" />
|
<Link to={`/product/${product.id}`}>
|
||||||
</button>
|
<div className="h-48 bg-gray-200 flex items-center justify-center">
|
||||||
|
{product.image ? (
|
||||||
<Link to={`/product/${item.id}`}>
|
<img
|
||||||
<img src={item.image} alt={item.title} className="w-full h-48 object-cover" />
|
src={product.image}
|
||||||
|
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="p-4">
|
||||||
<div className="flex justify-between items-start mb-2">
|
<div className="flex justify-between items-start">
|
||||||
<h3 className="text-lg font-medium text-gray-800 leading-tight">
|
<h3 className="text-lg font-semibold text-gray-800">
|
||||||
{item.title}
|
{product.name}
|
||||||
</h3>
|
</h3>
|
||||||
<span className="font-semibold text-green-600">${item.price}</span>
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
removeFromFavorites(product.id);
|
||||||
|
}}
|
||||||
|
className="text-red-500 hover:text-red-600"
|
||||||
|
>
|
||||||
|
<Trash2 size={24} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center text-sm text-gray-500 mb-3">
|
<p className="text-emerald-600 font-bold mt-1">
|
||||||
<Tag className="h-4 w-4 mr-1" />
|
${product.price.toFixed(2)}
|
||||||
<span>{item.category}</span>
|
</p>
|
||||||
<span className="mx-2">•</span>
|
|
||||||
<span>{item.condition}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-between items-center pt-2 border-t border-gray-100">
|
{product.categories.length > 0 && (
|
||||||
<span className="text-xs text-gray-500">Listed {item.datePosted}</span>
|
<div className="mt-2 flex flex-wrap gap-1">
|
||||||
<span className="text-sm font-medium text-gray-700">{item.seller}</span>
|
{product.categories.map((category) => (
|
||||||
|
<span
|
||||||
|
key={category}
|
||||||
|
className="text-xs bg-gray-100 text-gray-600 px-2 py-1"
|
||||||
|
>
|
||||||
|
{category}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p className="text-gray-500 text-sm mt-2 line-clamp-2">
|
||||||
|
{product.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="text-gray-400 text-xs mt-2">
|
||||||
|
Posted {product.datePosted}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -153,13 +192,66 @@ const Favorites = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Show count if there are favorites */}
|
{sortedFavorites.length > 0 && (
|
||||||
{filteredFavorites.length > 0 && (
|
|
||||||
<div className="mt-6 text-sm text-gray-500">
|
<div className="mt-6 text-sm text-gray-500">
|
||||||
Showing {filteredFavorites.length} {filteredFavorites.length === 1 ? 'item' : 'items'}
|
Showing {sortedFavorites.length}{" "}
|
||||||
{filterCategory !== 'All' && ` in ${filterCategory}`}
|
{sortedFavorites.length === 1 ? "item" : "items"}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,17 +1,121 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { Tag, Book, Laptop, Sofa, Utensils, Gift, Heart } from "lucide-react";
|
import {
|
||||||
|
Tag,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
Bookmark,
|
||||||
|
BookmarkCheck,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
import FloatingAlert from "../components/FloatingAlert"; // adjust path if needed
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [listings, setListings] = useState([]);
|
const [listings, setListings] = useState([]);
|
||||||
|
const [recommended, setRecommended] = useState([]);
|
||||||
|
const [history, sethistory] = useState([]);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
const [showAlert, setShowAlert] = useState(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",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
userID: storedUser.ID,
|
||||||
|
productID: id,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
setShowAlert(true);
|
||||||
|
}
|
||||||
|
console.log(`Add Product -> History: ${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addHistory = async (id) => {
|
||||||
|
const response = await fetch(
|
||||||
|
"http://localhost:3030/api/history/addHistory",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
userID: storedUser.ID,
|
||||||
|
productID: id,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchrecomProducts = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
"http://localhost:3030/api/engine/recommended",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: storedUser.ID,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!response.ok) throw new Error("Failed to fetch products");
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
setRecommended(
|
||||||
|
data.data.map((product) => ({
|
||||||
|
id: product.ProductID,
|
||||||
|
title: product.ProductName, // Use the alias from SQL
|
||||||
|
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
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
reloadPage();
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message || "Error fetching products");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching products:", error);
|
||||||
|
setError(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchrecomProducts();
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchProducts = async () => {
|
const fetchProducts = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
"http://localhost:3030/api/product/get_product",
|
"http://localhost:3030/api/product/getProduct",
|
||||||
);
|
);
|
||||||
if (!response.ok) throw new Error("Failed to fetch products");
|
if (!response.ok) throw new Error("Failed to fetch products");
|
||||||
|
|
||||||
@@ -21,14 +125,13 @@ const Home = () => {
|
|||||||
setListings(
|
setListings(
|
||||||
data.data.map((product) => ({
|
data.data.map((product) => ({
|
||||||
id: product.ProductID,
|
id: product.ProductID,
|
||||||
title: product.Name,
|
title: product.ProductName, // Use the alias from SQL
|
||||||
price: product.Price,
|
price: product.Price,
|
||||||
category: product.CategoryID,
|
category: product.Category, // Ensure this gets the category name
|
||||||
image: product.URL,
|
image: product.ProductImage, // Use the alias for image URL
|
||||||
condition: "New", // Modify based on actual data
|
seller: product.SellerName, // Fetch seller name properly
|
||||||
seller: product.UserID, // Modify if seller info is available
|
datePosted: product.DateUploaded, // Use the actual date
|
||||||
datePosted: "Just now",
|
isFavorite: false, // Default state
|
||||||
isFavorite: false,
|
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -39,28 +142,58 @@ const Home = () => {
|
|||||||
setError(error.message);
|
setError(error.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchProducts();
|
fetchProducts();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Toggle favorite status
|
useEffect(() => {
|
||||||
const toggleFavorite = (id, e) => {
|
const fetchrecomProducts = async () => {
|
||||||
e.preventDefault(); // Prevent navigation when clicking the heart icon
|
// Get the user's data from localStorage
|
||||||
setListings((prevListings) =>
|
try {
|
||||||
prevListings.map((listing) =>
|
const response = await fetch(
|
||||||
listing.id === id
|
"http://localhost:3030/api/history/getHistory",
|
||||||
? { ...listing, isFavorite: !listing.isFavorite }
|
{
|
||||||
: listing,
|
method: "POST",
|
||||||
),
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: storedUser.ID,
|
||||||
|
}),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
if (!response.ok) throw new Error("Failed to fetch products");
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
sethistory(
|
||||||
|
data.data.map((product) => ({
|
||||||
|
id: product.ProductID,
|
||||||
|
title: product.ProductName, // Use the alias from SQL
|
||||||
|
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
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message || "Error fetching products");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching products:", error);
|
||||||
|
setError(error.message);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
fetchrecomProducts();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleSelling = () => {
|
const handleSelling = () => {
|
||||||
navigate("/selling");
|
navigate("/selling");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-col min-h-screen">
|
||||||
|
<div className="flex-grow">
|
||||||
{/* Hero Section with School Background */}
|
{/* Hero Section with School Background */}
|
||||||
<div className="relative py-12 px-4 mb-8 shadow-sm">
|
<div className="relative py-12 px-4 mb-8 shadow-sm">
|
||||||
{/* Background Image - Positioned at bottom */}
|
{/* Background Image - Positioned at bottom */}
|
||||||
@@ -79,88 +212,182 @@ const Home = () => {
|
|||||||
Buy and Sell on Campus
|
Buy and Sell on Campus
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-white mb-6">
|
<p className="text-white mb-6">
|
||||||
The marketplace exclusively for university students. Find everything
|
The marketplace exclusively for university students. Find
|
||||||
you need or sell what you don't.
|
everything you need or sell what you don't.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={handleSelling}
|
onClick={handleSelling}
|
||||||
className="bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-6 focus:outline-none focus:ring-2 focus:ring-green-400 transition-colors"
|
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"
|
||||||
>
|
>
|
||||||
Post an Item
|
Post an Item
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Categories */}
|
{/* Recent Listings */}
|
||||||
{/* <div className="mb-8">
|
{showAlert && (
|
||||||
<h2 className="text-xl font-semibold text-gray-800 mb-4">Categories</h2>
|
<FloatingAlert
|
||||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
message="Product added to favorites!"
|
||||||
{categories.map((category) => (
|
onClose={() => setShowAlert(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="relative py-4">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-800 mb-4">
|
||||||
|
Recommendation
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
{/* Left Button - Overlaid on products */}
|
||||||
<button
|
<button
|
||||||
key={category.id}
|
onClick={() =>
|
||||||
className="flex flex-col items-center justify-center p-4 bg-white border border-gray-200 hover:border-green-500 hover:shadow-sm"
|
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"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center w-12 h-12 bg-green-50 text-green-600 rounded-full mb-2">
|
<ChevronLeft size={24} />{" "}
|
||||||
{category.icon}
|
|
||||||
</div>
|
|
||||||
<span className="text-sm font-medium text-gray-700">
|
|
||||||
{category.name}
|
|
||||||
</span>
|
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</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 */}
|
{/* Recent Listings */}
|
||||||
<div>
|
{showAlert && (
|
||||||
|
<FloatingAlert
|
||||||
|
message="Product added to favorites!"
|
||||||
|
onClose={() => setShowAlert(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="relative py-4">
|
||||||
<h2 className="text-xl font-semibold text-gray-800 mb-4">
|
<h2 className="text-xl font-semibold text-gray-800 mb-4">
|
||||||
Recent Listings
|
Recent Listings
|
||||||
</h2>
|
</h2>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-6">
|
|
||||||
|
<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) => (
|
{listings.map((listing) => (
|
||||||
<Link
|
<Link
|
||||||
key={listing.id}
|
key={listing.id}
|
||||||
to={`/product/${listing.id}`}
|
to={`/product/${listing.id}`}
|
||||||
className="bg-white border border-gray-200 hover:shadow-md transition-shadow"
|
className="bg-white border border-gray-200 hover:shadow-md transition-shadow w-70 flex-shrink-0 relative"
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<img
|
<img
|
||||||
src={listing.image}
|
src={listing.image}
|
||||||
alt={listing.title}
|
alt={listing.title}
|
||||||
|
onClick={() => addHistory(listing.id)}
|
||||||
className="w-full h-48 object-cover"
|
className="w-full h-48 object-cover"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => toggleFavorite(listing.id, e)}
|
onClick={(e) => {
|
||||||
className="absolute top-2 right-2 p-1 bg-white rounded-full shadow-sm"
|
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"
|
||||||
>
|
>
|
||||||
<Heart
|
<Bookmark className="text-white w-5 h-5" />
|
||||||
className={`h-5 w-5 ${
|
|
||||||
listing.isFavorite
|
|
||||||
? "text-red-500 fill-red-500"
|
|
||||||
: "text-gray-400"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="flex justify-between items-start mb-2">
|
|
||||||
<h3 className="text-lg font-medium text-gray-800 leading-tight">
|
<h3 className="text-lg font-medium text-gray-800 leading-tight">
|
||||||
{listing.title}
|
{listing.title}
|
||||||
</h3>
|
</h3>
|
||||||
<span className="font-semibold text-green-600">
|
<span className="font-semibold text-emerald-600 block mt-1">
|
||||||
${listing.price}
|
${listing.price}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center text-sm text-gray-500 mb-3">
|
<div className="flex items-center text-sm text-gray-500 mt-2">
|
||||||
<Tag className="h-4 w-4 mr-1" />
|
<Tag className="h-4 w-4 mr-1" />
|
||||||
<span>{listing.category}</span>
|
<span>{listing.category}</span>
|
||||||
<span className="mx-2">•</span>
|
|
||||||
<span>{listing.condition}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between items-center pt-2 border-t border-gray-100">
|
<div className="flex justify-between items-center pt-2 border-t border-gray-100 mt-3">
|
||||||
<span className="text-xs text-gray-500">
|
<span className="text-xs text-gray-500">
|
||||||
{listing.datePosted}
|
{listing.datePosted}
|
||||||
</span>
|
</span>
|
||||||
@@ -172,8 +399,169 @@ const Home = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* History Section */}
|
||||||
|
{showAlert && (
|
||||||
|
<FloatingAlert
|
||||||
|
message="Product added to favorites!"
|
||||||
|
onClose={() => setShowAlert(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="relative py-4">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-800 mb-4">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>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,550 +0,0 @@
|
|||||||
import { useState, useEffect } from "react";
|
|
||||||
import { Link, useParams, useNavigate } from "react-router-dom";
|
|
||||||
import { ArrowLeft, Plus, X, Save, Trash } from "lucide-react";
|
|
||||||
|
|
||||||
const ItemForm = () => {
|
|
||||||
const { id } = useParams(); // If id exists, we are editing, otherwise creating
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const isEditing = !!id;
|
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
title: "",
|
|
||||||
price: "",
|
|
||||||
category: "",
|
|
||||||
condition: "",
|
|
||||||
shortDescription: "",
|
|
||||||
description: "",
|
|
||||||
images: [],
|
|
||||||
status: "active",
|
|
||||||
});
|
|
||||||
|
|
||||||
const [originalData, setOriginalData] = useState(null);
|
|
||||||
const [errors, setErrors] = useState({});
|
|
||||||
const [imagePreviewUrls, setImagePreviewUrls] = useState([]);
|
|
||||||
const [isLoading, setIsLoading] = useState(isEditing);
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
|
||||||
|
|
||||||
// Categories with icons
|
|
||||||
const categories = [
|
|
||||||
"Electronics",
|
|
||||||
"Furniture",
|
|
||||||
"Books",
|
|
||||||
"Kitchen",
|
|
||||||
"Collectibles",
|
|
||||||
"Clothing",
|
|
||||||
"Sports & Outdoors",
|
|
||||||
"Tools",
|
|
||||||
"Toys & Games",
|
|
||||||
"Other",
|
|
||||||
];
|
|
||||||
|
|
||||||
// Condition options
|
|
||||||
const conditions = ["New", "Like New", "Good", "Fair", "Poor"];
|
|
||||||
|
|
||||||
// Status options
|
|
||||||
const statuses = ["active", "inactive", "sold", "pending"];
|
|
||||||
|
|
||||||
// Fetch item data if editing
|
|
||||||
useEffect(() => {
|
|
||||||
if (isEditing) {
|
|
||||||
// This would be an API call in a real app
|
|
||||||
// Simulating API call with timeout
|
|
||||||
setTimeout(() => {
|
|
||||||
// Sample data for item being edited
|
|
||||||
const itemData = {
|
|
||||||
id: parseInt(id),
|
|
||||||
title: "Dell XPS 13 Laptop - 2023 Model",
|
|
||||||
price: 850,
|
|
||||||
category: "Electronics",
|
|
||||||
condition: "Like New",
|
|
||||||
shortDescription:
|
|
||||||
"Dell XPS 13 laptop in excellent condition. Intel Core i7, 16GB RAM, 512GB SSD. Includes charger and original box.",
|
|
||||||
description:
|
|
||||||
"Selling my Dell XPS 13 laptop. Only 6 months old and in excellent condition. Intel Core i7 processor, 16GB RAM, 512GB SSD. Battery life is still excellent (around 10 hours of regular use). Comes with original charger and box. Selling because I'm upgrading to a MacBook for design work.\n\nSpecs:\n- Intel Core i7 11th Gen\n- 16GB RAM\n- 512GB NVMe SSD\n- 13.4\" FHD+ Display (1920x1200)\n- Windows 11 Pro\n- Backlit Keyboard\n- Thunderbolt 4 ports",
|
|
||||||
images: ["/image1.avif", "/image2.avif", "/image3.avif"],
|
|
||||||
status: "active",
|
|
||||||
datePosted: "2023-03-02",
|
|
||||||
};
|
|
||||||
|
|
||||||
setFormData(itemData);
|
|
||||||
setOriginalData(itemData);
|
|
||||||
setImagePreviewUrls(itemData.images);
|
|
||||||
setIsLoading(false);
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
}, [id, isEditing]);
|
|
||||||
|
|
||||||
const handleChange = (e) => {
|
|
||||||
const { name, value } = e.target;
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
[name]: value,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear error when field is edited
|
|
||||||
if (errors[name]) {
|
|
||||||
setErrors({
|
|
||||||
...errors,
|
|
||||||
[name]: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleImageChange = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const files = Array.from(e.target.files);
|
|
||||||
|
|
||||||
if (formData.images.length + files.length > 5) {
|
|
||||||
setErrors({
|
|
||||||
...errors,
|
|
||||||
images: "Maximum 5 images allowed",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create preview URLs for the images
|
|
||||||
const newImagePreviewUrls = [...imagePreviewUrls];
|
|
||||||
const newImages = [...formData.images];
|
|
||||||
|
|
||||||
files.forEach((file) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onloadend = () => {
|
|
||||||
newImagePreviewUrls.push(reader.result);
|
|
||||||
setImagePreviewUrls(newImagePreviewUrls);
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
newImages.push(file);
|
|
||||||
});
|
|
||||||
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
images: newImages,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear error if any
|
|
||||||
if (errors.images) {
|
|
||||||
setErrors({
|
|
||||||
...errors,
|
|
||||||
images: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeImage = (index) => {
|
|
||||||
const newImages = [...formData.images];
|
|
||||||
const newImagePreviewUrls = [...imagePreviewUrls];
|
|
||||||
|
|
||||||
newImages.splice(index, 1);
|
|
||||||
newImagePreviewUrls.splice(index, 1);
|
|
||||||
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
images: newImages,
|
|
||||||
});
|
|
||||||
setImagePreviewUrls(newImagePreviewUrls);
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateForm = () => {
|
|
||||||
const newErrors = {};
|
|
||||||
|
|
||||||
if (!formData.title.trim()) newErrors.title = "Title is required";
|
|
||||||
if (!formData.price) newErrors.price = "Price is required";
|
|
||||||
if (isNaN(formData.price) || formData.price <= 0)
|
|
||||||
newErrors.price = "Price must be a positive number";
|
|
||||||
if (!formData.category) newErrors.category = "Category is required";
|
|
||||||
if (!formData.condition) newErrors.condition = "Condition is required";
|
|
||||||
if (!formData.shortDescription.trim())
|
|
||||||
newErrors.shortDescription = "Short description is required";
|
|
||||||
if (!formData.description.trim())
|
|
||||||
newErrors.description = "Description is required";
|
|
||||||
if (formData.images.length === 0)
|
|
||||||
newErrors.images = "At least one image is required";
|
|
||||||
|
|
||||||
setErrors(newErrors);
|
|
||||||
return Object.keys(newErrors).length === 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!validateForm()) {
|
|
||||||
// Scroll to the first error
|
|
||||||
const firstErrorField = Object.keys(errors)[0];
|
|
||||||
document
|
|
||||||
.getElementsByName(firstErrorField)[0]
|
|
||||||
?.scrollIntoView({ behavior: "smooth" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSubmitting(true);
|
|
||||||
|
|
||||||
// Simulate API call to post/update the item
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log("Form submitted:", formData);
|
|
||||||
setIsSubmitting(false);
|
|
||||||
|
|
||||||
// Show success and redirect to listings
|
|
||||||
alert(`Item successfully ${isEditing ? "updated" : "created"}!`);
|
|
||||||
navigate("/selling");
|
|
||||||
}, 1500);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = () => {
|
|
||||||
setIsSubmitting(true);
|
|
||||||
|
|
||||||
// Simulate API call to delete the item
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log("Item deleted:", id);
|
|
||||||
setIsSubmitting(false);
|
|
||||||
setShowDeleteModal(false);
|
|
||||||
|
|
||||||
// Show success and redirect to listings
|
|
||||||
alert("Item successfully deleted!");
|
|
||||||
navigate("/selling");
|
|
||||||
}, 1500);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Show loading state if necessary
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="max-w-4xl mx-auto px-4 py-8">
|
|
||||||
<div className="flex items-center mb-6">
|
|
||||||
<Link to="/selling" className="text-green-600 hover:text-green-700">
|
|
||||||
<ArrowLeft className="h-4 w-4 mr-1" />
|
|
||||||
Back to listings
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="animate-pulse">
|
|
||||||
<div className="h-8 bg-gray-200 rounded w-1/3 mb-8"></div>
|
|
||||||
<div className="h-64 bg-gray-200 rounded mb-4"></div>
|
|
||||||
<div className="h-32 bg-gray-200 rounded"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="max-w-4xl mx-auto px-4 py-8">
|
|
||||||
{/* Breadcrumb & Back Link */}
|
|
||||||
<div className="mb-6">
|
|
||||||
<Link
|
|
||||||
to="/selling"
|
|
||||||
className="flex items-center text-green-600 hover:text-green-700"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="h-4 w-4 mr-1" />
|
|
||||||
<span>Back to listings</span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-between items-center mb-6">
|
|
||||||
<h1 className="text-3xl font-bold text-gray-800">
|
|
||||||
{isEditing ? "Edit Item" : "Create New Listing"}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
{isEditing && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowDeleteModal(true)}
|
|
||||||
className="bg-red-500 hover:bg-red-600 text-white font-medium py-2 px-4 flex items-center"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
<Trash className="h-5 w-5 mr-1" />
|
|
||||||
Delete Item
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
className="bg-white border border-gray-200 p-6 rounded-md"
|
|
||||||
>
|
|
||||||
{/* Title */}
|
|
||||||
<div className="mb-6">
|
|
||||||
<label
|
|
||||||
htmlFor="title"
|
|
||||||
className="block text-gray-700 font-medium mb-2"
|
|
||||||
>
|
|
||||||
Title <span className="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="title"
|
|
||||||
name="title"
|
|
||||||
value={formData.title}
|
|
||||||
onChange={handleChange}
|
|
||||||
className={`w-full px-4 py-2 border ${errors.title ? "border-red-500" : "border-gray-300"} focus:outline-none focus:border-green-500`}
|
|
||||||
placeholder="e.g., Dell XPS 13 Laptop - 2023 Model"
|
|
||||||
/>
|
|
||||||
{errors.title && (
|
|
||||||
<p className="text-red-500 text-sm mt-1">{errors.title}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Price, Category, Status (side by side on larger screens) */}
|
|
||||||
<div className="flex flex-col md:flex-row gap-6 mb-6">
|
|
||||||
<div className="w-full md:w-1/3">
|
|
||||||
<label
|
|
||||||
htmlFor="price"
|
|
||||||
className="block text-gray-700 font-medium mb-2"
|
|
||||||
>
|
|
||||||
Price ($) <span className="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
id="price"
|
|
||||||
name="price"
|
|
||||||
value={formData.price}
|
|
||||||
onChange={handleChange}
|
|
||||||
className={`w-full px-4 py-2 border ${errors.price ? "border-red-500" : "border-gray-300"} focus:outline-none focus:border-green-500`}
|
|
||||||
placeholder="e.g., 850"
|
|
||||||
min="0"
|
|
||||||
step="0.01"
|
|
||||||
/>
|
|
||||||
{errors.price && (
|
|
||||||
<p className="text-red-500 text-sm mt-1">{errors.price}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full md:w-1/3">
|
|
||||||
<label
|
|
||||||
htmlFor="category"
|
|
||||||
className="block text-gray-700 font-medium mb-2"
|
|
||||||
>
|
|
||||||
Category <span className="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id="category"
|
|
||||||
name="category"
|
|
||||||
value={formData.category}
|
|
||||||
onChange={handleChange}
|
|
||||||
className={`w-full px-4 py-2 border ${errors.category ? "border-red-500" : "border-gray-300"} focus:outline-none focus:border-green-500 bg-white`}
|
|
||||||
>
|
|
||||||
<option value="">Select a category</option>
|
|
||||||
{categories.map((category) => (
|
|
||||||
<option key={category} value={category}>
|
|
||||||
{category}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
{errors.category && (
|
|
||||||
<p className="text-red-500 text-sm mt-1">{errors.category}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isEditing && (
|
|
||||||
<div className="w-full md:w-1/3">
|
|
||||||
<label
|
|
||||||
htmlFor="status"
|
|
||||||
className="block text-gray-700 font-medium mb-2"
|
|
||||||
>
|
|
||||||
Status
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id="status"
|
|
||||||
name="status"
|
|
||||||
value={formData.status}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 focus:outline-none focus:border-green-500 bg-white"
|
|
||||||
>
|
|
||||||
{statuses.map((status) => (
|
|
||||||
<option key={status} value={status}>
|
|
||||||
{status.charAt(0).toUpperCase() + status.slice(1)}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Condition */}
|
|
||||||
<div className="mb-6">
|
|
||||||
<label className="block text-gray-700 font-medium mb-2">
|
|
||||||
Condition <span className="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<div className="flex flex-wrap gap-3">
|
|
||||||
{conditions.map((condition) => (
|
|
||||||
<label
|
|
||||||
key={condition}
|
|
||||||
className={`px-4 py-2 border cursor-pointer ${
|
|
||||||
formData.condition === condition
|
|
||||||
? "bg-green-50 border-green-500 text-green-700"
|
|
||||||
: "border-gray-300 text-gray-700 hover:bg-gray-50"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="condition"
|
|
||||||
value={condition}
|
|
||||||
checked={formData.condition === condition}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="sr-only"
|
|
||||||
/>
|
|
||||||
{condition}
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{errors.condition && (
|
|
||||||
<p className="text-red-500 text-sm mt-1">{errors.condition}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Short Description */}
|
|
||||||
<div className="mb-6">
|
|
||||||
<label
|
|
||||||
htmlFor="shortDescription"
|
|
||||||
className="block text-gray-700 font-medium mb-2"
|
|
||||||
>
|
|
||||||
Short Description <span className="text-red-500">*</span>
|
|
||||||
<span className="text-sm font-normal text-gray-500 ml-2">
|
|
||||||
(Brief summary that appears in listings)
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="shortDescription"
|
|
||||||
name="shortDescription"
|
|
||||||
value={formData.shortDescription}
|
|
||||||
onChange={handleChange}
|
|
||||||
className={`w-full px-4 py-2 border ${errors.shortDescription ? "border-red-500" : "border-gray-300"} focus:outline-none focus:border-green-500`}
|
|
||||||
placeholder="e.g., Dell XPS 13 laptop in excellent condition. Intel Core i7, 16GB RAM, 512GB SSD."
|
|
||||||
maxLength="150"
|
|
||||||
/>
|
|
||||||
<p className="text-sm text-gray-500 mt-1">
|
|
||||||
{formData.shortDescription.length}/150 characters
|
|
||||||
</p>
|
|
||||||
{errors.shortDescription && (
|
|
||||||
<p className="text-red-500 text-sm">{errors.shortDescription}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Full Description */}
|
|
||||||
<div className="mb-6">
|
|
||||||
<label
|
|
||||||
htmlFor="description"
|
|
||||||
className="block text-gray-700 font-medium mb-2"
|
|
||||||
>
|
|
||||||
Full Description <span className="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
id="description"
|
|
||||||
name="description"
|
|
||||||
value={formData.description}
|
|
||||||
onChange={handleChange}
|
|
||||||
className={`w-full px-4 py-2 border ${errors.description ? "border-red-500" : "border-gray-300"} focus:outline-none focus:border-green-500 h-40`}
|
|
||||||
placeholder="Describe your item in detail. Include specs, condition, reason for selling, etc."
|
|
||||||
></textarea>
|
|
||||||
<p className="text-sm text-gray-500 mt-1">
|
|
||||||
Use blank lines to separate paragraphs.
|
|
||||||
</p>
|
|
||||||
{errors.description && (
|
|
||||||
<p className="text-red-500 text-sm">{errors.description}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Image Upload */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<label className="block text-gray-700 font-medium mb-2">
|
|
||||||
Images <span className="text-red-500">*</span>
|
|
||||||
<span className="text-sm font-normal text-gray-500 ml-2">
|
|
||||||
(Up to 5 images)
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{/* Image Preview Area */}
|
|
||||||
<div className="flex flex-wrap gap-4 mb-4">
|
|
||||||
{imagePreviewUrls.map((url, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="relative w-24 h-24 border border-gray-300"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={url}
|
|
||||||
alt={`Preview ${index + 1}`}
|
|
||||||
className="w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => removeImage(index)}
|
|
||||||
className="absolute -top-2 -right-2 bg-white rounded-full p-1 shadow-md border border-gray-300"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4 text-gray-600" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Upload Button (only show if less than 5 images) */}
|
|
||||||
{formData.images.length < 5 && (
|
|
||||||
<label className="w-24 h-24 border-2 border-dashed border-gray-300 flex flex-col items-center justify-center text-gray-500 cursor-pointer hover:bg-gray-50">
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
multiple
|
|
||||||
onChange={handleImageChange}
|
|
||||||
className="sr-only"
|
|
||||||
/>
|
|
||||||
<Plus className="h-6 w-6 mb-1" />
|
|
||||||
<span className="text-xs">Add Image</span>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{errors.images && (
|
|
||||||
<p className="text-red-500 text-sm">{errors.images}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Submit Button */}
|
|
||||||
<div className="mt-8">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
className={`w-full py-3 px-4 text-white font-medium flex items-center justify-center ${
|
|
||||||
isSubmitting ? "bg-gray-400" : "bg-green-500 hover:bg-green-600"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Save className="h-5 w-5 mr-2" />
|
|
||||||
{isSubmitting
|
|
||||||
? "Saving..."
|
|
||||||
: isEditing
|
|
||||||
? "Save Changes"
|
|
||||||
: "Create Listing"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{/* Delete Confirmation Modal */}
|
|
||||||
{showDeleteModal && (
|
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
||||||
<div className="bg-white p-6 rounded-md max-w-md w-full">
|
|
||||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
|
||||||
Delete Listing
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-600 mb-4">
|
|
||||||
Are you sure you want to delete <strong>{formData.title}</strong>?
|
|
||||||
This action cannot be undone.
|
|
||||||
</p>
|
|
||||||
<div className="flex justify-end space-x-3">
|
|
||||||
<button
|
|
||||||
onClick={() => setShowDeleteModal(false)}
|
|
||||||
className="px-4 py-2 border border-gray-300 text-gray-700 font-medium rounded-md hover:bg-gray-50"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handleDelete}
|
|
||||||
className="px-4 py-2 bg-red-600 text-white font-medium rounded-md hover:bg-red-700 flex items-center"
|
|
||||||
disabled={isSubmitting}
|
|
||||||
>
|
|
||||||
{isSubmitting ? "Deleting..." : "Delete"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ItemForm;
|
|
||||||
@@ -1,255 +1,613 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useParams, Link } from "react-router-dom";
|
import { useParams, Link } from "react-router-dom";
|
||||||
import { Heart, ArrowLeft, Tag, User, Calendar } from "lucide-react";
|
import {
|
||||||
|
Heart,
|
||||||
|
ArrowLeft,
|
||||||
|
Tag,
|
||||||
|
User,
|
||||||
|
Calendar,
|
||||||
|
Star,
|
||||||
|
Phone,
|
||||||
|
Mail,
|
||||||
|
Bookmark,
|
||||||
|
} from "lucide-react";
|
||||||
|
import FloatingAlert from "../components/FloatingAlert"; // adjust path if needed
|
||||||
|
|
||||||
const ProductDetail = () => {
|
const ProductDetail = () => {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const [product, setProduct] = useState(null);
|
const [product, setProduct] = useState(null);
|
||||||
|
const [loading, setLoading] = useState({
|
||||||
|
product: true,
|
||||||
|
reviews: true,
|
||||||
|
submitting: false,
|
||||||
|
});
|
||||||
|
const [error, setError] = useState({
|
||||||
|
product: null,
|
||||||
|
reviews: null,
|
||||||
|
submit: null,
|
||||||
|
});
|
||||||
const [isFavorite, setIsFavorite] = useState(false);
|
const [isFavorite, setIsFavorite] = useState(false);
|
||||||
const [showContactForm, setShowContactForm] = useState(false);
|
const [showContactOptions, setShowContactOptions] = useState(false);
|
||||||
const [message, setMessage] = useState("");
|
|
||||||
const [currentImage, setCurrentImage] = useState(0);
|
const [currentImage, setCurrentImage] = useState(0);
|
||||||
|
const [reviews, setReviews] = useState([]);
|
||||||
|
const [showReviewForm, setShowReviewForm] = useState(false);
|
||||||
|
const [showAlert, setShowAlert] = useState(false);
|
||||||
|
const storedUser = JSON.parse(sessionStorage.getItem("user"));
|
||||||
|
|
||||||
// Fetch product details
|
const toggleFavorite = async (id) => {
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
setShowAlert(true);
|
||||||
|
}
|
||||||
|
console.log(`Add Product -> History: ${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [reviewForm, setReviewForm] = useState({
|
||||||
|
rating: 3,
|
||||||
|
comment: "",
|
||||||
|
name: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add this function to handle review input changes
|
||||||
|
const handleReviewInputChange = (e) => {
|
||||||
|
const { id, value } = e.target;
|
||||||
|
setReviewForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[id]: value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add this function to handle star rating selection
|
||||||
|
const handleRatingChange = (rating) => {
|
||||||
|
setReviewForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
rating,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitReview = async (e) => {
|
||||||
|
e.preventDefault(); // Prevent form default behavior
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading((prev) => ({ ...prev, submitting: true }));
|
||||||
|
setError((prev) => ({ ...prev, submit: null }));
|
||||||
|
|
||||||
|
const reviewData = {
|
||||||
|
productId: id,
|
||||||
|
rating: reviewForm.rating,
|
||||||
|
comment: reviewForm.comment,
|
||||||
|
userId: storedUser.ID,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`http://localhost:3030/api/review/addReview`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(reviewData),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
// Check if API returned an error message even with 200 status
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.message || "Failed to submit review");
|
||||||
|
}
|
||||||
|
|
||||||
|
alert("Review submitted successfully!");
|
||||||
|
|
||||||
|
setReviewForm({
|
||||||
|
rating: 3,
|
||||||
|
comment: "",
|
||||||
|
name: "",
|
||||||
|
});
|
||||||
|
setShowReviewForm(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading((prev) => ({ ...prev, reviews: true }));
|
||||||
|
const reviewsResponse = await fetch(
|
||||||
|
`http://localhost:3030/api/review/${id}`,
|
||||||
|
);
|
||||||
|
const reviewsResult = await reviewsResponse.json();
|
||||||
|
|
||||||
|
if (reviewsResult.success) {
|
||||||
|
setReviews(reviewsResult.data || []);
|
||||||
|
setError((prev) => ({ ...prev, reviews: null }));
|
||||||
|
} else {
|
||||||
|
throw new Error(reviewsResult.message || "Error fetching reviews");
|
||||||
|
}
|
||||||
|
} catch (reviewsError) {
|
||||||
|
console.error("Error fetching reviews:", reviewsError);
|
||||||
|
setError((prev) => ({ ...prev, reviews: reviewsError.message }));
|
||||||
|
} finally {
|
||||||
|
setLoading((prev) => ({ ...prev, reviews: false }));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error submitting review:", error);
|
||||||
|
alert(`Error: ${error.message}`);
|
||||||
|
setError((prev) => ({
|
||||||
|
...prev,
|
||||||
|
submit: error.message,
|
||||||
|
}));
|
||||||
|
} finally {
|
||||||
|
setLoading((prev) => ({ ...prev, submitting: false }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch product data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchProduct = async () => {
|
const fetchProduct = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
setLoading((prev) => ({ ...prev, product: true }));
|
||||||
`http://localhost:3030/api/product/get_productID/${id}`,
|
const response = await fetch(`http://localhost:3030/api/product/${id}`);
|
||||||
);
|
|
||||||
if (!response.ok) throw new Error("Failed to fetch product");
|
|
||||||
|
|
||||||
const data = await response.json();
|
if (!response.ok) {
|
||||||
if (data.success) {
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
setProduct(data.data); // Update the state with product details
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
setProduct(result.data);
|
||||||
|
setError((prev) => ({ ...prev, product: null }));
|
||||||
} else {
|
} else {
|
||||||
throw new Error(data.message || "Error fetching product");
|
throw new Error(result.message || "Error fetching product");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching product:", error);
|
console.error("Error fetching product:", error);
|
||||||
|
setError((prev) => ({ ...prev, product: error.message }));
|
||||||
|
} finally {
|
||||||
|
setLoading((prev) => ({ ...prev, product: false }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchProduct();
|
fetchProduct();
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
// Handle favorite toggle
|
// Fetch reviews data
|
||||||
const toggleFavorite = () => {
|
useEffect(() => {
|
||||||
setIsFavorite(!isFavorite);
|
const fetchReviews = async () => {
|
||||||
|
try {
|
||||||
|
setLoading((prev) => ({ ...prev, reviews: true }));
|
||||||
|
const response = await fetch(`http://localhost:3030/api/review/${id}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
setReviews(result.data || []);
|
||||||
|
setError((prev) => ({ ...prev, reviews: null }));
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || "Error fetching reviews");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching reviews:", error);
|
||||||
|
setError((prev) => ({ ...prev, reviews: error.message }));
|
||||||
|
setReviews([]);
|
||||||
|
} finally {
|
||||||
|
setLoading((prev) => ({ ...prev, reviews: false }));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle message submission
|
fetchReviews();
|
||||||
const handleSendMessage = (e) => {
|
}, [id]);
|
||||||
e.preventDefault();
|
|
||||||
// Handle message logic here (send to seller)
|
|
||||||
console.log("Message sent:", message);
|
|
||||||
setMessage("");
|
|
||||||
setShowContactForm(false);
|
|
||||||
alert("Message sent to seller!");
|
|
||||||
};
|
|
||||||
|
|
||||||
// Image navigation
|
// Image navigation
|
||||||
const nextImage = () => {
|
const nextImage = () => {
|
||||||
|
if (product?.images?.length > 0) {
|
||||||
setCurrentImage((prev) =>
|
setCurrentImage((prev) =>
|
||||||
prev === product.images.length - 1 ? 0 : prev + 1,
|
prev === product.images.length - 1 ? 0 : prev + 1,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const prevImage = () => {
|
const prevImage = () => {
|
||||||
|
if (product?.images?.length > 0) {
|
||||||
setCurrentImage((prev) =>
|
setCurrentImage((prev) =>
|
||||||
prev === 0 ? product.images.length - 1 : prev - 1,
|
prev === 0 ? product.images.length - 1 : prev - 1,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectImage = (index) => {
|
const selectImage = (index) => {
|
||||||
setCurrentImage(index);
|
setCurrentImage(index);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!product) return <div>Loading...</div>; // Handle loading state
|
// Function to render stars based on rating
|
||||||
|
const renderStars = (rating) => {
|
||||||
|
const stars = [];
|
||||||
|
for (let i = 1; i <= 5; i++) {
|
||||||
|
stars.push(
|
||||||
|
<Star
|
||||||
|
key={i}
|
||||||
|
className={`h-4 w-4 ${i <= rating ? "text-yellow-400 fill-yellow-400" : "text-gray-300"}`}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return stars;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render loading state for the entire page
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render error state for product
|
||||||
|
if (error.product) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center h-screen">
|
||||||
|
<div className="text-center">
|
||||||
|
<h2 className="text-2xl text-red-500 mb-4">Error Loading Product</h2>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
Back to Listings
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety check for product
|
||||||
|
if (!product) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center h-screen">
|
||||||
|
<div className="text-center">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
Back to Listings
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render product details
|
||||||
return (
|
return (
|
||||||
<div className="max-w-6xl mx-auto px-4 py-8">
|
<div className="max-w-6xl mx-auto px-4 py-8">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/search"
|
||||||
className="flex items-center text-green-600 hover:text-green-700"
|
className="flex items-center text-green-600 hover:text-green-700"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-4 w-4 mr-1" />
|
<ArrowLeft className="h-4 w-4 mr-1" />
|
||||||
<span>Back to listings</span>
|
<span>Back</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
{showAlert && (
|
||||||
|
<FloatingAlert
|
||||||
|
message="Product added to favorites!"
|
||||||
|
onClose={() => setShowAlert(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col md:flex-row gap-8">
|
<div className="flex flex-col md:flex-row gap-8">
|
||||||
<div className="md:w-3/5">
|
<div className="md:w-3/5">
|
||||||
<div className="bg-white border border-gray-200 mb-4 relative">
|
<div className="bg-white border border-gray-200 mb-4 relative">
|
||||||
|
{product.images && product.images.length > 0 ? (
|
||||||
|
<>
|
||||||
<img
|
<img
|
||||||
src={product.images[currentImage]}
|
src={product.images[currentImage]}
|
||||||
alt={product.title}
|
alt={product.Name}
|
||||||
className="w-full h-auto object-contain cursor-pointer"
|
className="w-full h-auto object-contain cursor-pointer"
|
||||||
onClick={nextImage}
|
onClick={nextImage}
|
||||||
|
onError={(e) => {
|
||||||
|
e.target.onerror = null;
|
||||||
|
e.target.src = "https://via.placeholder.com/400x300";
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
{product.images.length > 1 && (
|
||||||
|
<div className="absolute inset-x-0 bottom-0 flex justify-between p-2">
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
prevImage();
|
||||||
|
}}
|
||||||
|
className="bg-white/70 p-1"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
<div className="text-sm bg-white/70 px-2 py-1 ">
|
||||||
|
{currentImage + 1}/{product.images.length}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-96 flex items-center justify-center bg-gray-200 text-gray-500">
|
||||||
|
No Image Available
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{product.images.length > 1 && (
|
{product.images && product.images.length > 1 && (
|
||||||
<div className="flex gap-2 overflow-x-auto pb-2">
|
<div className="flex gap-2 overflow-x-auto pb-2">
|
||||||
{product.images.map((image, index) => (
|
{product.images.map((image, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className={`bg-white border ${currentImage === index ? "border-green-500" : "border-gray-200"} min-w-[100px] cursor-pointer`}
|
className={`bg-white border ${currentImage === index ? "border-green-500 border-2" : "border-gray-200"} min-w-[100px] cursor-pointer`}
|
||||||
onClick={() => selectImage(index)}
|
onClick={() => selectImage(index)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={image}
|
src={image}
|
||||||
alt={`${product.title} - view ${index + 1}`}
|
alt={`${product.Name} - view ${index + 1}`}
|
||||||
className="w-full h-auto object-cover"
|
className="w-full h-auto object-cover"
|
||||||
|
onError={(e) => {
|
||||||
|
e.target.onerror = null;
|
||||||
|
e.target.src =
|
||||||
|
"https://via.placeholder.com/100x100?text=Error";
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="md:w-2/5">
|
<div className="md:w-2/5">
|
||||||
<div className="bg-white border border-gray-200 p-6 mb-6">
|
<div className="bg-white border border-gray-200 p-6 mb-6">
|
||||||
<div className="flex justify-between items-start mb-4">
|
<div className="flex justify-between items-start mb-4">
|
||||||
<h1 className="text-2xl font-bold text-gray-800">
|
<h1 className="text-2xl font-bold text-gray-800">
|
||||||
{product.title}
|
{product.Name || "Unnamed Product"}
|
||||||
</h1>
|
</h1>
|
||||||
<button
|
<button
|
||||||
onClick={toggleFavorite}
|
onClick={(e) => {
|
||||||
className="p-2 hover:bg-gray-100"
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
toggleFavorite(product.ProductID);
|
||||||
|
}}
|
||||||
|
className="top-0 p-2 rounded-bl-md bg-emerald-600 hover:bg-emerald-500 transition shadow-sm"
|
||||||
>
|
>
|
||||||
<Heart
|
<Bookmark className="text-white w-5 h-5" />
|
||||||
className={`h-6 w-6 ${isFavorite ? "text-red-500 fill-red-500" : "text-gray-400"}`}
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-2xl font-bold text-green-600 mb-4">
|
<div className="text-2xl font-bold text-green-600 mb-4">
|
||||||
${product.price}
|
$
|
||||||
|
{typeof product.Price === "number"
|
||||||
|
? product.Price.toFixed(2)
|
||||||
|
: product.Price}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-x-4 gap-y-2 mb-6 text-sm">
|
<div className="flex flex-wrap gap-x-4 gap-y-2 mb-6 text-sm">
|
||||||
|
{product.Category && (
|
||||||
<div className="flex items-center text-gray-600">
|
<div className="flex items-center text-gray-600">
|
||||||
<Tag className="h-4 w-4 mr-1" />
|
<Tag className="h-4 w-4 mr-1" />
|
||||||
<span>{product.category}</span>
|
<span>{product.Category}</span>
|
||||||
</div>
|
|
||||||
<div className="flex items-center text-gray-600">
|
|
||||||
<span className="font-medium">Condition:</span>
|
|
||||||
<span className="ml-1">{product.condition}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{product.Date && (
|
||||||
<div className="flex items-center text-gray-600">
|
<div className="flex items-center text-gray-600">
|
||||||
<Calendar className="h-4 w-4 mr-1" />
|
<Calendar className="h-4 w-4 mr-1" />
|
||||||
<span>Posted on {product.datePosted}</span>
|
<span>Posted on {product.Date}</span>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-gray-50 p-4 mb-6 border border-gray-200">
|
<div className="bg-gray-50 p-4 mb-6 border border-gray-200">
|
||||||
<p className="text-gray-700">{product.shortDescription}</p>
|
<p className="text-gray-700">
|
||||||
|
{product.Description || "No description available"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowContactForm(!showContactForm)}
|
onClick={() => setShowContactOptions(!showContactOptions)}
|
||||||
className="w-full bg-green-500 hover:bg-green-600 text-white font-medium py-3 px-4 mb-3"
|
className="w-full bg-green-500 hover:bg-green-600 text-white font-medium py-3 px-4 mb-3"
|
||||||
>
|
>
|
||||||
Contact Seller
|
Contact Seller
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{showContactForm && (
|
{showContactOptions && (
|
||||||
<div className="border border-gray-200 p-4 mb-4">
|
<div className="absolute z-10 w-full bg-white border border-gray-200 shadow-md">
|
||||||
<h3 className="font-medium text-gray-800 mb-2">
|
{product.SellerPhone && (
|
||||||
Contact Seller
|
<a
|
||||||
</h3>
|
href={`tel:${product.SellerPhone}`}
|
||||||
<form onSubmit={handleSendMessage}>
|
className="flex items-center gap-2 p-3 hover:bg-gray-50 border-b border-gray-100"
|
||||||
<div className="mb-3">
|
|
||||||
<label htmlFor="email" className="block text-gray-700 mb-1">
|
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
id="email"
|
|
||||||
value={message}
|
|
||||||
onChange={(e) => setMessage(e.target.value)}
|
|
||||||
className="w-full p-3 border border-gray-300 focus:outline-none focus:border-green-500"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mb-3">
|
|
||||||
<label htmlFor="phone" className="block text-gray-700 mb-1">
|
|
||||||
Phone Number
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="tel"
|
|
||||||
id="phone"
|
|
||||||
className="w-full p-3 border border-gray-300 focus:outline-none focus:border-green-500"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mb-3">
|
|
||||||
<label
|
|
||||||
htmlFor="contactMessage"
|
|
||||||
className="block text-gray-700 mb-1"
|
|
||||||
>
|
>
|
||||||
Message (Optional)
|
<Phone className="h-5 w-5 text-green-500" />
|
||||||
</label>
|
<span>Call Seller</span>
|
||||||
<input
|
</a>
|
||||||
type="text"
|
|
||||||
id="contactMessage"
|
|
||||||
value={message}
|
|
||||||
onChange={(e) => setMessage(e.target.value)}
|
|
||||||
placeholder="Hi, is this item still available?"
|
|
||||||
className="w-full p-3 border border-gray-300 focus:outline-none focus:border-green-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-4"
|
|
||||||
>
|
|
||||||
Send Contact Info
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="pt-4 border-t border-gray-200">
|
{product.SellerEmail && (
|
||||||
<div className="flex items-center mb-3">
|
<a
|
||||||
<div className="mr-3">
|
href={`mailto:${product.SellerEmail}`}
|
||||||
{product.seller.avatar ? (
|
className="flex items-center gap-2 p-3 hover:bg-gray-50"
|
||||||
<img
|
>
|
||||||
src={product.seller.avatar}
|
<Mail className="h-5 w-5 text-green-500" />
|
||||||
alt="Seller"
|
<span>Email Seller</span>
|
||||||
className="h-12 w-12 rounded-full"
|
</a>
|
||||||
/>
|
)}
|
||||||
) : (
|
|
||||||
<div className="h-12 w-12 rounded-full bg-gray-200 flex items-center justify-center">
|
|
||||||
<User className="h-6 w-6 text-gray-600" />
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 border-t border-gray-200">
|
||||||
|
{/* Seller Info */}
|
||||||
|
<div className="flex items-center mb-3">
|
||||||
|
<div className="mr-3">
|
||||||
|
<div className="h-12 w-12 bg-gray-200 flex items-center justify-center">
|
||||||
|
<User className="h-6 w-6 text-gray-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-medium text-gray-800">
|
<h3 className="font-medium text-gray-800">
|
||||||
{product.seller.name}
|
{product.SellerName || "Unknown Seller"}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
Member since {product.seller.memberSince}
|
Member since {product.SellerJoinDate || "N/A"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-600">
|
|
||||||
<div>
|
|
||||||
<span className="font-medium">Rating:</span>{" "}
|
|
||||||
{product.seller.rating}/5
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Reviews Section */}
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<h2 className="text-xl font-bold text-gray-800 mb-4">Description</h2>
|
<h2 className="text-xl font-bold text-gray-800 mb-4">Reviews</h2>
|
||||||
|
|
||||||
<div className="bg-white border border-gray-200 p-6">
|
<div className="bg-white border border-gray-200 p-6">
|
||||||
<div className="text-gray-700">{product.description}</div>
|
{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>
|
</div>
|
||||||
|
) : error.reviews ? (
|
||||||
|
<div className="text-red-500 mb-4">
|
||||||
|
Error loading reviews: {error.reviews}
|
||||||
|
</div>
|
||||||
|
) : reviews.length === 0 ? (
|
||||||
|
<div className="text-gray-500 text-center py-6">
|
||||||
|
No reviews yet for this product
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{reviews.map((review) => (
|
||||||
|
<div
|
||||||
|
key={review.id || `review-${Math.random()}`}
|
||||||
|
className="border-b border-gray-200 pb-6 last:border-0 last:pb-0"
|
||||||
|
>
|
||||||
|
<div className="flex justify-between mb-2">
|
||||||
|
<div className="font-medium text-gray-800">
|
||||||
|
{review.ReviewerName}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
{review.ReviewDate
|
||||||
|
? new Date(review.ReviewDate).toLocaleDateString()
|
||||||
|
: "Unknown date"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center mb-2">
|
||||||
|
{renderStars(review.Rating || 0)}
|
||||||
|
<span className="ml-2 text-sm text-gray-600">
|
||||||
|
{review.Rating || 0}/5
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-gray-700">
|
||||||
|
{review.Comment || "No comment provided"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowReviewForm(true)}
|
||||||
|
className="bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-4"
|
||||||
|
>
|
||||||
|
Write a Review
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Review Popup Form */}
|
||||||
|
{showReviewForm && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||||
|
<div className="bg-white shadow-xl max-w-md w-full p-6">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h3 className="text-xl font-bold text-gray-800">
|
||||||
|
Write a Review
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowReviewForm(false)}
|
||||||
|
className="text-gray-500 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmitReview}>
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="block text-gray-700 mb-1">
|
||||||
|
Rating <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<div className="flex items-center">
|
||||||
|
{[1, 2, 3, 4, 5].map((star) => (
|
||||||
|
<button
|
||||||
|
key={star}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleRatingChange(star)}
|
||||||
|
className="focus:outline-none mr-1"
|
||||||
|
>
|
||||||
|
<Star
|
||||||
|
className={`h-6 w-6 ${
|
||||||
|
star <= reviewForm.rating
|
||||||
|
? "text-yellow-400 fill-yellow-400"
|
||||||
|
: "text-gray-300"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<span className="ml-2 text-gray-600">
|
||||||
|
{reviewForm.rating}/5
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<label htmlFor="comment" className="block text-gray-700 mb-1">
|
||||||
|
Your Review <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="comment"
|
||||||
|
value={reviewForm.comment}
|
||||||
|
onChange={handleReviewInputChange}
|
||||||
|
className="w-full p-3 border border-gray-300 focus:outline-none focus:border-green-500"
|
||||||
|
rows="4"
|
||||||
|
required
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end space-x-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowReviewForm(false)}
|
||||||
|
className="px-4 py-2 border border-gray-300 text-gray-700 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-4 py-2 bg-green-500 text-white hover:bg-green-600"
|
||||||
|
disabled={loading.submitting}
|
||||||
|
>
|
||||||
|
{loading.submitting ? "Submitting..." : "Submit Review"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
257
frontend/src/pages/SearchPage.jsx
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useLocation, Link } from "react-router-dom";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const SearchPage = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
const queryParams = new URLSearchParams(location.search);
|
||||||
|
const nameParam = queryParams.get("name") || "";
|
||||||
|
const initialSearchQuery = location.state?.query || nameParam || "";
|
||||||
|
|
||||||
|
const [products, setProducts] = useState([]);
|
||||||
|
const [filteredProducts, setFilteredProducts] = useState([]);
|
||||||
|
const [searchQuery, setSearchQuery] = useState(initialSearchQuery);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [priceRange, setPriceRange] = useState({ min: 0, max: 1000 });
|
||||||
|
const [isFilterOpen, setIsFilterOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchProducts(initialSearchQuery);
|
||||||
|
}, [initialSearchQuery]);
|
||||||
|
|
||||||
|
const fetchProducts = async (query) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(
|
||||||
|
`http://localhost:3030/api/search/getProduct`,
|
||||||
|
{
|
||||||
|
params: { name: query },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
const transformedProducts = response.data.data.map((product) => ({
|
||||||
|
id: product.ProductID,
|
||||||
|
title: product.Name,
|
||||||
|
description: product.Description || "",
|
||||||
|
price: product.Price || 0,
|
||||||
|
category: product.Category || "Uncategorized",
|
||||||
|
condition: product.Condition || "Used",
|
||||||
|
image: product.images,
|
||||||
|
seller: product.SellerName || "Unknown Seller",
|
||||||
|
isFavorite: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setProducts(transformedProducts);
|
||||||
|
setFilteredProducts(transformedProducts);
|
||||||
|
} else {
|
||||||
|
setError(response.data.message || "Failed to fetch products");
|
||||||
|
setProducts([]);
|
||||||
|
setFilteredProducts([]);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching products:", err);
|
||||||
|
setError(err.response?.data?.message || "Error connecting to the server");
|
||||||
|
setProducts([]);
|
||||||
|
setFilteredProducts([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleFavorite = (id, e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setProducts((prev) =>
|
||||||
|
prev.map((product) =>
|
||||||
|
product.id === id
|
||||||
|
? { ...product, isFavorite: !product.isFavorite }
|
||||||
|
: product,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterProducts = () => {
|
||||||
|
let result = products;
|
||||||
|
result = result.filter(
|
||||||
|
(product) =>
|
||||||
|
product.price >= priceRange.min && product.price <= priceRange.max,
|
||||||
|
);
|
||||||
|
setFilteredProducts(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyFilters = () => {
|
||||||
|
filterProducts();
|
||||||
|
setIsFilterOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
setPriceRange({ min: 0, max: 1000 });
|
||||||
|
setFilteredProducts(products);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<div className="flex flex-col md:flex-row gap-6">
|
||||||
|
{/* Filter sidebar */}
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
fixed inset-0 z-50 bg-white transform transition-transform duration-300
|
||||||
|
${isFilterOpen ? "translate-x-0" : "translate-x-full"}
|
||||||
|
md:translate-x-0 md:relative md:block md:w-72
|
||||||
|
overflow-y-auto shadow-md
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div className="md:hidden flex justify-between items-center p-4 border-b">
|
||||||
|
<h3 className="text-lg font-semibold">Filters</h3>
|
||||||
|
<button onClick={() => setIsFilterOpen(false)}>
|
||||||
|
<X className="text-gray-600" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
<div className="bg-gray-50 p-3">
|
||||||
|
<h3 className="font-semibold text-gray-700 mb-3">Price Range</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="Min"
|
||||||
|
value={priceRange.min}
|
||||||
|
onChange={(e) =>
|
||||||
|
setPriceRange((prev) => ({
|
||||||
|
...prev,
|
||||||
|
min: Number(e.target.value),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className="w-full p-2 border text-gray-700"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="Max"
|
||||||
|
value={priceRange.max}
|
||||||
|
onChange={(e) =>
|
||||||
|
setPriceRange((prev) => ({
|
||||||
|
...prev,
|
||||||
|
max: Number(e.target.value),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className="w-full p-2 border text-gray-700"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
Apply Filters
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={resetFilters}
|
||||||
|
className="w-full bg-gray-200 text-gray-700 p-3 hover:bg-gray-300 transition-colors"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
<div className="flex-1 mt-4 md:mt-0">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-800">
|
||||||
|
{filteredProducts.length} Results
|
||||||
|
{searchQuery && (
|
||||||
|
<span className="text-lg font-normal text-gray-600">
|
||||||
|
{" "}
|
||||||
|
for "{searchQuery}"
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mt-4">
|
||||||
|
{filteredProducts.map((listing) => (
|
||||||
|
<Link
|
||||||
|
key={listing.id}
|
||||||
|
to={`/product/${listing.id}`}
|
||||||
|
className="bg-white border border-gray-200 hover:shadow-md transition-shadow block"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={listing.image}
|
||||||
|
alt={listing.title}
|
||||||
|
className="w-full h-48 object-cover"
|
||||||
|
/>
|
||||||
|
<div className="p-4">
|
||||||
|
<h3 className="text-lg font-medium text-gray-800">
|
||||||
|
{listing.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-emerald-600 font-semibold">
|
||||||
|
${Number(listing.price).toFixed(2)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</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>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchPage;
|
||||||
@@ -1,11 +1,222 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import { Link } from 'react-router-dom';
|
import ProductForm from "../components/ProductForm";
|
||||||
import { Tag, Book, Laptop, Sofa, Utensils, Gift, Heart } from 'lucide-react';
|
|
||||||
|
|
||||||
const Selling = () => {
|
const Selling = () => {
|
||||||
return (
|
// State to store user's products
|
||||||
<div>
|
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 [editingProduct, setEditingProduct] = useState({
|
||||||
|
name: "",
|
||||||
|
price: "",
|
||||||
|
description: "",
|
||||||
|
categories: [],
|
||||||
|
status: "Unsold",
|
||||||
|
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: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
setProducts(mockProducts);
|
||||||
|
};
|
||||||
|
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset form and hide it
|
||||||
|
setShowForm(false);
|
||||||
|
setEditingProduct({
|
||||||
|
name: "",
|
||||||
|
price: "",
|
||||||
|
description: "",
|
||||||
|
categories: [],
|
||||||
|
status: "Unsold",
|
||||||
|
images: [],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle editing a product
|
||||||
|
const handleEditProduct = (product) => {
|
||||||
|
setEditingProduct({
|
||||||
|
...product,
|
||||||
|
images: product.images || [], // Ensure images array exists
|
||||||
|
});
|
||||||
|
setShowForm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle adding a new product
|
||||||
|
const handleAddProduct = () => {
|
||||||
|
setEditingProduct({
|
||||||
|
name: "",
|
||||||
|
price: "",
|
||||||
|
description: "",
|
||||||
|
categories: [],
|
||||||
|
status: "Unsold",
|
||||||
|
images: [],
|
||||||
|
});
|
||||||
|
setShowForm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-4 max-w-6xl">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-800">My Listings</h1>
|
||||||
|
{!showForm && (
|
||||||
|
<button
|
||||||
|
onClick={handleAddProduct}
|
||||||
|
className="bg-emerald-600 text-white px-4 py-2 hover:bg-emerald-700"
|
||||||
|
>
|
||||||
|
+ Add New Product
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showForm ? (
|
||||||
|
<ProductForm
|
||||||
|
editingProduct={editingProduct}
|
||||||
|
setEditingProduct={setEditingProduct}
|
||||||
|
onSave={handleSaveProduct}
|
||||||
|
onCancel={() => setShowForm(false)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{products.length === 0 ? (
|
||||||
|
<div className="text-center py-10 bg-gray-50">
|
||||||
|
<p className="text-gray-500 mb-4">
|
||||||
|
You don't have any listings yet
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={handleAddProduct}
|
||||||
|
className="bg-emerald-600 text-white px-4 py-2 hover:bg-emerald-700"
|
||||||
|
>
|
||||||
|
Create Your First Listing
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<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={() => 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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { User, Lock, Trash2, History, Search, Shield } from "lucide-react";
|
import { User, Lock, Trash2, History, Shield } from "lucide-react";
|
||||||
|
import FloatingAlert from "../components/FloatingAlert"; // adjust path if needed
|
||||||
|
|
||||||
const Settings = () => {
|
const Settings = () => {
|
||||||
const [userData, setUserData] = useState({
|
const [userData, setUserData] = useState({
|
||||||
@@ -9,13 +10,13 @@ const Settings = () => {
|
|||||||
phone: "",
|
phone: "",
|
||||||
UCID: "",
|
UCID: "",
|
||||||
address: "",
|
address: "",
|
||||||
currentPassword: "",
|
password: "",
|
||||||
newPassword: "",
|
|
||||||
confirmPassword: "",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
const [showAlert, setShowAlert] = useState(false);
|
||||||
|
const storedUser = JSON.parse(sessionStorage.getItem("user"));
|
||||||
|
|
||||||
// Fetch user data when component mounts
|
// Fetch user data when component mounts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -59,10 +60,7 @@ const Settings = () => {
|
|||||||
UCID: data.UCID || storedUser.UCID || "",
|
UCID: data.UCID || storedUser.UCID || "",
|
||||||
phone: data.phone || storedUser.phone || "",
|
phone: data.phone || storedUser.phone || "",
|
||||||
address: data.address || storedUser.address || "",
|
address: data.address || storedUser.address || "",
|
||||||
// Reset password fields
|
password: data.password,
|
||||||
currentPassword: "",
|
|
||||||
newPassword: "",
|
|
||||||
confirmPassword: "",
|
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
throw new Error(data.error || "Failed to retrieve user data");
|
throw new Error(data.error || "Failed to retrieve user data");
|
||||||
@@ -88,6 +86,25 @@ const Settings = () => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const removeHistory = async () => {
|
||||||
|
const response = await fetch(
|
||||||
|
"http://localhost:3030/api/history/delHistory",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
userID: storedUser.ID,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
setShowAlert(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleUpdateProfile = async () => {
|
const handleUpdateProfile = async () => {
|
||||||
try {
|
try {
|
||||||
// Ensure userId is present
|
// Ensure userId is present
|
||||||
@@ -124,47 +141,6 @@ const Settings = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePasswordUpdate = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
try {
|
|
||||||
// Validate passwords match
|
|
||||||
if (userData.newPassword !== userData.confirmPassword) {
|
|
||||||
alert("New passwords do not match!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Implement the actual password update API call
|
|
||||||
console.log("Password updated");
|
|
||||||
|
|
||||||
// Update password in localStorage
|
|
||||||
const storedUser = JSON.parse(localStorage.getItem("user"));
|
|
||||||
const updatedUser = {
|
|
||||||
...storedUser,
|
|
||||||
password: userData.newPassword,
|
|
||||||
};
|
|
||||||
localStorage.setItem("user", JSON.stringify(updatedUser));
|
|
||||||
|
|
||||||
// Reset password fields
|
|
||||||
setUserData((prevData) => ({
|
|
||||||
...prevData,
|
|
||||||
currentPassword: "",
|
|
||||||
newPassword: "",
|
|
||||||
confirmPassword: "",
|
|
||||||
}));
|
|
||||||
|
|
||||||
alert("Password updated successfully!");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error updating password:", error);
|
|
||||||
alert("Failed to update password: " + error.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteHistory = (type) => {
|
|
||||||
// TODO: Delete the specified history
|
|
||||||
console.log(`Deleting ${type} history`);
|
|
||||||
alert(`${type} history deleted successfully!`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteAccount = async () => {
|
const handleDeleteAccount = async () => {
|
||||||
if (
|
if (
|
||||||
window.confirm(
|
window.confirm(
|
||||||
@@ -214,7 +190,7 @@ const Settings = () => {
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center items-center h-64">
|
<div className="flex justify-center items-center h-64">
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-green-500"></div>
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-emerald-500"></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -258,7 +234,7 @@ const Settings = () => {
|
|||||||
id="name"
|
id="name"
|
||||||
value={userData.name}
|
value={userData.name}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500"
|
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-500"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -275,7 +251,7 @@ const Settings = () => {
|
|||||||
id="email"
|
id="email"
|
||||||
value={userData.email}
|
value={userData.email}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500"
|
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-500"
|
||||||
required
|
required
|
||||||
readOnly // Email is often used as identifier and not changeable
|
readOnly // Email is often used as identifier and not changeable
|
||||||
/>
|
/>
|
||||||
@@ -293,7 +269,7 @@ const Settings = () => {
|
|||||||
id="phone"
|
id="phone"
|
||||||
value={userData.phone}
|
value={userData.phone}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500"
|
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -309,7 +285,7 @@ const Settings = () => {
|
|||||||
id="UCID"
|
id="UCID"
|
||||||
value={userData.UCID}
|
value={userData.UCID}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500"
|
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-500"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -326,14 +302,29 @@ const Settings = () => {
|
|||||||
id="address"
|
id="address"
|
||||||
value={userData.address}
|
value={userData.address}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500"
|
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="password"
|
||||||
|
className="block text-sm font-medium text-gray-700 mb-1"
|
||||||
|
>
|
||||||
|
password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="password"
|
||||||
|
value={userData.password}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-4"
|
className="bg-emerald-500 hover:bg-emerald-600 text-white font-medium py-2 px-4"
|
||||||
>
|
>
|
||||||
Update Profile
|
Update Profile
|
||||||
</button>
|
</button>
|
||||||
@@ -341,81 +332,13 @@ const Settings = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Security Section */}
|
|
||||||
<div className="bg-white border border-gray-200 mb-6">
|
|
||||||
<div className="border-b border-gray-200 p-4">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Lock className="h-5 w-5 text-gray-500 mr-2" />
|
|
||||||
<h2 className="text-lg font-medium text-gray-800">Password</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-4">
|
|
||||||
<form onSubmit={handlePasswordUpdate}>
|
|
||||||
<div className="space-y-4 mb-4">
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="currentPassword"
|
|
||||||
className="block text-sm font-medium text-gray-700 mb-1"
|
|
||||||
>
|
|
||||||
Current Password
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="currentPassword"
|
|
||||||
value={userData.currentPassword}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="newPassword"
|
|
||||||
className="block text-sm font-medium text-gray-700 mb-1"
|
|
||||||
>
|
|
||||||
New Password
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="newPassword"
|
|
||||||
value={userData.newPassword}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="confirmPassword"
|
|
||||||
className="block text-sm font-medium text-gray-700 mb-1"
|
|
||||||
>
|
|
||||||
Confirm New Password
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="confirmPassword"
|
|
||||||
value={userData.confirmPassword}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-4"
|
|
||||||
>
|
|
||||||
Change Password
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Privacy Section */}
|
{/* Privacy Section */}
|
||||||
|
{showAlert && (
|
||||||
|
<FloatingAlert
|
||||||
|
message="We Removed Your History! 😉"
|
||||||
|
onClose={() => setShowAlert(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="bg-white border border-gray-200 mb-6">
|
<div className="bg-white border border-gray-200 mb-6">
|
||||||
<div className="border-b border-gray-200 p-4">
|
<div className="border-b border-gray-200 p-4">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@@ -426,39 +349,18 @@ const Settings = () => {
|
|||||||
|
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex justify-between items-center pb-4 border-b border-gray-100">
|
|
||||||
<div className="flex items-start">
|
|
||||||
<Search className="h-5 w-5 text-gray-500 mr-2 mt-0.5" />
|
|
||||||
<div>
|
|
||||||
<h3 className="font-medium text-gray-800">Search History</h3>
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
Delete all your search history on StudentMarket
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => handleDeleteHistory("search")}
|
|
||||||
className="bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium py-2 px-4 flex items-center"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4 mr-1" />
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex items-start">
|
<div className="flex items-start">
|
||||||
<History className="h-5 w-5 text-gray-500 mr-2 mt-0.5" />
|
<History className="h-5 w-5 text-gray-500 mr-2 mt-0.5" />
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-medium text-gray-800">
|
<h3 className="font-medium text-gray-800"> History</h3>
|
||||||
Browsing History
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
Delete all your browsing history on StudentMarket
|
Delete all your history on Market
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDeleteHistory("browsing")}
|
onClick={() => removeHistory()}
|
||||||
className="bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium py-2 px-4 flex items-center"
|
className="bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium py-2 px-4 flex items-center"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4 mr-1" />
|
<Trash2 className="h-4 w-4 mr-1" />
|
||||||
@@ -472,7 +374,7 @@ const Settings = () => {
|
|||||||
{/* Delete Account (Danger Zone) */}
|
{/* Delete Account (Danger Zone) */}
|
||||||
<div className="bg-white border border-red-200 mb-6">
|
<div className="bg-white border border-red-200 mb-6">
|
||||||
<div className="border-b border-red-200 p-4 bg-red-50">
|
<div className="border-b border-red-200 p-4 bg-red-50">
|
||||||
<h2 className="text-lg font-medium text-red-700">Danger Zone</h2>
|
<h2 className="text-lg font-medium text-red-700">Danger Zone !!!</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from "react-router-dom";
|
||||||
import { Tag, Book, Laptop, Sofa, Utensils, Gift, Heart } from 'lucide-react';
|
|
||||||
|
|
||||||
const Transactions = () => {
|
const Transactions = () => {
|
||||||
return (
|
return <div></div>;
|
||||||
<div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Transactions;
|
export default Transactions;
|
||||||
455
mysql-code/Init-Data.sql
Normal file
@@ -0,0 +1,455 @@
|
|||||||
|
-- Inserting sample data into the Marketplace database
|
||||||
|
SET
|
||||||
|
FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
TRUNCATE TABLE Product_Category;
|
||||||
|
|
||||||
|
TRUNCATE TABLE Favorites;
|
||||||
|
|
||||||
|
TRUNCATE TABLE History;
|
||||||
|
|
||||||
|
TRUNCATE TABLE Recommendation;
|
||||||
|
|
||||||
|
TRUNCATE TABLE Transaction;
|
||||||
|
|
||||||
|
TRUNCATE TABLE Review;
|
||||||
|
|
||||||
|
TRUNCATE TABLE Image_URL;
|
||||||
|
|
||||||
|
TRUNCATE TABLE Product;
|
||||||
|
|
||||||
|
TRUNCATE TABLE Category;
|
||||||
|
|
||||||
|
TRUNCATE TABLE UserRole;
|
||||||
|
|
||||||
|
TRUNCATE TABLE User;
|
||||||
|
|
||||||
|
TRUNCATE TABLE AuthVerification;
|
||||||
|
|
||||||
|
SET
|
||||||
|
FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|
||||||
|
-- Insert Users
|
||||||
|
INSERT INTO
|
||||||
|
User (
|
||||||
|
UserID,
|
||||||
|
Name,
|
||||||
|
Email,
|
||||||
|
UCID,
|
||||||
|
Password,
|
||||||
|
Phone,
|
||||||
|
Address
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
1,
|
||||||
|
'John Doe',
|
||||||
|
'john.doe@example.com',
|
||||||
|
'U123456',
|
||||||
|
'hashedpassword1',
|
||||||
|
'555-123-4567',
|
||||||
|
'123 Main St, Calgary, AB'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
2,
|
||||||
|
'Jane Smith',
|
||||||
|
'jane.smith@example.com',
|
||||||
|
'U234567',
|
||||||
|
'hashedpassword2',
|
||||||
|
'555-234-5678',
|
||||||
|
'456 Oak Ave, Calgary, AB'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Insert User Roles
|
||||||
|
INSERT INTO
|
||||||
|
UserRole (UserID, Client, Admin)
|
||||||
|
VALUES
|
||||||
|
(1, TRUE, TRUE),
|
||||||
|
(2, TRUE, FALSE);
|
||||||
|
|
||||||
|
-- Insert Categories
|
||||||
|
INSERT INTO
|
||||||
|
Category (CategoryID, 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)');
|
||||||
|
|
||||||
|
-- Insert Products
|
||||||
|
INSERT INTO
|
||||||
|
Product (
|
||||||
|
ProductID,
|
||||||
|
Name,
|
||||||
|
Price,
|
||||||
|
StockQuantity,
|
||||||
|
UserID,
|
||||||
|
Description,
|
||||||
|
CategoryID,
|
||||||
|
Date
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
1,
|
||||||
|
'Calculus Textbook 8th Edition',
|
||||||
|
79.99,
|
||||||
|
5,
|
||||||
|
1,
|
||||||
|
'Like new calculus textbook, minor highlighting',
|
||||||
|
1,
|
||||||
|
'2024-10-15 10:00:00'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
2,
|
||||||
|
'HP Laptop',
|
||||||
|
699.99,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
'2023 HP Pavilion, 16GB RAM, 512GB SSD',
|
||||||
|
2,
|
||||||
|
'2024-10-10 14:30:00'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
3,
|
||||||
|
'Dorm Desk',
|
||||||
|
120.00,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
'Sturdy desk perfect for studying, minor scratches',
|
||||||
|
3,
|
||||||
|
'2024-10-12 09:15:00'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
4,
|
||||||
|
'University Hoodie',
|
||||||
|
35.00,
|
||||||
|
3,
|
||||||
|
2,
|
||||||
|
'Size L, university logo, worn twice',
|
||||||
|
4,
|
||||||
|
'2024-10-14 16:45:00'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
5,
|
||||||
|
'Basketball',
|
||||||
|
25.50,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
'Slightly used indoor basketball',
|
||||||
|
5,
|
||||||
|
'2024-10-11 11:20:00'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
6,
|
||||||
|
'Acoustic Guitar',
|
||||||
|
175.00,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
'Beginner acoustic guitar with case',
|
||||||
|
6,
|
||||||
|
'2024-10-09 13:10:00'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
7,
|
||||||
|
'Physics Textbook',
|
||||||
|
65.00,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
'University Physics 14th Edition, good condition',
|
||||||
|
1,
|
||||||
|
'2024-10-08 10:30:00'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
8,
|
||||||
|
'Mini Fridge',
|
||||||
|
85.00,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
'Small dorm fridge, works perfectly',
|
||||||
|
8,
|
||||||
|
'2024-10-13 15:00:00'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
9,
|
||||||
|
'PlayStation 5 Controller',
|
||||||
|
55.00,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
'Extra controller, barely used',
|
||||||
|
9,
|
||||||
|
'2024-10-07 17:20:00'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
10,
|
||||||
|
'Mountain Bike',
|
||||||
|
350.00,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
'Trek mountain bike, great condition, new tires',
|
||||||
|
10,
|
||||||
|
'2024-10-06 14:00:00'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
11,
|
||||||
|
'Wireless Mouse',
|
||||||
|
22.99,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
'Logitech wireless mouse with battery',
|
||||||
|
11,
|
||||||
|
'2024-10-05 09:30:00'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
12,
|
||||||
|
'Chemistry Lab Coat',
|
||||||
|
30.00,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
'Size M, required for chem labs',
|
||||||
|
15,
|
||||||
|
'2024-10-04 13:45:00'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
13,
|
||||||
|
'Graphing Calculator',
|
||||||
|
75.00,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
'TI-84 Plus, perfect working condition',
|
||||||
|
12,
|
||||||
|
'2024-10-03 11:15:00'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
14,
|
||||||
|
'Yoga Mat',
|
||||||
|
20.00,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
'Thick yoga mat, barely used',
|
||||||
|
13,
|
||||||
|
'2024-10-02 16:00:00'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
15,
|
||||||
|
'Winter Jacket',
|
||||||
|
120.00,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
'Columbia winter jacket, size XL, very warm',
|
||||||
|
26,
|
||||||
|
'2024-10-01 10:20:00'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
16,
|
||||||
|
'Computer Science Textbook',
|
||||||
|
70.00,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
'Introduction to Algorithms, like new',
|
||||||
|
1,
|
||||||
|
'2024-09-30 14:30:00'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
17,
|
||||||
|
'Desk Lamp',
|
||||||
|
15.00,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
'LED desk lamp with adjustable brightness',
|
||||||
|
24,
|
||||||
|
'2024-09-29 12:00:00'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
18,
|
||||||
|
'Scientific Calculator',
|
||||||
|
25.00,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
'Casio scientific calculator',
|
||||||
|
12,
|
||||||
|
'2024-09-28 11:30:00'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
19,
|
||||||
|
'Bluetooth Speaker',
|
||||||
|
45.00,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
'JBL Bluetooth speaker, great sound',
|
||||||
|
23,
|
||||||
|
'2024-09-27 15:45:00'
|
||||||
|
),
|
||||||
|
(
|
||||||
|
20,
|
||||||
|
'Backpack',
|
||||||
|
40.00,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
'North Face backpack, lots of pockets',
|
||||||
|
22,
|
||||||
|
'2024-09-26 09:15:00'
|
||||||
|
);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
-- Insert History records
|
||||||
|
INSERT INTO
|
||||||
|
History (HistoryID, UserID, ProductID)
|
||||||
|
VALUES
|
||||||
|
(1, 1, 1),
|
||||||
|
(2, 1, 3),
|
||||||
|
(3, 1, 5),
|
||||||
|
(4, 1, 7),
|
||||||
|
(5, 1, 9),
|
||||||
|
(6, 1, 11),
|
||||||
|
(7, 2, 2),
|
||||||
|
(8, 2, 4),
|
||||||
|
(9, 2, 5),
|
||||||
|
(10, 1, 15),
|
||||||
|
(11, 1, 18);
|
||||||
|
|
||||||
|
-- Insert Favorites
|
||||||
|
INSERT INTO
|
||||||
|
Favorites (UserID, ProductID)
|
||||||
|
VALUES
|
||||||
|
(1, 2), -- User 1 likes HP Laptop
|
||||||
|
(1, 7), -- User 1 likes Physics Textbook
|
||||||
|
(2, 3), -- User 2 likes Dorm Desk
|
||||||
|
(2, 10), -- User 2 likes Mountain Bike
|
||||||
|
(1, 6), -- User 3 likes Acoustic Guitar
|
||||||
|
(1, 5), -- User 4 likes Basketball
|
||||||
|
(2, 8);
|
||||||
|
|
||||||
|
-- User 5 likes Mini Fridge
|
||||||
|
-- Insert Transactions
|
||||||
|
INSERT INTO
|
||||||
|
Transaction (
|
||||||
|
TransactionID,
|
||||||
|
UserID,
|
||||||
|
ProductID,
|
||||||
|
Date,
|
||||||
|
PaymentStatus
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(1, 1, 1, '2024-10-16 10:30:00', 'Completed'),
|
||||||
|
(2, 1, 6, '2024-10-15 15:45:00', 'Completed'),
|
||||||
|
(3, 1, 8, '2024-10-14 12:20:00', 'Pending'),
|
||||||
|
(4, 2, 10, '2024-10-13 17:10:00', 'Completed'),
|
||||||
|
(5, 2, 4, '2024-10-12 14:30:00', 'Completed');
|
||||||
|
|
||||||
|
INSERT INTO
|
||||||
|
Review (UserID, ProductID, Comment, Rating, Date)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
'This is a great fake product! Totally recommend it.',
|
||||||
|
5,
|
||||||
|
);
|
||||||
@@ -30,7 +30,7 @@ CREATE TABLE Category (
|
|||||||
|
|
||||||
-- Product Entity
|
-- Product Entity
|
||||||
CREATE TABLE Product (
|
CREATE TABLE Product (
|
||||||
ProductID INT PRIMARY KEY,
|
ProductID INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
Name VARCHAR(255) NOT NULL,
|
Name VARCHAR(255) NOT NULL,
|
||||||
Price DECIMAL(10, 2) NOT NULL,
|
Price DECIMAL(10, 2) NOT NULL,
|
||||||
StockQuantity INT,
|
StockQuantity INT,
|
||||||
@@ -51,7 +51,7 @@ CREATE TABLE Image_URL (
|
|||||||
|
|
||||||
-- 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)
|
||||||
CREATE TABLE Review (
|
CREATE TABLE Review (
|
||||||
ReviewID INT PRIMARY KEY,
|
ReviewID INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
UserID INT,
|
UserID INT,
|
||||||
ProductID INT,
|
ProductID INT,
|
||||||
Comment TEXT,
|
Comment TEXT,
|
||||||
@@ -77,16 +77,17 @@ CREATE TABLE Transaction (
|
|||||||
|
|
||||||
-- Recommendation Entity (Many-to-One with User, Many-to-One with Product)
|
-- Recommendation Entity (Many-to-One with User, Many-to-One with Product)
|
||||||
CREATE TABLE Recommendation (
|
CREATE TABLE Recommendation (
|
||||||
RecommendationID_PK INT PRIMARY KEY,
|
RecommendationID_PK INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
UserID INT,
|
UserID INT,
|
||||||
RecommendedProductID INT,
|
RecommendedProductID INT,
|
||||||
|
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (UserID) REFERENCES User (UserID),
|
FOREIGN KEY (UserID) REFERENCES User (UserID),
|
||||||
FOREIGN KEY (RecommendedProductID) REFERENCES Product (ProductID)
|
FOREIGN KEY (RecommendedProductID) REFERENCES Product (ProductID)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- History Entity (Many-to-One with User, Many-to-One with Product)
|
-- History Entity (Many-to-One with User, Many-to-One with Product)
|
||||||
CREATE TABLE History (
|
CREATE TABLE History (
|
||||||
HistoryID INT PRIMARY KEY,
|
HistoryID INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
UserID INT,
|
UserID INT,
|
||||||
ProductID INT,
|
ProductID INT,
|
||||||
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
Date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
@@ -100,7 +101,8 @@ CREATE TABLE Favorites (
|
|||||||
UserID INT,
|
UserID INT,
|
||||||
ProductID INT,
|
ProductID INT,
|
||||||
FOREIGN KEY (UserID) REFERENCES User (UserID),
|
FOREIGN KEY (UserID) REFERENCES User (UserID),
|
||||||
FOREIGN KEY (ProductID) REFERENCES Product (ProductID)
|
FOREIGN KEY (ProductID) REFERENCES Product (ProductID),
|
||||||
|
UNIQUE (UserID, ProductID)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Product-Category Junction Table (Many-to-Many)
|
-- Product-Category Junction Table (Many-to-Many)
|
||||||
@@ -120,277 +122,3 @@ CREATE TABLE AuthVerification (
|
|||||||
Authenticated BOOLEAN DEFAULT FALSE,
|
Authenticated BOOLEAN DEFAULT FALSE,
|
||||||
Date DATETIME DEFAULT CURRENT_TIMESTAMP
|
Date DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
-- -- Insert sample categories
|
|
||||||
-- INSERT INTO
|
|
||||||
-- Category (CategoryID, Name)
|
|
||||||
-- VALUES
|
|
||||||
-- (1, 'Electronics'),
|
|
||||||
-- (2, 'Clothing'),
|
|
||||||
-- (3, 'Books'),
|
|
||||||
-- (4, 'Home & Garden'),
|
|
||||||
-- (5, 'Sports & Outdoors');
|
|
||||||
-- -- -- USER CRUD OPERATIONS
|
|
||||||
-- -- -- Create User (INSERT)
|
|
||||||
-- -- INSERT INTO
|
|
||||||
-- -- User (Name, Email, UCID, Password, Phone, Address)
|
|
||||||
-- -- VALUES
|
|
||||||
-- -- (
|
|
||||||
-- -- 'John Doe',
|
|
||||||
-- -- 'john@example.com',
|
|
||||||
-- -- 'UC123456',
|
|
||||||
-- -- 'hashed_password_here',
|
|
||||||
-- -- '555-123-4567',
|
|
||||||
-- -- '123 Main St'
|
|
||||||
-- -- );
|
|
||||||
-- -- -- Set user role
|
|
||||||
-- -- INSERT INTO
|
|
||||||
-- -- UserRole (UserID, Client, Admin)
|
|
||||||
-- -- VALUES
|
|
||||||
-- -- (LAST_INSERT_ID (), TRUE, FALSE);
|
|
||||||
-- -- -- Read User (SELECT)
|
|
||||||
-- -- SELECT
|
|
||||||
-- -- u.*,
|
|
||||||
-- -- ur.Client,
|
|
||||||
-- -- ur.Admin
|
|
||||||
-- -- FROM
|
|
||||||
-- -- User u
|
|
||||||
-- -- JOIN UserRole ur ON u.UserID = ur.UserID
|
|
||||||
-- -- WHERE
|
|
||||||
-- -- u.UserID = 1;
|
|
||||||
-- -- -- Update User (UPDATE)
|
|
||||||
-- -- UPDATE User
|
|
||||||
-- -- SET
|
|
||||||
-- -- Name = 'John Smith',
|
|
||||||
-- -- Phone = '555-987-6543',
|
|
||||||
-- -- Address = '456 Elm St'
|
|
||||||
-- -- WHERE
|
|
||||||
-- -- UserID = 1;
|
|
||||||
-- -- -- Update User Role
|
|
||||||
-- -- UPDATE UserRole
|
|
||||||
-- -- SET
|
|
||||||
-- -- Admin = TRUE
|
|
||||||
-- -- WHERE
|
|
||||||
-- -- UserID = 1;
|
|
||||||
-- -- -- PRODUCT CRUD OPERATIONS
|
|
||||||
-- -- -- Create Product (INSERT)
|
|
||||||
-- -- INSERT INTO
|
|
||||||
-- -- Product (
|
|
||||||
-- -- ProductID,
|
|
||||||
-- -- Name,
|
|
||||||
-- -- Price,
|
|
||||||
-- -- StockQuantity,
|
|
||||||
-- -- UserID,
|
|
||||||
-- -- Description,
|
|
||||||
-- -- CategoryID
|
|
||||||
-- -- )
|
|
||||||
-- -- VALUES
|
|
||||||
-- -- (
|
|
||||||
-- -- 1,
|
|
||||||
-- -- 'Smartphone',
|
|
||||||
-- -- 599.99,
|
|
||||||
-- -- 50,
|
|
||||||
-- -- 1,
|
|
||||||
-- -- 'Latest model smartphone with amazing features',
|
|
||||||
-- -- 1
|
|
||||||
-- -- );
|
|
||||||
-- -- -- Add product images with the placeholder URL
|
|
||||||
-- -- INSERT INTO
|
|
||||||
-- -- Image_URL (URL, ProductID)
|
|
||||||
-- -- VALUES
|
|
||||||
-- -- ('https://picsum.photos/id/237/200/300', 1),
|
|
||||||
-- -- ('https://picsum.photos/id/237/200/300', 1);
|
|
||||||
-- -- -- Create another product for recommendations
|
|
||||||
-- -- INSERT INTO
|
|
||||||
-- -- Product (
|
|
||||||
-- -- ProductID,
|
|
||||||
-- -- Name,
|
|
||||||
-- -- Price,
|
|
||||||
-- -- StockQuantity,
|
|
||||||
-- -- UserID,
|
|
||||||
-- -- Description,
|
|
||||||
-- -- CategoryID
|
|
||||||
-- -- )
|
|
||||||
-- -- VALUES
|
|
||||||
-- -- (
|
|
||||||
-- -- 2,
|
|
||||||
-- -- 'Tablet',
|
|
||||||
-- -- 799.99,
|
|
||||||
-- -- 30,
|
|
||||||
-- -- 1,
|
|
||||||
-- -- 'High-performance tablet',
|
|
||||||
-- -- 1
|
|
||||||
-- -- );
|
|
||||||
-- -- -- Add placeholder images for the second product
|
|
||||||
-- -- INSERT INTO
|
|
||||||
-- -- Image_URL (URL, ProductID)
|
|
||||||
-- -- VALUES
|
|
||||||
-- -- ('https://picsum.photos/id/237/200/300', 2),
|
|
||||||
-- -- ('https://picsum.photos/id/237/200/300', 2);
|
|
||||||
-- -- -- Read Product (SELECT)
|
|
||||||
-- -- SELECT
|
|
||||||
-- -- p.*,
|
|
||||||
-- -- c.Name as CategoryName,
|
|
||||||
-- -- u.Name as SellerName,
|
|
||||||
-- -- i.URL as ImageURL
|
|
||||||
-- -- FROM
|
|
||||||
-- -- Product p
|
|
||||||
-- -- JOIN Category c ON p.CategoryID = c.CategoryID
|
|
||||||
-- -- JOIN User u ON p.UserID = u.UserID
|
|
||||||
-- -- LEFT JOIN Image_URL i ON p.ProductID = i.ProductID
|
|
||||||
-- -- WHERE
|
|
||||||
-- -- p.ProductID = 1;
|
|
||||||
-- -- -- Update Product (UPDATE)
|
|
||||||
-- -- UPDATE Product
|
|
||||||
-- -- SET
|
|
||||||
-- -- Name = 'Premium Smartphone',
|
|
||||||
-- -- Price = 649.99,
|
|
||||||
-- -- StockQuantity = 45,
|
|
||||||
-- -- Description = 'Updated description with new features'
|
|
||||||
-- -- WHERE
|
|
||||||
-- -- ProductID = 1;
|
|
||||||
-- -- -- CATEGORY CRUD OPERATIONS
|
|
||||||
-- -- -- Create Category (INSERT)
|
|
||||||
-- -- INSERT INTO
|
|
||||||
-- -- Category (CategoryID, Name)
|
|
||||||
-- -- VALUES
|
|
||||||
-- -- (6, 'Toys & Games');
|
|
||||||
-- -- -- Read Category (SELECT)
|
|
||||||
-- -- SELECT
|
|
||||||
-- -- *
|
|
||||||
-- -- FROM
|
|
||||||
-- -- Category
|
|
||||||
-- -- WHERE
|
|
||||||
-- -- CategoryID = 6;
|
|
||||||
-- -- -- Update Category (UPDATE)
|
|
||||||
-- -- UPDATE Category
|
|
||||||
-- -- SET
|
|
||||||
-- -- Name = 'Toys & Children''s Games'
|
|
||||||
-- -- WHERE
|
|
||||||
-- -- CategoryID = 6;
|
|
||||||
-- -- -- REVIEW OPERATIONS
|
|
||||||
-- -- INSERT INTO
|
|
||||||
-- -- Review (ReviewID, UserID, ProductID, Comment, Rating)
|
|
||||||
-- -- VALUES
|
|
||||||
-- -- (
|
|
||||||
-- -- 1,
|
|
||||||
-- -- 1,
|
|
||||||
-- -- 1,
|
|
||||||
-- -- 'Great product, very satisfied with the purchase!',
|
|
||||||
-- -- 5
|
|
||||||
-- -- );
|
|
||||||
-- -- -- TRANSACTION OPERATIONS
|
|
||||||
-- -- INSERT INTO
|
|
||||||
-- -- Transaction (TransactionID, UserID, ProductID, PaymentStatus)
|
|
||||||
-- -- VALUES
|
|
||||||
-- -- (1, 1, 1, 'Completed');
|
|
||||||
-- -- -- HISTORY OPERATIONS
|
|
||||||
-- -- INSERT INTO
|
|
||||||
-- -- History (HistoryID, UserID, ProductID)
|
|
||||||
-- -- VALUES
|
|
||||||
-- -- (1, 1, 1);
|
|
||||||
-- -- -- Read History (SELECT)
|
|
||||||
-- -- SELECT
|
|
||||||
-- -- h.*,
|
|
||||||
-- -- p.Name as ProductName
|
|
||||||
-- -- FROM
|
|
||||||
-- -- History h
|
|
||||||
-- -- JOIN Product p ON h.ProductID = p.ProductID
|
|
||||||
-- -- WHERE
|
|
||||||
-- -- h.UserID = 1
|
|
||||||
-- -- ORDER BY
|
|
||||||
-- -- h.Date DESC;
|
|
||||||
-- -- -- FAVORITES OPERATIONS
|
|
||||||
-- -- INSERT INTO
|
|
||||||
-- -- Favorites (UserID, ProductID)
|
|
||||||
-- -- VALUES
|
|
||||||
-- -- (1, 1);
|
|
||||||
-- -- -- Read Favorites (SELECT)
|
|
||||||
-- -- SELECT
|
|
||||||
-- -- f.*,
|
|
||||||
-- -- p.Name as ProductName,
|
|
||||||
-- -- p.Price
|
|
||||||
-- -- FROM
|
|
||||||
-- -- Favorites f
|
|
||||||
-- -- JOIN Product p ON f.ProductID = p.ProductID
|
|
||||||
-- -- WHERE
|
|
||||||
-- -- f.UserID = 1;
|
|
||||||
-- -- -- RECOMMENDATION OPERATIONS
|
|
||||||
-- -- INSERT INTO
|
|
||||||
-- -- Recommendation (RecommendationID_PK, UserID, RecommendedProductID)
|
|
||||||
-- -- VALUES
|
|
||||||
-- -- (1, 1, 2);
|
|
||||||
-- -- -- Read Recommendations (SELECT)
|
|
||||||
-- -- SELECT
|
|
||||||
-- -- r.*,
|
|
||||||
-- -- p.Name as RecommendedProductName,
|
|
||||||
-- -- p.Price,
|
|
||||||
-- -- p.Description
|
|
||||||
-- -- FROM
|
|
||||||
-- -- Recommendation r
|
|
||||||
-- -- JOIN Product p ON r.RecommendedProductID = p.ProductID
|
|
||||||
-- -- WHERE
|
|
||||||
-- -- r.UserID = 1;
|
|
||||||
-- -- -- Authentication Operations
|
|
||||||
-- -- -- Create verification code
|
|
||||||
-- -- INSERT INTO
|
|
||||||
-- -- AuthVerification (Email, VerificationCode)
|
|
||||||
-- -- VALUES
|
|
||||||
-- -- ('new_user@example.com', '123456');
|
|
||||||
-- -- -- Update authentication status
|
|
||||||
-- -- UPDATE AuthVerification
|
|
||||||
-- -- SET
|
|
||||||
-- -- Authenticated = TRUE
|
|
||||||
-- -- WHERE
|
|
||||||
-- -- Email = 'new_user@example.com'
|
|
||||||
-- -- AND VerificationCode = '123456';
|
|
||||||
-- -- -- Get top-selling products
|
|
||||||
-- -- SELECT
|
|
||||||
-- -- p.ProductID,
|
|
||||||
-- -- p.Name,
|
|
||||||
-- -- COUNT(t.TransactionID) as SalesCount,
|
|
||||||
-- -- SUM(p.Price) as TotalRevenue
|
|
||||||
-- -- FROM
|
|
||||||
-- -- Product p
|
|
||||||
-- -- JOIN Transaction t ON p.ProductID = t.ProductID
|
|
||||||
-- -- WHERE
|
|
||||||
-- -- t.PaymentStatus = 'Completed'
|
|
||||||
-- -- GROUP BY
|
|
||||||
-- -- p.ProductID,
|
|
||||||
-- -- p.Name
|
|
||||||
-- -- ORDER BY
|
|
||||||
-- -- SalesCount DESC
|
|
||||||
-- -- LIMIT
|
|
||||||
-- -- 10;
|
|
||||||
-- -- -- Get highest-rated products
|
|
||||||
-- -- SELECT
|
|
||||||
-- -- p.ProductID,
|
|
||||||
-- -- p.Name,
|
|
||||||
-- -- AVG(r.Rating) as AverageRating,
|
|
||||||
-- -- COUNT(r.ReviewID) as ReviewCount
|
|
||||||
-- -- FROM
|
|
||||||
-- -- Product p
|
|
||||||
-- -- JOIN Review r ON p.ProductID = r.ProductID
|
|
||||||
-- -- GROUP BY
|
|
||||||
-- -- p.ProductID,
|
|
||||||
-- -- p.Name
|
|
||||||
-- -- HAVING
|
|
||||||
-- -- ReviewCount >= 5
|
|
||||||
-- -- ORDER BY
|
|
||||||
-- -- AverageRating DESC
|
|
||||||
-- -- LIMIT
|
|
||||||
-- -- 10;
|
|
||||||
-- -- -- Get user purchase history with product details
|
|
||||||
-- -- SELECT
|
|
||||||
-- -- t.TransactionID,
|
|
||||||
-- -- t.Date,
|
|
||||||
-- -- p.Name,
|
|
||||||
-- -- p.Price,
|
|
||||||
-- -- t.PaymentStatus
|
|
||||||
-- -- FROM
|
|
||||||
-- -- Transaction t
|
|
||||||
-- -- JOIN Product p ON t.ProductID = p.ProductID
|
|
||||||
-- -- WHERE
|
|
||||||
-- -- t.UserID = 1
|
|
||||||
-- -- ORDER BY
|
|
||||||
-- -- t.Date DESC;
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
import subprocess
|
|
||||||
if (subprocess.run("mysql -u root mysql < SQL_code/Schema.sql", shell=True, check=True)):
|
|
||||||
print("successfully created the Marketplace databse")
|
|
||||||
BIN
recommondation-engine/__pycache__/app.cpython-313.pyc
Normal file
160
recommondation-engine/app.py
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
# pip install mysql.connector
|
||||||
|
import mysql.connector
|
||||||
|
from sklearn.metrics.pairwise import cosine_similarity
|
||||||
|
import numpy as np
|
||||||
|
import logging
|
||||||
|
from unittest import result
|
||||||
|
|
||||||
|
def database():
|
||||||
|
db_connection = mysql.connector.connect(
|
||||||
|
host = "localhost",
|
||||||
|
port = "3306",
|
||||||
|
user = "root",
|
||||||
|
database = "Marketplace"
|
||||||
|
)
|
||||||
|
return db_connection
|
||||||
|
|
||||||
|
def get_popular_products():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def delete_user_recommendation(userID, Array):
|
||||||
|
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});")
|
||||||
|
|
||||||
|
db_con.commit()
|
||||||
|
|
||||||
|
#results = cursor.fetchall()
|
||||||
|
#print(results)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_all_products():
|
||||||
|
|
||||||
|
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]
|
||||||
|
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
|
||||||
|
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_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()
|
||||||
|
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.
|
||||||
|
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
|
||||||
|
|
||||||
|
# Upload the recommendations to the database
|
||||||
|
history_upload(user_id, product_indices) # Pass the indices directly to history_upload
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
def history_upload(userID, anrr):
|
||||||
|
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});")
|
||||||
|
|
||||||
|
# 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}")
|
||||||
|
db_con.rollback()
|
||||||
|
finally:
|
||||||
|
# Close the cursor and connection
|
||||||
|
cursor.close()
|
||||||
|
db_con.close()
|
||||||
30
recommondation-engine/server.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
from flask import Flask, request, jsonify
|
||||||
|
from flask_cors import CORS
|
||||||
|
from app import get_recommendations
|
||||||
|
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
CORS(app) # Enable CORS for all routes
|
||||||
|
|
||||||
|
@app.route('/api/user/session', methods=['POST'])
|
||||||
|
def handle_session_data():
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
user_id = data.get('userId')
|
||||||
|
email = data.get('email')
|
||||||
|
is_authenticated = data.get('isAuthenticated')
|
||||||
|
|
||||||
|
if not user_id or not email or is_authenticated is None:
|
||||||
|
return jsonify({'error': 'Invalid data'}), 400
|
||||||
|
|
||||||
|
get_recommendations(user_id)
|
||||||
|
|
||||||
|
print(f"Received session data: User ID: {user_id}, Email: {email}, Authenticated: {is_authenticated}")
|
||||||
|
return jsonify({'message': 'Session data received successfully'})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
return jsonify({'error': 'Server error'}), 500
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import websockets
|
|
||||||
|
|
||||||
async def handle_client(websocket, path):
|
|
||||||
try:
|
|
||||||
# Receive user ID
|
|
||||||
user_id = await websocket.recv()
|
|
||||||
print(f"Received user ID: {user_id}")
|
|
||||||
|
|
||||||
# Optional: You can add more logic here if needed
|
|
||||||
|
|
||||||
except websockets.exceptions.ConnectionClosed:
|
|
||||||
print("Client disconnected")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error processing request: {str(e)}")
|
|
||||||
|
|
||||||
async def start_server():
|
|
||||||
server = await websockets.serve(handle_client, "localhost", 5555)
|
|
||||||
print("WebSocket server started")
|
|
||||||
await server.wait_closed()
|
|
||||||
|
|
||||||
# Run the server
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(start_server())
|
|
||||||