diff --git a/README.md b/README.md index 47f82f1..b506b12 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,42 @@ ### 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, prefix 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 - 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 - 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 - 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 - npm run dev + pip install mysql.connector ``` + ### 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 ./SQL_code/init-db.py + 1. mysql -u root + 2. \. 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 bd40907..569e4c6 100644 --- a/backend/controllers/product.js +++ b/backend/controllers/product.js @@ -1,13 +1,45 @@ const db = require("../utils/database"); -exports.addToFavorite = async (req, res) => { - const { userID, productsID } = req.body; +exports.addProduct = async (req, res) => { + 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 { // 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,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 exports.getAllProducts = async (req, res) => { try { const [data, fields] = await db.execute(` - SELECT p.*, i.URL - FROM Product p - LEFT JOIN Image_URL i ON p.ProductID = i.ProductID + SELECT + P.ProductID, + P.Name AS ProductName, + P.Price, + P.Date AS DateUploaded, + U.Name AS SellerName, + MIN(I.URL) AS ProductImage, + C.Name AS Category +FROM Product P +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({ @@ -43,44 +155,58 @@ exports.getAllProducts = async (req, res) => { } }; -// Get a single product by ID along with image URLs exports.getProductById = async (req, res) => { const { id } = req.params; - console.log(id); + console.log("Received Product ID:", id); + 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], ); + // Log raw data for debugging + console.log("Raw Database Result:", data); + if (data.length === 0) { + console.log("No product found with ID:", id); return res.status(404).json({ success: false, 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 = { - ...data[0], // First product found in the query - images: data.map((image) => image.image_url), // Collect all image URLs into an array + ...data[0], // Base product details + images: images, // Collected image URLs }; + // Log processed product for debugging + console.log("Processed Product:", product); + res.json({ success: true, message: "Product fetched successfully", data: product, }); } catch (error) { - console.error("Error fetching product:", error); + console.error("Full Error Details:", error); return res.status(500).json({ success: false, - error: "Database error occurred", + message: "Database error occurred", + error: error.message, }); } }; 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/controllers/search.js b/backend/controllers/search.js new file mode 100644 index 0000000..ba6700c --- /dev/null +++ b/backend/controllers/search.js @@ -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", +// }); +// } +// }; diff --git a/backend/controllers/user.js b/backend/controllers/user.js index 27a0dc3..32aef17 100644 --- a/backend/controllers/user.js +++ b/backend/controllers/user.js @@ -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) => { try { const [users, fields] = await db.execute("SELECT * FROM User;"); @@ -174,6 +230,7 @@ exports.findUserByEmail = async (req, res) => { UCID: user.UCID, phone: user.Phone, address: user.Address, + password: user.Password, // Include any other fields your user might have // 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 UCID = req.body?.UCID; const address = req.body?.address; - + const password = req.body?.password; if (!userId) { 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 (UCID) updateData.UCID = UCID; if (address) updateData.address = address; - + if (password) updateData.password = password; if (Object.keys(updateData).length === 0) { return res.status(400).json({ error: "No valid fields to update" }); } diff --git a/backend/index.js b/backend/index.js index 01fc880..7afa3fe 100644 --- a/backend/index.js +++ b/backend/index.js @@ -1,10 +1,15 @@ const express = require("express"); const cors = require("cors"); -//Get the db connection + 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, @@ -28,15 +33,20 @@ transporter console.error("Email connection failed:", error); }); -//Check database connection checkDatabaseConnection(db); //Routes -app.use("/api/user", userRouter); //prefix with /api/user -app.use("/api/product", productRouter); //prefix with /api/product +app.use("/api/user", userRouter); +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 -setInterval(cleanupExpiredCodes, 60 * 60 * 1000); +clean_up_time = 30*60*1000; +setInterval(cleanupExpiredCodes, clean_up_time); app.listen(3030, () => { console.log(`Running Backend on http://localhost:3030/`); 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 aeea0d9..944e63b 100644 --- a/backend/routes/product.js +++ b/backend/routes/product.js @@ -1,16 +1,27 @@ +// routes/product.js const express = require("express"); const { - addToFavorite, + addFavorite, + getFavorites, + removeFavorite, getAllProducts, getProductById, + addProduct, } = require("../controllers/product"); - 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; 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..b39c8f6 --- /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("/addReview", submitReview); + +module.exports = router; diff --git a/backend/routes/search.js b/backend/routes/search.js new file mode 100644 index 0000000..d59f785 --- /dev/null +++ b/backend/routes/search.js @@ -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; diff --git a/backend/routes/user.js b/backend/routes/user.js index 3d11102..1ccbc88 100644 --- a/backend/routes/user.js +++ b/backend/routes/user.js @@ -7,6 +7,7 @@ const { findUserByEmail, updateUser, deleteUser, + doLogin, } = require("../controllers/user"); const router = express.Router(); @@ -26,6 +27,9 @@ router.get("/fetch_all_users", getAllUser); //Fetch One user Data with all fields: router.post("/find_user", findUserByEmail); +//Fetch One user Data with all fields: +router.post("/do_login", doLogin); + //Update A uses Data: router.post("/update", updateUser); diff --git a/backend/utils/database.js b/backend/utils/database.js index 0bd82e5..6e75c3a 100644 --- a/backend/utils/database.js +++ b/backend/utils/database.js @@ -1,11 +1,9 @@ const mysql = require("mysql2"); -//Create a pool of connections to allow multiple query happen at the same time const pool = mysql.createPool({ host: "localhost", user: "root", - database: "marketplace", + database: "Marketplace", }); -//Export a promise for promise-based query module.exports = pool.promise(); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2d81364..7b44684 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@tailwindcss/vite": "^4.0.9", + "axios": "^1.8.4", "lucide-react": "^0.477.0", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -1770,6 +1771,12 @@ "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": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", @@ -1824,6 +1831,17 @@ "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": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1898,7 +1916,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -1993,6 +2010,18 @@ "dev": true, "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": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2153,6 +2182,15 @@ "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": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", @@ -2182,7 +2220,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -2283,7 +2320,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2293,7 +2329,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2331,7 +2366,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -2344,7 +2378,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -2732,6 +2765,26 @@ "dev": true, "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": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -2748,6 +2801,21 @@ "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": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -2780,7 +2848,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2831,7 +2898,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -2856,7 +2922,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -2931,7 +2996,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3002,7 +3066,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3015,7 +3078,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -3031,7 +3093,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -3895,12 +3956,32 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "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": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4251,6 +4332,12 @@ "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": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index aa89b14..8a323b6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@tailwindcss/vite": "^4.0.9", + "axios": "^1.8.4", "lucide-react": "^0.477.0", "react": "^19.0.0", "react-dom": "^19.0.0", 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 f6f42cd..4b88b70 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -12,7 +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 function App() { // Authentication state - initialize from localStorage if available @@ -30,6 +30,8 @@ function App() { const [error, setError] = useState(""); const [isLoading, setIsLoading] = useState(false); + const [userId, setUserId] = useState(null); + // New verification states const [verificationStep, setVerificationStep] = useState("initial"); // 'initial', 'code-sent', 'verifying' const [tempUserData, setTempUserData] = useState(null); @@ -50,6 +52,10 @@ function App() { return () => window.removeEventListener("resize", handleResize); }, []); + useEffect(() => { + sendSessionDataToServer(); + }, []); + // Send verification code const sendVerificationCode = async (userData) => { try { @@ -189,6 +195,9 @@ function App() { sessionStorage.setItem("isAuthenticated", "true"); sessionStorage.setItem("user", JSON.stringify(newUser)); + // After successful signup, send session data to server + sendSessionDataToServer(); // Call it after signup + // Reset verification steps setVerificationStep("initial"); setTempUserData(null); @@ -240,7 +249,7 @@ function App() { UCID: formValues.ucid, phone: formValues.phone, password: formValues.password, // This will be needed for the final signup - address: "NOT_GIVEN", + address: formValues.address, // Add this line client: 1, admin: 0, }; @@ -256,7 +265,7 @@ function App() { // Make API call to localhost:3030/find_user const response = await fetch( - "http://localhost:3030/api/user/find_user", + "http://localhost:3030/api/user/do_login", { method: "POST", headers: { @@ -356,6 +365,48 @@ function App() { 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 const LoginComponent = () => (
@@ -480,6 +531,25 @@ function App() {
)} + {isSignUp && ( +
+ + +
+ )} +
@@ -57,10 +70,11 @@ const Navbar = ({ onLogout, userName }) => { {/* Favorites Button */} + {/* User Profile */} diff --git a/frontend/src/components/ProductForm.jsx b/frontend/src/components/ProductForm.jsx new file mode 100644 index 0000000..32ed01b --- /dev/null +++ b/frontend/src/components/ProductForm.jsx @@ -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 ( +
+ {/* Back Button */} + + +

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

+ +
+ {/* Product Name */} +
+ + + setEditingProduct({ ...editingProduct, name: e.target.value }) + } + className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-500 focus:outline-none" + /> +
+ + {/* Price */} +
+ + + setEditingProduct({ + ...editingProduct, + price: e.target.value, + }) + } + className="w-full px-3 py-2 border border-gray-300 focus:border-emerald-500 focus:outline-none" + /> +
+ + {/* Sold Status */} +
+
+ + + {editingProduct.isSold && ( + + Sold + + )} +
+
+ + {/* Categories */} +
+ +
+ + +
+ + {/* Selected Categories */} + {(editingProduct.categories || []).length > 0 ? ( +
+ {(editingProduct.categories || []).map((category) => ( + + {category} + + + ))} +
+ ) : ( +

+ Please select at least one category +

+ )} +
+ + {/* Description */} +
+ + +
+ + {/* Image Upload */} +
+ + + { + 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 + +
+ ))} +
+
+ )} +
+
+ + {/* Actions */} +
+ + +
+ + +
+
+
+ ); +}; + +export default ProductForm; diff --git a/frontend/src/components/UserDropdown.jsx b/frontend/src/components/UserDropdown.jsx index 76870e7..e8eed00 100644 --- a/frontend/src/components/UserDropdown.jsx +++ b/frontend/src/components/UserDropdown.jsx @@ -1,19 +1,19 @@ -import { useState, useRef, useEffect } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; -import { User, Settings, ShoppingBag, DollarSign, LogOut } from 'lucide-react'; +import { useState, useRef, useEffect } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { User, Settings, ShoppingBag, DollarSign, LogOut } from "lucide-react"; const UserDropdown = ({ onLogout, userName }) => { const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); const navigate = useNavigate(); - + // Use passed userName or fallback to default - const displayName = userName || 'User'; - + const displayName = userName || "User"; + const toggleDropdown = () => { setIsOpen(!isOpen); }; - + // Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event) => { @@ -21,45 +21,45 @@ const UserDropdown = ({ onLogout, userName }) => { setIsOpen(false); } }; - - document.addEventListener('mousedown', handleClickOutside); + + document.addEventListener("mousedown", handleClickOutside); return () => { - document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener("mousedown", handleClickOutside); }; }, []); - + const handleLogout = () => { // Close the dropdown setIsOpen(false); - + // Call the onLogout function from props if (onLogout) { onLogout(); console.log("Logout successful"); } - + // Navigate to login page (this may be redundant as App.jsx should handle redirection) - navigate('/login'); + navigate("/login"); }; - + return (
- + {isOpen && (
{/* User Info */}

{displayName}

- + {/* Navigation Links */}
{ My Listings - + { Transactions - + { Settings - +
- {/* Filters and Sorting */} - {showFilters && ( -
-
-
- - -
-
- - -
-
-
- )} - - {/* Favorites List */} - {filteredFavorites.length === 0 ? ( + {sortedFavorites.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
) : ( -
- {filteredFavorites.map((item) => ( -
- - - - {item.title} - +
+ {sortedFavorites.map((product) => ( +
+ +
+ {product.image ? ( + {product.name} + ) : ( +
No image
+ )} +
+
-
-

- {item.title} +
+

+ {product.name}

- ${item.price} -
- -
- - {item.category} - - {item.condition} -
- -
- Listed {item.datePosted} - {item.seller} +
+ +

+ ${product.price.toFixed(2)} +

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

+ {product.description} +

+ +

+ Posted {product.datePosted} +

@@ -153,15 +192,68 @@ const Favorites = () => {
)} - {/* Show count if there are favorites */} - {filteredFavorites.length > 0 && ( + {sortedFavorites.length > 0 && (
- Showing {filteredFavorites.length} {filteredFavorites.length === 1 ? 'item' : 'items'} - {filterCategory !== 'All' && ` in ${filterCategory}`} + Showing {sortedFavorites.length}{" "} + {sortedFavorites.length === 1 ? "item" : "items"}
)} + +
+
+
+
+

Campus Marketplace

+

+ Your trusted university trading platform +

+
+ +
+
+

Quick Links

+
    +
  • + + Home + +
  • +
  • + + Sell an Item + +
  • +
  • + + My Favorites + +
  • +
+
+ +
+

Contact

+
    +
  • support@campusmarket.com
  • +
  • University of Calgary
  • +
+
+
+
+ +
+

+ © {new Date().getFullYear()} Campus Marketplace. All rights + reserved. +

+
+
+
); }; -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 4ae5a6e..22dc205 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -1,17 +1,121 @@ 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, + ChevronLeft, + ChevronRight, + Bookmark, + BookmarkCheck, +} 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 [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(() => { 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"); @@ -21,14 +125,13 @@ const Home = () => { setListings( data.data.map((product) => ({ id: product.ProductID, - title: product.Name, + title: product.ProductName, // Use the alias from SQL price: product.Price, - category: product.CategoryID, - image: product.URL, - condition: "New", // Modify based on actual data - seller: product.UserID, // Modify if seller info is available - datePosted: "Just now", - isFavorite: false, + 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 { @@ -39,140 +142,425 @@ const Home = () => { setError(error.message); } }; - 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 + 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(); + 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"); }; return ( -
- {/* Hero Section with School Background */} -
- {/* Background Image - Positioned at bottom */} -
- University of Calgary - {/* Dark overlay for better text readability */} -
+
+
+ {/* Hero Section with School Background */} +
+ {/* Background Image - Positioned at bottom */} +
+ University of Calgary + {/* Dark overlay for better text readability */} +
- {/* Content */} -
-

- Buy and Sell on Campus -

-

- The marketplace exclusively for university students. Find everything - you need or sell what you don't. -

- -
-
- - {/* Categories */} - {/*
-

Categories

-
- {categories.map((category) => ( + {/* Content */} +
+

+ Buy and Sell on Campus +

+

+ The marketplace exclusively for university students. Find + everything you need or sell what you don't. +

- ))} +
-
*/} - {/* Recent Listings */} -
-

- Recent Listings -

-
- {listings.map((listing) => ( - setShowAlert(false)} + /> + )} +
+

+ Recommendation +

+ +
+ {/* Left Button - Overlaid on products */} + + + {/* Scrollable Listings Container */} +
+ {recommended.map((recommended) => ( + addHistory(recommended.id)} + className="bg-white border border-gray-200 hover:shadow-md transition-shadow w-70 flex-shrink-0 relative" > - - -
+
+ {recommended.title} + +
-
-
-

- {listing.title} -

- - ${listing.price} - -
+
+

+ {recommended.title} +

+ + ${recommended.price} + -
- - {listing.category} - - {listing.condition} -
+
+ + {recommended.category} +
-
- - {listing.datePosted} - - - {listing.seller} - -
-
- - ))} +
+ + {recommended.datePosted} + + + {recommended.seller} + +
+
+ + ))} +
+ + {/* Right Button - Overlaid on products */} + +
+
+ + {/* Recent Listings */} + {showAlert && ( + setShowAlert(false)} + /> + )} +
+

+ Recent Listings +

+ +
+ {/* Left Button - Overlaid on products */} + + + {/* Scrollable Listings Container */} +
+ {listings.map((listing) => ( + +
+ {listing.title} addHistory(listing.id)} + className="w-full h-48 object-cover" + /> + +
+ +
+

+ {listing.title} +

+ + ${listing.price} + + +
+ + {listing.category} +
+ +
+ + {listing.datePosted} + + + {listing.seller} + +
+
+ + ))} +
+ + {/* Right Button - Overlaid on products */} + +
+
+ + {/* History Section */} + {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 */} + +
+ + {/* Footer - Added here */} +
+
+
+
+

Campus Marketplace

+

+ Your trusted university trading platform +

+
+ +
+
+

Quick Links

+
    +
  • + + Home + +
  • +
  • + + Sell an Item + +
  • +
  • + + My Favorites + +
  • +
+
+ +
+

Contact

+
    +
  • support@campusmarket.com
  • +
  • University of Calgary
  • +
+
+
+
+ +
+

+ © {new Date().getFullYear()} Campus Marketplace. All rights + reserved. +

+
+
+
); }; 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 95a5286..a5f0d73 100644 --- a/frontend/src/pages/ProductDetail.jsx +++ b/frontend/src/pages/ProductDetail.jsx @@ -1,255 +1,613 @@ 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, + Bookmark, +} from "lucide-react"; +import FloatingAlert from "../components/FloatingAlert"; // adjust path if needed const ProductDetail = () => { const { id } = useParams(); 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 [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 [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(() => { const fetchProduct = async () => { try { - const response = await fetch( - `http://localhost:3030/api/product/get_productID/${id}`, - ); - if (!response.ok) throw new Error("Failed to fetch product"); + setLoading((prev) => ({ ...prev, product: true })); + const response = await fetch(`http://localhost:3030/api/product/${id}`); - const data = await response.json(); - if (data.success) { - setProduct(data.data); // Update the state with product details + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + const result = await response.json(); + + if (result.success) { + setProduct(result.data); + setError((prev) => ({ ...prev, product: null })); } else { - throw new Error(data.message || "Error fetching product"); + throw new Error(result.message || "Error fetching product"); } } catch (error) { console.error("Error fetching product:", error); + setError((prev) => ({ ...prev, product: error.message })); + } finally { + setLoading((prev) => ({ ...prev, product: false })); } }; fetchProduct(); }, [id]); - // Handle favorite toggle - const toggleFavorite = () => { - setIsFavorite(!isFavorite); - }; + // Fetch reviews data + useEffect(() => { + const fetchReviews = async () => { + try { + setLoading((prev) => ({ ...prev, reviews: true })); + const response = await fetch(`http://localhost:3030/api/review/${id}`); - // Handle message submission - const handleSendMessage = (e) => { - e.preventDefault(); - // Handle message logic here (send to seller) - console.log("Message sent:", message); - setMessage(""); - setShowContactForm(false); - alert("Message sent to seller!"); - }; + 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]); // Image navigation const nextImage = () => { - setCurrentImage((prev) => - prev === product.images.length - 1 ? 0 : prev + 1, - ); + if (product?.images?.length > 0) { + setCurrentImage((prev) => + prev === product.images.length - 1 ? 0 : prev + 1, + ); + } }; const prevImage = () => { - setCurrentImage((prev) => - prev === 0 ? product.images.length - 1 : prev - 1, - ); + if (product?.images?.length > 0) { + setCurrentImage((prev) => + prev === 0 ? product.images.length - 1 : prev - 1, + ); + } }; const selectImage = (index) => { setCurrentImage(index); }; - if (!product) return
Loading...
; // Handle loading state + // 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 ( +
+
+
+ ); + } + + // Render error state for product + if (error.product) { + return ( +
+
+

Error Loading Product

+

{error.product}

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

Product Not Found

+ + Back to Listings + +
+
+ ); + } + + // Render product details return (
- Back to listings + Back
+ {showAlert && ( + setShowAlert(false)} + /> + )}
- {product.title} + {product.images && product.images.length > 0 ? ( + <> + {product.Name} { + e.target.onerror = null; + e.target.src = "https://via.placeholder.com/400x300"; + }} + /> + {product.images.length > 1 && ( +
+ +
+ {currentImage + 1}/{product.images.length} +
+
+ )} + + ) : ( +
+ No Image Available +
+ )}
- {product.images.length > 1 && ( + {product.images && product.images.length > 1 && (
{product.images.map((image, index) => (
selectImage(index)} > {`${product.title} { + e.target.onerror = null; + e.target.src = + "https://via.placeholder.com/100x100?text=Error"; + }} />
))}
)}
-

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

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

{product.shortDescription}

+

+ {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 */}
- {product.seller.avatar ? ( - Seller - ) : ( -
- -
- )} +
+ +

- {product.seller.name} + {product.SellerName || "Unknown Seller"}

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

-
-
- Rating:{" "} - {product.seller.rating}/5 -
-
+ {/* Reviews Section */}
-

Description

+

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 new file mode 100644 index 0000000..631c2bd --- /dev/null +++ b/frontend/src/pages/SearchPage.jsx @@ -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 ( +
+
+ {/* Filter sidebar */} +
+
+

Filters

+ +
+
+
+

Price Range

+
+
+ + setPriceRange((prev) => ({ + ...prev, + min: Number(e.target.value), + })) + } + className="w-full p-2 border text-gray-700" + /> + + setPriceRange((prev) => ({ + ...prev, + max: Number(e.target.value), + })) + } + className="w-full p-2 border text-gray-700" + /> +
+
+
+
+ + +
+
+
+ + {/* Main content */} +
+

+ {filteredProducts.length} Results + {searchQuery && ( + + {" "} + for "{searchQuery}" + + )} +

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

+ {listing.title} +

+

+ ${Number(listing.price).toFixed(2)} +

+
+ + ))} +
+
+
+ {/* Footer - Added here */} +
+
+
+
+

Campus Marketplace

+

+ Your trusted university trading platform +

+
+ +
+
+

Quick Links

+
    +
  • + + Home + +
  • +
  • + + Sell an Item + +
  • +
  • + + My Favorites + +
  • +
+
+ +
+

Contact

+
    +
  • support@campusmarket.com
  • +
  • University of Calgary
  • +
+
+
+
+ +
+

+ © {new Date().getFullYear()} Campus Marketplace. All rights + reserved. +

+
+
+
+
+ ); +}; + +export default SearchPage; 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..61c0c27 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 { User, Lock, Trash2, History, Shield } from "lucide-react"; +import FloatingAlert from "../components/FloatingAlert"; // adjust path if needed const Settings = () => { const [userData, setUserData] = useState({ @@ -9,13 +10,13 @@ const Settings = () => { phone: "", UCID: "", address: "", - currentPassword: "", - newPassword: "", - confirmPassword: "", + password: "", }); 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(() => { @@ -59,10 +60,7 @@ const Settings = () => { UCID: data.UCID || storedUser.UCID || "", phone: data.phone || storedUser.phone || "", address: data.address || storedUser.address || "", - // Reset password fields - currentPassword: "", - newPassword: "", - confirmPassword: "", + password: data.password, })); } else { 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 () => { try { // 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 () => { if ( window.confirm( @@ -214,7 +190,7 @@ const Settings = () => { if (isLoading) { return (
-
+
); } @@ -258,7 +234,7 @@ const Settings = () => { id="name" value={userData.name} onChange={handleInputChange} - className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500" + className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-500" required />
@@ -275,7 +251,7 @@ const Settings = () => { id="email" value={userData.email} onChange={handleInputChange} - className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500" + className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-500" required readOnly // Email is often used as identifier and not changeable /> @@ -293,7 +269,7 @@ const Settings = () => { id="phone" value={userData.phone} onChange={handleInputChange} - className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500" + className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-500" />
@@ -309,7 +285,7 @@ const Settings = () => { id="UCID" value={userData.UCID} onChange={handleInputChange} - className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500" + className="w-full p-2 border border-gray-300 focus:outline-none focus:border-emerald-500" required />
@@ -326,14 +302,29 @@ const Settings = () => { id="address" value={userData.address} 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" + /> +
+
+ +
@@ -341,81 +332,13 @@ const Settings = () => {
- {/* Security Section */} -
-
-
- -

Password

-
-
- -
-
-
-
- - -
- -
- - -
- -
- - -
-
- - -
-
-
- {/* Privacy Section */} + {showAlert && ( + setShowAlert(false)} + /> + )}
@@ -426,39 +349,18 @@ 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