diff --git a/README.md b/README.md index 8659323..55e9604 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,34 @@ ### Some ground rules -1. Add both node_modules from Slient and Server to your `gitignore` file -2. Make a brach with the following naming conventionp, refix it with your name `name-some branch name`. +1. Add both node_modules from Slient and Server to your ```gitignore``` file +2. Make a brach with the following naming conventionp, refix it with your name ```Your-Name Branch-Name```. +--- -### `frontend` -- Use React Js and vite as the node manger +### Frontend +1. `cd frontend` into the dir and then type command ```Bash + #Install the needed lib with the command bellow npm install -``` -2. **Start The Server**, `cd frontend` into the dir and then type command -```Bash + #Start The Server npm run dev ``` -### `backend` -1. Install the needed lib with the command bellow +--- + +### Backend +1. `cd backend` into the dir and then type command ```Bash + #Install the needed lib with the command bellow npm install -``` -2. **Start The Server**, `cd backend` into the dir and then type command -```Bash + #Start The Server npm run dev ``` +--- ### 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 - python3 ./mysql-code/init-db.py + 1. mysql -u root + 2. use Marketplace; + 3. \. PathToYour/Schema.sql + 3. \. PathToYour/Init-Data.sql ``` -- MySql Version 9.2.0 diff --git a/backend/controllers/history.js b/backend/controllers/history.js new file mode 100644 index 0000000..1c85042 --- /dev/null +++ b/backend/controllers/history.js @@ -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" }); + } +}; diff --git a/backend/controllers/product.js b/backend/controllers/product.js index 71396b8..c5a790c 100644 --- a/backend/controllers/product.js +++ b/backend/controllers/product.js @@ -1,13 +1,13 @@ const db = require("../utils/database"); -exports.addToFavorite = async (req, res) => { - const { userID, productsID } = req.body; - +exports.addFavorite = async (req, res) => { + const { userID, productID } = req.body; + console.log(userID); try { // Use parameterized query to prevent SQL injection const [result] = await db.execute( - "INSERT INTO Favorites (UserID, ProductID) VALUES (?, ?)", - [userID, productsID], + `INSERT INTO Favorites (UserID, ProductID) VALUES (?, ?)`, + [userID, productID], ); res.json({ @@ -20,25 +20,86 @@ 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.*, + u.Name AS SellerName, + 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 = ? + `, + [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 exports.getAllProducts = async (req, res) => { 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 + ) SELECT - P.ProductID, - P.Name AS ProductName, - P.Price, - P.Date AS DateUploaded, - U.Name AS SellerName, - I.URL AS ProductImage, - C.Name AS Category - FROM Product P - JOIN Image_URL I ON p.ProductID = i.ProductID - JOIN User U ON P.UserID = U.UserID - JOIN Category C ON P.CategoryID = C.CategoryID; + ProductID, + ProductName, + Price, + DateUploaded, + SellerName, + ProductImage, + Category + FROM RankedImages + WHERE RowNum = 1; `); - console.log(data); res.json({ success: true, message: "Products fetched successfully", @@ -60,9 +121,10 @@ exports.getProductById = async (req, res) => { try { 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 LEFT JOIN Image_URL i ON p.ProductID = i.ProductID + JOIN User U ON p.UserID = U.UserID WHERE p.ProductID = ? `, [id], diff --git a/backend/controllers/recommendation.js b/backend/controllers/recommendation.js new file mode 100644 index 0000000..63c9516 --- /dev/null +++ b/backend/controllers/recommendation.js @@ -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", + }); + } +}; diff --git a/backend/controllers/review.js b/backend/controllers/review.js new file mode 100644 index 0000000..e792409 --- /dev/null +++ b/backend/controllers/review.js @@ -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, +// }); +// } +// }; diff --git a/backend/index.js b/backend/index.js index c78cda7..0d0dbcb 100644 --- a/backend/index.js +++ b/backend/index.js @@ -6,6 +6,10 @@ const db = require("./utils/database"); const userRouter = require("./routes/user"); 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 { cleanupExpiredCodes, @@ -35,7 +39,10 @@ checkDatabaseConnection(db); //Routes app.use("/api/user", userRouter); //prefix with /api/user app.use("/api/product", productRouter); //prefix with /api/product -app.use("/api/search_products", searchRouter); //prefix with /api/product +app.use("/api/search", searchRouter); //prefix with /api/product +app.use("/api/engine", recommendedRouter); //prefix with /api/ +app.use("/api/history", history); //prefix with /api/ +app.use("/api/review", review); //prefix with /api/ // Set up a scheduler to run cleanup every hour setInterval(cleanupExpiredCodes, 60 * 60 * 1000); diff --git a/backend/routes/history.js b/backend/routes/history.js new file mode 100644 index 0000000..6713233 --- /dev/null +++ b/backend/routes/history.js @@ -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; diff --git a/backend/routes/product.js b/backend/routes/product.js index 90c7914..24c5705 100644 --- a/backend/routes/product.js +++ b/backend/routes/product.js @@ -1,7 +1,9 @@ // routes/product.js const express = require("express"); const { - addToFavorite, + addFavorite, + getFavorites, + removeFavorite, getAllProducts, getProductById, } = require("../controllers/product"); @@ -13,8 +15,11 @@ router.use((req, res, next) => { next(); }); -router.post("/add_fav_product", addToFavorite); -router.get("/get_product", getAllProducts); +router.post("/addFavorite", addFavorite); +router.post("/getFavorites", getFavorites); +router.post("/delFavorite", removeFavorite); + +router.get("/getProduct", getAllProducts); router.get("/:id", getProductById); // Simplified route module.exports = router; diff --git a/backend/routes/recommendation.js b/backend/routes/recommendation.js new file mode 100644 index 0000000..e252a34 --- /dev/null +++ b/backend/routes/recommendation.js @@ -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; diff --git a/backend/routes/review.js b/backend/routes/review.js new file mode 100644 index 0000000..5b26a87 --- /dev/null +++ b/backend/routes/review.js @@ -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("/add", submitReview); + +module.exports = router; diff --git a/backend/routes/search.js b/backend/routes/search.js index 2871eba..d59f785 100644 --- a/backend/routes/search.js +++ b/backend/routes/search.js @@ -9,6 +9,6 @@ router.use((req, res, next) => { next(); }); -router.get("/search", searchProductsByName); +router.get("/getProduct", searchProductsByName); module.exports = router; diff --git a/frontend/public/Pictures/Acoustic-Guitar.jpg b/frontend/public/Pictures/Acoustic-Guitar.jpg new file mode 100644 index 0000000..143f359 Binary files /dev/null and b/frontend/public/Pictures/Acoustic-Guitar.jpg differ diff --git a/frontend/public/Pictures/Backpack.jpg b/frontend/public/Pictures/Backpack.jpg new file mode 100644 index 0000000..1abff9d Binary files /dev/null and b/frontend/public/Pictures/Backpack.jpg differ diff --git a/frontend/public/Pictures/Basketball.jpg b/frontend/public/Pictures/Basketball.jpg new file mode 100644 index 0000000..0d41950 Binary files /dev/null and b/frontend/public/Pictures/Basketball.jpg differ diff --git a/frontend/public/Pictures/Bluetooth-Speaker.jpg b/frontend/public/Pictures/Bluetooth-Speaker.jpg new file mode 100644 index 0000000..e85097e Binary files /dev/null and b/frontend/public/Pictures/Bluetooth-Speaker.jpg differ diff --git a/frontend/public/Pictures/CS-Textbook.jpg b/frontend/public/Pictures/CS-Textbook.jpg new file mode 100644 index 0000000..4603981 Binary files /dev/null and b/frontend/public/Pictures/CS-Textbook.jpg differ diff --git a/frontend/public/Pictures/Calculator.jpg b/frontend/public/Pictures/Calculator.jpg new file mode 100644 index 0000000..710dbd9 Binary files /dev/null and b/frontend/public/Pictures/Calculator.jpg differ diff --git a/frontend/public/Pictures/Calculus-Textbook.jpg b/frontend/public/Pictures/Calculus-Textbook.jpg new file mode 100644 index 0000000..b4849d4 Binary files /dev/null and b/frontend/public/Pictures/Calculus-Textbook.jpg differ diff --git a/frontend/public/Pictures/Calculus-Textbook2.jpg b/frontend/public/Pictures/Calculus-Textbook2.jpg new file mode 100644 index 0000000..439c512 Binary files /dev/null and b/frontend/public/Pictures/Calculus-Textbook2.jpg differ diff --git a/frontend/public/Pictures/Calculus-Textbook3.jpg b/frontend/public/Pictures/Calculus-Textbook3.jpg new file mode 100644 index 0000000..557c095 Binary files /dev/null and b/frontend/public/Pictures/Calculus-Textbook3.jpg differ diff --git a/frontend/public/Pictures/Controller.jpg b/frontend/public/Pictures/Controller.jpg new file mode 100644 index 0000000..9056402 Binary files /dev/null and b/frontend/public/Pictures/Controller.jpg differ diff --git a/frontend/public/image1.avif b/frontend/public/Pictures/Dell1.jpg similarity index 100% rename from frontend/public/image1.avif rename to frontend/public/Pictures/Dell1.jpg diff --git a/frontend/public/image2.avif b/frontend/public/Pictures/Dell2.jpg similarity index 100% rename from frontend/public/image2.avif rename to frontend/public/Pictures/Dell2.jpg diff --git a/frontend/public/image3.avif b/frontend/public/Pictures/Dell3.jpg similarity index 100% rename from frontend/public/image3.avif rename to frontend/public/Pictures/Dell3.jpg diff --git a/frontend/public/Pictures/Desk-Lamp.jpg b/frontend/public/Pictures/Desk-Lamp.jpg new file mode 100644 index 0000000..80bcbee Binary files /dev/null and b/frontend/public/Pictures/Desk-Lamp.jpg differ diff --git a/frontend/public/Pictures/Dorm-Desk.jpg b/frontend/public/Pictures/Dorm-Desk.jpg new file mode 100644 index 0000000..8838fd8 Binary files /dev/null and b/frontend/public/Pictures/Dorm-Desk.jpg differ diff --git a/frontend/public/Pictures/HP-Calculator.jpg b/frontend/public/Pictures/HP-Calculator.jpg new file mode 100644 index 0000000..8f1f94a Binary files /dev/null and b/frontend/public/Pictures/HP-Calculator.jpg differ diff --git a/frontend/public/Pictures/HP-Laptop1.jpg b/frontend/public/Pictures/HP-Laptop1.jpg new file mode 100644 index 0000000..ab8457d Binary files /dev/null and b/frontend/public/Pictures/HP-Laptop1.jpg differ diff --git a/frontend/public/Pictures/HP-Laptop2.jpg b/frontend/public/Pictures/HP-Laptop2.jpg new file mode 100644 index 0000000..b65d2ce Binary files /dev/null and b/frontend/public/Pictures/HP-Laptop2.jpg differ diff --git a/frontend/public/Pictures/Lab-Coat.jpg b/frontend/public/Pictures/Lab-Coat.jpg new file mode 100644 index 0000000..5a6729b Binary files /dev/null and b/frontend/public/Pictures/Lab-Coat.jpg differ diff --git a/frontend/public/Pictures/Mini-Fridge.jpg b/frontend/public/Pictures/Mini-Fridge.jpg new file mode 100644 index 0000000..ad6eedf Binary files /dev/null and b/frontend/public/Pictures/Mini-Fridge.jpg differ diff --git a/frontend/public/Pictures/Mountain-Bike.jpg b/frontend/public/Pictures/Mountain-Bike.jpg new file mode 100644 index 0000000..1664e79 Binary files /dev/null and b/frontend/public/Pictures/Mountain-Bike.jpg differ diff --git a/frontend/public/Pictures/Physics-Textbook.jpg b/frontend/public/Pictures/Physics-Textbook.jpg new file mode 100644 index 0000000..bff3ab2 Binary files /dev/null and b/frontend/public/Pictures/Physics-Textbook.jpg differ diff --git a/frontend/public/Pictures/University-Hoodie.jpg b/frontend/public/Pictures/University-Hoodie.jpg new file mode 100644 index 0000000..f00b434 Binary files /dev/null and b/frontend/public/Pictures/University-Hoodie.jpg differ diff --git a/frontend/public/Pictures/Winter-Jacket.jpg b/frontend/public/Pictures/Winter-Jacket.jpg new file mode 100644 index 0000000..0496261 Binary files /dev/null and b/frontend/public/Pictures/Winter-Jacket.jpg differ diff --git a/frontend/public/Pictures/Wireless-Mouse.jpg b/frontend/public/Pictures/Wireless-Mouse.jpg new file mode 100644 index 0000000..b1d4490 Binary files /dev/null and b/frontend/public/Pictures/Wireless-Mouse.jpg differ diff --git a/frontend/public/Pictures/Yoga-Mat.jpg b/frontend/public/Pictures/Yoga-Mat.jpg new file mode 100644 index 0000000..83d81a4 Binary files /dev/null and b/frontend/public/Pictures/Yoga-Mat.jpg differ diff --git a/frontend/public/icon/apple-touch-icon.png b/frontend/public/icon/apple-touch-icon.png deleted file mode 100644 index b69ea23..0000000 Binary files a/frontend/public/icon/apple-touch-icon.png and /dev/null differ diff --git a/frontend/public/icon/favicon.ico b/frontend/public/icon/favicon.ico index 3c826fc..84fef13 100644 Binary files a/frontend/public/icon/favicon.ico and b/frontend/public/icon/favicon.ico differ diff --git a/frontend/public/icon/icon-192-maskable.png b/frontend/public/icon/icon-192-maskable.png deleted file mode 100644 index 5913259..0000000 Binary files a/frontend/public/icon/icon-192-maskable.png and /dev/null differ diff --git a/frontend/public/icon/icon-192.png b/frontend/public/icon/icon-192.png deleted file mode 100644 index 97f48c7..0000000 Binary files a/frontend/public/icon/icon-192.png and /dev/null differ diff --git a/frontend/public/icon/icon-512-maskable.png b/frontend/public/icon/icon-512-maskable.png deleted file mode 100644 index 4066f48..0000000 Binary files a/frontend/public/icon/icon-512-maskable.png and /dev/null differ diff --git a/frontend/public/icon/icon-512.png b/frontend/public/icon/icon-512.png index c4bd39c..77f6209 100644 Binary files a/frontend/public/icon/icon-512.png and b/frontend/public/icon/icon-512.png differ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 12b3231..a3ec64f 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -12,9 +12,7 @@ import Selling from "./pages/Selling"; import Transactions from "./pages/Transactions"; import Favorites from "./pages/Favorites"; import ProductDetail from "./pages/ProductDetail"; -import ItemForm from "./pages/MyListings"; import SearchPage from "./pages/SearchPage"; // Make sure to import the SearchPage -import axios from "axios"; function App() { // Authentication state - initialize from localStorage if available @@ -298,10 +296,7 @@ function App() { // Save to localStorage to persist across refreshes sessionStorage.setItem("isAuthenticated", "true"); sessionStorage.setItem("user", JSON.stringify(userObj)); - - // After successful signup, send session data to server sendSessionDataToServer(); // Call it after signup - sessionStorage.getItem("user"); console.log("Login successful for:", userData.email); @@ -388,7 +383,7 @@ function App() { 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://localhost:5000/api/user/session", { + const response = await fetch("http://0.0.0.0:5000/api/user/session", { method: "POST", headers: { "Content-Type": "application/json", @@ -716,27 +711,6 @@ function App() { } /> - {/* Add new selling routes */} - -
- -
- - } - /> - -
- -
- - } - /> { + useEffect(() => { + const timer = setTimeout(() => { + onClose(); + }, duration); + + return () => clearTimeout(timer); + }, [onClose, duration]); + + return ( +
+ {message} +
+ ); +}; +export default FloatingAlert; diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index fc6465c..827399e 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -35,7 +35,7 @@ const Navbar = ({ onLogout, userName }) => { alt="Campus Plug" className="h-8 px-2" /> - + Campus Plug diff --git a/frontend/src/components/ProductForm.jsx b/frontend/src/components/ProductForm.jsx new file mode 100644 index 0000000..f594d24 --- /dev/null +++ b/frontend/src/components/ProductForm.jsx @@ -0,0 +1,300 @@ +import React, { useState } from "react"; + +const ProductForm = ({ + editingProduct, + setEditingProduct, + onSave, + onCancel, +}) => { + const [selectedCategory, setSelectedCategory] = useState(""); + + 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", + ]; + + 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, + ), + })); + }; + + return ( +
+ {/* Back Button */} + + +

+ {editingProduct?.id ? "Edit Your Product" : "List a New Product"} +

+ +
+ {/* Product Name */} +
+ + + setEditingProduct({ ...editingProduct, name: e.target.value }) + } + className="w-full px-4 py-2 border-2 border-gray-200 rounded-md focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500" + /> +
+ + {/* Price */} +
+ + + setEditingProduct({ + ...editingProduct, + price: e.target.value, + }) + } + className="w-full px-4 py-2 border-2 border-gray-200 rounded-md focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500" + /> +
+ + {/* Categories - Dropdown with Add button */} +
+ +
+ + +
+ + {/* Selected Categories */} + {(editingProduct.categories || []).length > 0 ? ( +
+ {(editingProduct.categories || []).map((category) => ( + + {category} + + + ))} +
+ ) : ( +

+ Please select at least one category +

+ )} +
+ + {/* Status - Updated to Unsold/Sold */} +
+ + +
+ + {/* Description - New Field */} +
+ + +
+ + {/* Simplified Image Upload */} +
+ + + {/* Simple file input */} + { + 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" + /> + + + {/* Image previews */} + {editingProduct.images.length > 0 && ( +
+

+ {editingProduct.images.length}{" "} + {editingProduct.images.length === 1 ? "image" : "images"}{" "} + selected +

+
+ {editingProduct.images.map((img, idx) => ( +
+ {`Product + +
+ ))} + {editingProduct.images.length > 0 && ( + + )} +
+
+ )} +
+
+ + {/* Actions */} +
+ + +
+
+ ); +}; + +export default ProductForm; diff --git a/frontend/src/pages/Favorites.jsx b/frontend/src/pages/Favorites.jsx index 86e9e1f..55b191d 100644 --- a/frontend/src/pages/Favorites.jsx +++ b/frontend/src/pages/Favorites.jsx @@ -1,112 +1,141 @@ -import { useState } from 'react'; -import { Link } from 'react-router-dom'; -import { Heart, Tag, Trash2, Filter, ChevronDown } from 'lucide-react'; +import { useState, useEffect } from "react"; +import { Link } from "react-router-dom"; +import { Heart, Tag, Trash2 } from "lucide-react"; const Favorites = () => { - const [favorites, setFavorites] = useState([ - { - id: 0, - 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', - }, - - ]); - + const [favorites, setFavorites] = useState([]); const [showFilters, setShowFilters] = useState(false); - const [sortBy, setSortBy] = useState('dateAdded'); - const [filterCategory, setFilterCategory] = useState('All'); + const [sortBy, setSortBy] = useState("dateAdded"); + const [filterCategory, setFilterCategory] = useState("All"); + const storedUser = JSON.parse(sessionStorage.getItem("user")); - // Function to remove item from favorites - const removeFromFavorites = (id) => { - setFavorites(favorites.filter(item => item.id !== id)); + const mapCategory = (id) => { + const categories = { + 1: "Electronics", + 2: "Textbooks", + 3: "Furniture", + 4: "Clothing", + 5: "Kitchen", + 6: "Other", + }; + return categories[id] || "Other"; }; - // Available categories for filtering - const categories = ['All', 'Electronics', 'Textbooks', 'Furniture', 'Kitchen', 'Other']; - - // Sort favorites based on selected sort option - const sortedFavorites = [...favorites].sort((a, b) => { - if (sortBy === 'dateAdded') { - 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; + function reloadPage() { + var doctTimestamp = new Date(performance.timing.domLoading).getTime(); + var now = Date.now(); + if (now > doctTimestamp) { + location.reload(); } + } + + const removeFromFavorites = async (itemID) => { + 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, + }), + }, + ); + const data = await response.json(); + if (data.success) { + reloadPage(); + } + + if (!response.ok) throw new Error("Failed to fetch products"); + console.log(response); + console.log(`Add Product -> History: ${itemID}`); + }; + + 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, + title: item.Name, + price: parseFloat(item.Price), + category: mapCategory(item.CategoryID), + image: item.image_url || "/default-image.jpg", + 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; }); - // Filter favorites based on selected category - const filteredFavorites = filterCategory === 'All' - ? sortedFavorites - : sortedFavorites.filter(item => item.category === filterCategory); + const filteredFavorites = + filterCategory === "All" + ? sortedFavorites + : sortedFavorites.filter((item) => item.category === filterCategory); return (

My Favorites

-
- {/* Filters and Sorting */} - {showFilters && ( -
-
-
- - -
-
- - -
-
-
- )} - {/* Favorites List */} {filteredFavorites.length === 0 ? (
-

No favorites yet

+

+ No favorites yet +

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

- Browse Listings @@ -115,7 +144,10 @@ const Favorites = () => { ) : (
{filteredFavorites.map((item) => ( -
+
- + - {item.title} - + {item.title} +

{item.title}

- ${item.price} + + ${item.price} +
- +
{item.category} - - {item.condition}
- +
- Listed {item.datePosted} - {item.seller} + + Listed {item.datePosted} + + + {item.seller} +
@@ -156,12 +196,13 @@ const Favorites = () => { {/* Show count if there are favorites */} {filteredFavorites.length > 0 && (
- Showing {filteredFavorites.length} {filteredFavorites.length === 1 ? 'item' : 'items'} - {filterCategory !== 'All' && ` in ${filterCategory}`} + Showing {filteredFavorites.length}{" "} + {filteredFavorites.length === 1 ? "item" : "items"} + {filterCategory !== "All" && ` in ${filterCategory}`}
)}
); }; -export default Favorites; \ No newline at end of file +export default Favorites; diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index da47d13..315bf3b 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -1,17 +1,118 @@ import { useState, useEffect } from "react"; import { Link, useNavigate } from "react-router-dom"; -import { Tag, Book, Laptop, Sofa, Utensils, Gift, Heart } from "lucide-react"; +import { Tag } from "lucide-react"; + +import FloatingAlert from "../components/FloatingAlert"; // adjust path if needed const Home = () => { const navigate = useNavigate(); const [listings, setListings] = useState([]); + const [recommended, setRecommended] = useState([]); + const [history, sethistory] = useState([]); const [error, setError] = useState(null); + const storedUser = JSON.parse(sessionStorage.getItem("user")); + const [showAlert, setShowAlert] = useState(false); + + 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(response); + 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 () => { + // Get the user's data from localStorage + console.log(storedUser); + 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(); + console.log(data); + 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 + })), + ); + } else { + throw new Error(data.message || "Error fetching products"); + } + } catch (error) { + console.error("Error fetching products:", error); + setError(error.message); + } + }; + fetchrecomProducts(); + //reloadPage(); + }, []); + reloadPage(); useEffect(() => { const fetchProducts = async () => { try { 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"); @@ -25,7 +126,6 @@ const Home = () => { price: product.Price, category: product.Category, // Ensure this gets the category name image: product.ProductImage, // Use the alias for image URL - condition: "New", // Modify based on actual data seller: product.SellerName, // Fetch seller name properly datePosted: product.DateUploaded, // Use the actual date isFavorite: false, // Default state @@ -42,17 +142,49 @@ const Home = () => { fetchProducts(); }, []); - // Toggle favorite status - const toggleFavorite = (id, e) => { - e.preventDefault(); // Prevent navigation when clicking the heart icon - setListings((prevListings) => - prevListings.map((listing) => - listing.id === id - ? { ...listing, isFavorite: !listing.isFavorite } - : listing, - ), - ); - }; + useEffect(() => { + const fetchrecomProducts = async () => { + // Get the user's data from localStorage + console.log(storedUser); + try { + const response = await fetch( + "http://localhost:3030/api/history/getHistory", + { + 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(); + console.log(data); + 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 = () => { navigate("/selling"); @@ -90,27 +222,13 @@ const Home = () => {
- {/* Categories */} - {/*
-

Categories

-
- {categories.map((category) => ( - - ))} -
-
*/} - {/* Recent Listings */} + {showAlert && ( + setShowAlert(false)} + /> + )}

Recommendation @@ -134,53 +252,50 @@ const Home = () => { id="RecomContainer" className="overflow-x-auto whitespace-nowrap flex space-x-6 scroll-smooth scrollbar-hide px-10 pl-0" > - {listings.map((listing) => ( + {recommended.map((recommended) => ( addHistory(recommended.id)} className="bg-white border border-gray-200 hover:shadow-md transition-shadow w-70 flex-shrink-0 relative" >
{listing.title}

- {listing.title} + {recommended.title}

- ${listing.price} + ${recommended.price}
- {listing.category} - - {listing.condition} + {recommended.category}
- {listing.datePosted} + {recommended.datePosted} - {listing.seller} + {recommended.seller}
@@ -203,6 +318,12 @@ const Home = () => {

{/* Recent Listings */} + {showAlert && ( + setShowAlert(false)} + /> + )}

Recent Listings @@ -236,19 +357,18 @@ const Home = () => { {listing.title} addHistory(listing.id)} className="w-full h-48 object-cover" />

@@ -263,8 +383,6 @@ const Home = () => {
{listing.category} - - {listing.condition}
@@ -293,6 +411,98 @@ const Home = () => {
+ + {/* Recent Listings */} + {showAlert && ( + setShowAlert(false)} + /> + )} +
+

History

+ +
+ {/* Left Button - Overlaid on products */} + + + {/* Scrollable Listings Container */} +
+ {history.map((history) => ( + +
+ {history.title} + +
+ +
+

+ {history.title} +

+ + ${history.price} + + +
+ + {history.category} +
+ +
+ + {history.datePosted} + + + {history.seller} + +
+
+ + ))} +
+ + {/* Right Button - Overlaid on products */} + +
+
); }; diff --git a/frontend/src/pages/MyListings.jsx b/frontend/src/pages/MyListings.jsx deleted file mode 100644 index 6cafcc7..0000000 --- a/frontend/src/pages/MyListings.jsx +++ /dev/null @@ -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 ( -
-
- - - Back to listings - -
-
-
-
-
-
-
- ); - } - - return ( -
- {/* Breadcrumb & Back Link */} -
- - - Back to listings - -
- -
-

- {isEditing ? "Edit Item" : "Create New Listing"} -

- - {isEditing && ( - - )} -
- -
- {/* Title */} -
- - - {errors.title && ( -

{errors.title}

- )} -
- - {/* Price, Category, Status (side by side on larger screens) */} -
-
- - - {errors.price && ( -

{errors.price}

- )} -
- -
- - - {errors.category && ( -

{errors.category}

- )} -
- - {isEditing && ( -
- - -
- )} -
- - {/* Condition */} -
- -
- {conditions.map((condition) => ( - - ))} -
- {errors.condition && ( -

{errors.condition}

- )} -
- - {/* Short Description */} -
- - -

- {formData.shortDescription.length}/150 characters -

- {errors.shortDescription && ( -

{errors.shortDescription}

- )} -
- - {/* Full Description */} -
- - -

- Use blank lines to separate paragraphs. -

- {errors.description && ( -

{errors.description}

- )} -
- - {/* Image Upload */} -
- - - {/* Image Preview Area */} -
- {imagePreviewUrls.map((url, index) => ( -
- {`Preview - -
- ))} - - {/* Upload Button (only show if less than 5 images) */} - {formData.images.length < 5 && ( - - )} -
- {errors.images && ( -

{errors.images}

- )} -
- - {/* Submit Button */} -
- -
-
- - {/* Delete Confirmation Modal */} - {showDeleteModal && ( -
-
-

- Delete Listing -

-

- Are you sure you want to delete {formData.title}? - This action cannot be undone. -

-
- - -
-
-
- )} -
- ); -}; - -export default ItemForm; diff --git a/frontend/src/pages/ProductDetail.jsx b/frontend/src/pages/ProductDetail.jsx index 15c8d86..711d1f3 100644 --- a/frontend/src/pages/ProductDetail.jsx +++ b/frontend/src/pages/ProductDetail.jsx @@ -1,50 +1,188 @@ import { useState, useEffect } from "react"; 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, +} from "lucide-react"; const ProductDetail = () => { const { id } = useParams(); const [product, setProduct] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = 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 [showContactForm, setShowContactForm] = useState(false); - const [message, setMessage] = useState(""); + const [showContactOptions, setShowContactOptions] = useState(false); const [currentImage, setCurrentImage] = useState(0); + const [reviews, setReviews] = useState([]); + const [showReviewForm, setShowReviewForm] = useState(false); + const storedUser = JSON.parse(sessionStorage.getItem("user")); - // Fetch product details + 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/add`, { + 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(() => { const fetchProduct = async () => { try { - setLoading(true); + setLoading((prev) => ({ ...prev, product: true })); const response = await fetch(`http://localhost:3030/api/product/${id}`); if (!response.ok) { - throw new Error("Failed to fetch product"); + throw new Error(`HTTP error! Status: ${response.status}`); } const result = await response.json(); - console.log(result); if (result.success) { setProduct(result.data); - setError(null); + setError((prev) => ({ ...prev, product: null })); } else { throw new Error(result.message || "Error fetching product"); } } catch (error) { console.error("Error fetching product:", error); - setError(error.message); - setProduct(null); + setError((prev) => ({ ...prev, product: error.message })); } finally { - setLoading(false); + setLoading((prev) => ({ ...prev, product: false })); } }; fetchProduct(); }, [id]); - // Handle favorite toggle + // Fetch reviews data + useEffect(() => { + 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 })); + } + }; + + fetchReviews(); + }, [id]); + + // Handle favorite toggle with error handling const toggleFavorite = async () => { try { const response = await fetch( @@ -61,28 +199,25 @@ const ProductDetail = () => { }, ); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const result = await response.json(); if (result.success) { setIsFavorite(!isFavorite); + } else { + throw new Error(result.message || "Failed to toggle favorite"); } } catch (error) { console.error("Error toggling favorite:", error); + alert(`Failed to add to favorites: ${error.message}`); } }; - // Handle message submission - const handleSendMessage = (e) => { - e.preventDefault(); - // TODO: Implement actual message sending logic - console.log("Message sent:", message); - setMessage(""); - setShowContactForm(false); - alert("Message sent to seller!"); - }; - // Image navigation const nextImage = () => { - if (product && product.images) { + if (product?.images?.length > 0) { setCurrentImage((prev) => prev === product.images.length - 1 ? 0 : prev + 1, ); @@ -90,7 +225,7 @@ const ProductDetail = () => { }; const prevImage = () => { - if (product && product.images) { + if (product?.images?.length > 0) { setCurrentImage((prev) => prev === 0 ? product.images.length - 1 : prev - 1, ); @@ -101,8 +236,22 @@ const ProductDetail = () => { setCurrentImage(index); }; - // Render loading state - if (loading) { + // Function to render stars based on rating + const renderStars = (rating) => { + const stars = []; + for (let i = 1; i <= 5; i++) { + stars.push( + , + ); + } + return stars; + }; + + // Render loading state for the entire page + if (loading.product) { return (
@@ -110,13 +259,30 @@ const ProductDetail = () => { ); } - // Render error state - if (error) { + // Render error state for product + if (error.product) { return (

Error Loading Product

-

{error}

+

{error.product}

+ + Back to Listings + +
+
+ ); + } + + // Safety check for product + if (!product) { + return ( +
+
+

Product Not Found

{
{product.images && product.images.length > 0 ? ( - {product.Name} + <> + {product.Name} { + e.target.onerror = null; + e.target.src = + "https://via.placeholder.com/400x300?text=Image+Not+Available"; + }} + /> + {product.images.length > 1 && ( +
+ +
+ {currentImage + 1}/{product.images.length} +
+
+ )} + ) : (
No Image Available @@ -170,6 +359,11 @@ const ProductDetail = () => { src={image} alt={`${product.Name} - view ${index + 1}`} className="w-full h-auto object-cover" + onError={(e) => { + e.target.onerror = null; + e.target.src = + "https://via.placeholder.com/100x100?text=Error"; + }} />
))} @@ -181,11 +375,14 @@ const ProductDetail = () => {

- {product.Name} + {product.Name || "Unnamed Product"}

- ${product.Price} + $ + {typeof product.Price === "number" + ? product.Price.toFixed(2) + : product.Price}
-
- - {product.Category} -
-
- Condition: - {product.condition} -
-
- - Posted on {product.Date} -
+ {product.Category && ( +
+ + {product.Category} +
+ )} + + {product.Date && ( +
+ + Posted on {product.Date} +
+ )}
-

{product.Description}

+

+ {product.Description || "No description available"} +

- +
+ - {showContactForm && ( -
-

- Contact Seller -

-
-
- - setMessage(e.target.value)} - className="w-full p-3 border border-gray-300 focus:outline-none focus:border-green-500" - required - /> -
-
- - -
- - - -
- )} + + Call Seller + + )} + + {product.SellerEmail && ( + + + Email Seller + + )} +
+ )} +
+ {/* Seller Info */}
@@ -287,31 +461,157 @@ const ProductDetail = () => {

- {product.UserID || "Unknown Seller"} + {product.SellerName || "Unknown Seller"}

- Member since{" "} - {product.seller ? product.seller.memberSince : "N/A"} + Member since {product.SellerJoinDate || "N/A"}

-
-
- Rating:{" "} - {product.seller ? `${product.seller.rating}/5` : "N/A"} -
-
- {/*
-

Description

+ {/* Reviews Section */} +
+

Reviews

+
-
{product.Description}
+ {loading.reviews ? ( +
+
+
+ ) : error.reviews ? ( +
+ Error loading reviews: {error.reviews} +
+ ) : reviews.length === 0 ? ( +
+ No reviews yet for this product +
+ ) : ( +
+ {reviews.map((review) => ( +
+
+
+ {review.ReviewerName} +
+
+ {review.ReviewDate + ? new Date(review.ReviewDate).toLocaleDateString() + : "Unknown date"} +
+
+ +
+ {renderStars(review.Rating || 0)} + + {review.Rating || 0}/5 + +
+ +
+ {review.Comment || "No comment provided"} +
+
+ ))} +
+ )}
-
*/} + +
+ +
+ + {/* Review Popup Form */} + {showReviewForm && ( +
+
+
+

+ Write a Review +

+ +
+ +
+
+ +
+ {[1, 2, 3, 4, 5].map((star) => ( + + ))} + + {reviewForm.rating}/5 + +
+
+ +
+ + +
+ +
+ + +
+
+
+
+ )} +
); }; diff --git a/frontend/src/pages/SearchPage.jsx b/frontend/src/pages/SearchPage.jsx index 905eb70..0885ab1 100644 --- a/frontend/src/pages/SearchPage.jsx +++ b/frontend/src/pages/SearchPage.jsx @@ -1,5 +1,5 @@ -import React, { useState, useEffect } from "react"; -import { Filter, Grid, Heart, Tag, X } from "lucide-react"; +import { useState, useEffect } from "react"; +import { X } from "lucide-react"; import { useLocation, Link } from "react-router-dom"; import axios from "axios"; @@ -27,7 +27,7 @@ const SearchPage = () => { try { const response = await axios.get( - `http://localhost:3030/api/search_products/search`, + `http://localhost:3030/api/search/getProduct`, { params: { name: query }, }, @@ -96,12 +96,13 @@ const SearchPage = () => { return (
+ {/* Filter sidebar */}
@@ -110,9 +111,8 @@ const SearchPage = () => {
-
-
+

Price Range

@@ -126,7 +126,7 @@ const SearchPage = () => { min: Number(e.target.value), })) } - className="w-full p-2 border rounded text-gray-700" + className="w-full p-2 border text-gray-700" /> { max: Number(e.target.value), })) } - className="w-full p-2 border rounded text-gray-700" + className="w-full p-2 border text-gray-700" />
@@ -146,13 +146,13 @@ const SearchPage = () => {
@@ -160,6 +160,7 @@ const SearchPage = () => {
+ {/* Main content */}

{filteredProducts.length} Results @@ -170,18 +171,17 @@ const SearchPage = () => { )}

-
{filteredProducts.map((listing) => ( {listing.title}

diff --git a/frontend/src/pages/Selling.jsx b/frontend/src/pages/Selling.jsx index 1ed5fdf..99de591 100644 --- a/frontend/src/pages/Selling.jsx +++ b/frontend/src/pages/Selling.jsx @@ -1,13 +1,224 @@ -import { useState } from 'react'; -import { Link } from 'react-router-dom'; -import { Tag, Book, Laptop, Sofa, Utensils, Gift, Heart } from 'lucide-react'; +import { useState, useEffect } from "react"; +import ProductForm from "../components/ProductForm"; const Selling = () => { + // State to store user's products + const [products, setProducts] = useState([]); + // State to control when editing form is shown + const [showForm, setShowForm] = useState(false); + // State to store the product being edited (or empty for new product) + const [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 ( -
- +
+
+

My Listings

+ {!showForm && ( + + )} +
+ + {showForm ? ( + setShowForm(false)} + /> + ) : ( + <> + {products.length === 0 ? ( +
+

+ You don't have any listings yet +

+ +
+ ) : ( +
+ {products.map((product) => ( +
+
+ {product.images && product.images.length > 0 ? ( + {product.name} + ) : ( +
No image
+ )} +
+ +
+
+

+ {product.name} +

+ + {product.status} + +
+ +

+ ${product.price} +

+ + {product.categories && product.categories.length > 0 && ( +
+ {product.categories.map((category) => ( + + {category} + + ))} +
+ )} + +

+ {product.description} +

+ +
+ + +
+
+
+ ))} +
+ )} + + )}
); }; -export default Selling; \ No newline at end of file +export default Selling; diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index 150ff05..42751fd 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -1,5 +1,6 @@ import { useState, useEffect } from "react"; import { User, Lock, Trash2, History, Search, Shield } from "lucide-react"; +import FloatingAlert from "../components/FloatingAlert"; // adjust path if needed const Settings = () => { const [userData, setUserData] = useState({ @@ -16,6 +17,8 @@ const Settings = () => { const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); + const [showAlert, setShowAlert] = useState(false); + const storedUser = JSON.parse(sessionStorage.getItem("user")); // Fetch user data when component mounts useEffect(() => { @@ -88,6 +91,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 () => { try { // Ensure userId is present @@ -159,12 +181,6 @@ const Settings = () => { } }; - const handleDeleteHistory = (type) => { - // TODO: Delete the specified history - console.log(`Deleting ${type} history`); - alert(`${type} history deleted successfully!`); - }; - const handleDeleteAccount = async () => { if ( window.confirm( @@ -416,6 +432,12 @@ const Settings = () => {
{/* Privacy Section */} + {showAlert && ( + setShowAlert(false)} + /> + )}
@@ -427,38 +449,17 @@ const Settings = () => {
-
- -
-

Search History

-

- Delete all your search history on StudentMarket -

-
-
- -
- -
-

- Browsing History -

+

History

- Delete all your browsing history on StudentMarket + Delete all your history on Market