From dee6e3ce101e2fcea5338c988f4a26678d55858b Mon Sep 17 00:00:00 2001 From: Mann Patel <130435633+MannPatel0@users.noreply.github.com> Date: Fri, 18 Apr 2025 22:09:04 -0600 Subject: [PATCH 1/7] getCategory update --- backend/controllers/category.js | 25 ++++++++++++++++++ backend/index.js | 6 +++-- backend/routes/category.js | 7 +++++ backend/utils/database.js | 4 +++ .../__pycache__/app.cpython-313.pyc | Bin 5724 -> 5730 bytes 5 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 backend/controllers/category.js create mode 100644 backend/routes/category.js diff --git a/backend/controllers/category.js b/backend/controllers/category.js new file mode 100644 index 0000000..98a6c25 --- /dev/null +++ b/backend/controllers/category.js @@ -0,0 +1,25 @@ +const db = require("../utils/database"); + +exports.getAllCategory = async (req, res) => { + try { + const [data, fields] = await db.execute(`SELECT * FROM Category`); + + // Format as { ID: "", ID: "" } + const formattedData = {}; + data.forEach((row) => { + formattedData[row.CategoryID] = row.Name; + }); + + res.json({ + success: true, + message: "Categories fetched successfully", + data: formattedData, + }); + } catch (error) { + console.error("Error fetching categories:", error); + return res.status(500).json({ + success: false, + error: "Database error occurred", + }); + } +}; diff --git a/backend/index.js b/backend/index.js index 7afa3fe..04c623e 100644 --- a/backend/index.js +++ b/backend/index.js @@ -9,12 +9,14 @@ const searchRouter = require("./routes/search"); const recommendedRouter = require("./routes/recommendation"); const history = require("./routes/history"); const review = require("./routes/review"); +const category = require("./routes/category"); const { generateEmailTransporter } = require("./utils/mail"); const { cleanupExpiredCodes, checkDatabaseConnection, } = require("./utils/helper"); +const { getAllCategory } = require("./controllers/category"); const app = express(); @@ -42,10 +44,10 @@ app.use("/api/search", searchRouter); app.use("/api/engine", recommendedRouter); app.use("/api/history", history); app.use("/api/review", review); - +app.use("/api/category", category); // Set up a scheduler to run cleanup every hour -clean_up_time = 30*60*1000; +clean_up_time = 30 * 60 * 1000; setInterval(cleanupExpiredCodes, clean_up_time); app.listen(3030, () => { diff --git a/backend/routes/category.js b/backend/routes/category.js new file mode 100644 index 0000000..425adc6 --- /dev/null +++ b/backend/routes/category.js @@ -0,0 +1,7 @@ +const express = require("express"); +const { getAllCategory } = require("../controllers/category"); +const router = express.Router(); + +router.get("/", getAllCategory); + +module.exports = router; diff --git a/backend/utils/database.js b/backend/utils/database.js index 6e75c3a..379d5df 100644 --- a/backend/utils/database.js +++ b/backend/utils/database.js @@ -6,4 +6,8 @@ const pool = mysql.createPool({ database: "Marketplace", }); +// const pool = mysql.createPool( +// "singlestore://mann-619d0:@svc-3482219c-a389-4079-b18b-d50662524e8a-shared-dml.aws-virginia-6.svc.singlestore.com:3333/db_mann_48ba9?ssl={}", +// ); + module.exports = pool.promise(); diff --git a/recommondation-engine/__pycache__/app.cpython-313.pyc b/recommondation-engine/__pycache__/app.cpython-313.pyc index f5efd97bfd14a014c6683a8edf8f01535a1d88e2..373cb2686da6709f2f82ff2345857432f34cf2aa 100644 GIT binary patch delta 43 xcmcbk^GJvLGcPX}0}ym_GH>K=Vife!4=qkDD%Q_U%*!iCEJ@ATJcIFw2mlnr4ZHvV delta 37 rcmaE)b4Q2!GcPX}0}w>4VBE;v#K`TUpOK%Ns-K&fm$!Ky;}H=6(b)^5 From d169c9ba587d5d389e002b963ba3ec2e3eac10e0 Mon Sep 17 00:00:00 2001 From: Mann Patel <130435633+MannPatel0@users.noreply.github.com> Date: Sat, 19 Apr 2025 10:22:16 -0600 Subject: [PATCH 2/7] update --- backend/controllers/product.js | 71 +++++--- backend/routes/product.js | 2 + frontend/src/App.jsx | 4 - frontend/src/components/ProductForm.jsx | 169 +++++++++--------- frontend/src/index.css | 2 +- frontend/src/pages/Selling.jsx | 14 -- .../__pycache__/app.cpython-313.pyc | Bin 5730 -> 5724 bytes 7 files changed, 135 insertions(+), 127 deletions(-) diff --git a/backend/controllers/product.js b/backend/controllers/product.js index 569e4c6..9684513 100644 --- a/backend/controllers/product.js +++ b/backend/controllers/product.js @@ -1,34 +1,22 @@ const db = require("../utils/database"); exports.addProduct = async (req, res) => { - const { userID, name, price, qty, description, category, images } = req.body; - + const { userID, name, price, stockQty, Description } = req.body; + console.log(userID); try { + // Use parameterized query to prevent SQL injection const [result] = await db.execute( - `INSERT INTO Product (Name, Price, StockQuantity, UserID, Description, CategoryID) VALUES (?, ?, ?, ?, ?, ?)`, - [name, price, qty, userID, description, category], + `INSERT INTO Favorites (UserID, ProductID) VALUES (?, ?)`, + [userID, productID], ); - 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", + message: "Product added to favorites successfully", }); } catch (error) { - console.error("Error adding product or images:", error); - console.log(error); - return res.json({ error: "Could not add product or images" }); + console.error("Error adding favorite product:", error); + return res.json({ error: "Could not add favorite product" }); } }; @@ -72,6 +60,49 @@ exports.removeFavorite = async (req, res) => { } }; +exports.myProduct = 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 Product p + JOIN User u ON p.UserID = u.UserID + LEFT JOIN Image_URL i ON p.ProductID = i.ProductID + WHERE p.UserID = ? + GROUP BY + p.ProductID, + p.Name, + p.Description, + p.Price, + p.CategoryID, + p.UserID, + p.Date, + u.Name; + `, + [userID], + ); + + res.json({ + success: true, + favorites: favorites, + }); + } catch (error) { + console.error("Error retrieving favorites:", error); + res.status(500).json({ error: "Could not retrieve favorite products" }); + } +}; + exports.getFavorites = async (req, res) => { const { userID } = req.body; diff --git a/backend/routes/product.js b/backend/routes/product.js index 944e63b..8343405 100644 --- a/backend/routes/product.js +++ b/backend/routes/product.js @@ -7,6 +7,7 @@ const { getAllProducts, getProductById, addProduct, + myProduct, } = require("../controllers/product"); const router = express.Router(); @@ -20,6 +21,7 @@ router.post("/addFavorite", addFavorite); router.post("/getFavorites", getFavorites); router.post("/delFavorite", removeFavorite); +router.post("/myProduct", myProduct); router.post("/addProduct", addProduct); router.get("/getProduct", getAllProducts); router.get("/:id", getProductById); // Simplified route diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 4b88b70..5524aac 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -30,8 +30,6 @@ 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); @@ -384,8 +382,6 @@ function App() { isAuthenticated, }; - console.log("Sending user data to the server:", requestData); - // Send data to Python server (replace with your actual server URL) const response = await fetch("http://0.0.0.0:5000/api/user/session", { method: "POST", diff --git a/frontend/src/components/ProductForm.jsx b/frontend/src/components/ProductForm.jsx index 32ed01b..1461c1b 100644 --- a/frontend/src/components/ProductForm.jsx +++ b/frontend/src/components/ProductForm.jsx @@ -1,5 +1,5 @@ -import React, { useState } from "react"; -import { X, ChevronLeft, Plus, Trash2, Check } from "lucide-react"; +import React, { useState, useEffect } from "react"; +import { X, ChevronLeft, Plus, Trash2 } from "lucide-react"; const ProductForm = ({ editingProduct, @@ -8,46 +8,42 @@ const ProductForm = ({ onCancel, }) => { const [selectedCategory, setSelectedCategory] = useState(""); + const [categories, setCategories] = useState([]); + const [categoryMapping, setCategoryMapping] = 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", - ]; + // Fetch categories from API + useEffect(() => { + const fetchCategories = async () => { + try { + const response = await fetch("http://localhost:3030/api/category"); + if (!response.ok) throw new Error("Failed to fetch categories"); - // 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 responseJson = await response.json(); + const data = responseJson.data; + + // Create an array of category names for the dropdown + // Transform the object into an array of category names + const categoryNames = []; + const mapping = {}; + + // Process the data properly to avoid rendering objects + Object.entries(data).forEach(([id, name]) => { + // Make sure each category name is a string + const categoryName = String(name); + categoryNames.push(categoryName); + mapping[categoryName] = parseInt(id); + }); + + setCategories(categoryNames); + setCategoryMapping(mapping); + } catch (error) { + console.error("Error fetching categories:", error); + } + }; + + fetchCategories(); + }, []); const handleSave = async () => { // Check if the user has selected at least one category @@ -61,13 +57,9 @@ const ProductForm = ({ 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) => { + editingProduct.images.forEach((file) => { const simulatedPath = `/public/uploads/${file.name}`; imagePaths.push(simulatedPath); }); @@ -75,13 +67,13 @@ const ProductForm = ({ // 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 + const categoryID = categoryMapping[categoryName] || 1; // 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 + qty: 1, userID: storedUser.ID, description: editingProduct.description || "", category: categoryID, @@ -115,6 +107,24 @@ const ProductForm = ({ } }; + const markAsSold = async () => { + // This would call an API to move the product to the transaction table + try { + // API call would go here + console.log("Moving product to transaction table:", editingProduct.id); + + // Toggle the sold status in the UI + setEditingProduct((prev) => ({ + ...prev, + isSold: !prev.isSold, + })); + + // You would add your API call here to update the backend + } catch (error) { + console.error("Error marking product as sold:", error); + } + }; + const addCategory = () => { if ( selectedCategory && @@ -137,13 +147,6 @@ const ProductForm = ({ })); }; - const toggleSoldStatus = () => { - setEditingProduct((prev) => ({ - ...prev, - isSold: !prev.isSold, - })); - }; - return (
{/* Back Button */} @@ -196,18 +199,8 @@ const ProductForm = ({ {/* Sold Status */}
- - {editingProduct.isSold && ( - + Sold )} @@ -232,8 +225,8 @@ const ProductForm = ({ .filter( (cat) => !(editingProduct.categories || []).includes(cat), ) - .map((category) => ( - ))} @@ -252,9 +245,9 @@ const ProductForm = ({ {/* Selected Categories */} {(editingProduct.categories || []).length > 0 ? (
- {(editingProduct.categories || []).map((category) => ( + {(editingProduct.categories || []).map((category, index) => ( {category} @@ -375,33 +368,33 @@ const ProductForm = ({
{/* Actions */} -
+
-
+ {editingProduct.id && ( - -
+ )} + +
); diff --git a/frontend/src/index.css b/frontend/src/index.css index e065eb9..9fd180c 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,4 +1,4 @@ @import "tailwindcss"; @tailwind base; @tailwind components; -@tailwind utilities; \ No newline at end of file +@tailwind utilities; diff --git a/frontend/src/pages/Selling.jsx b/frontend/src/pages/Selling.jsx index 99de591..e8a87d8 100644 --- a/frontend/src/pages/Selling.jsx +++ b/frontend/src/pages/Selling.jsx @@ -12,7 +12,6 @@ const Selling = () => { price: "", description: "", categories: [], - status: "Unsold", images: [], }); @@ -28,7 +27,6 @@ const Selling = () => { price: "299.99", description: "A beautiful vintage film camera in excellent condition", categories: ["Electronics", "Art & Collectibles"], - status: "Unsold", images: ["/public/Pictures/Dell1.jpg"], }, { @@ -37,7 +35,6 @@ const Selling = () => { price: "149.50", description: "Genuine leather jacket, worn only a few times", categories: ["Clothing"], - status: "Unsold", images: [], }, ]; @@ -71,7 +68,6 @@ const Selling = () => { price: "", description: "", categories: [], - status: "Unsold", images: [], }); }; @@ -99,7 +95,6 @@ const Selling = () => { price: "", description: "", categories: [], - status: "Unsold", images: [], }); setShowForm(true); @@ -164,15 +159,6 @@ const Selling = () => {

{product.name}

- - {product.status} -

diff --git a/recommondation-engine/__pycache__/app.cpython-313.pyc b/recommondation-engine/__pycache__/app.cpython-313.pyc index 373cb2686da6709f2f82ff2345857432f34cf2aa..f5efd97bfd14a014c6683a8edf8f01535a1d88e2 100644 GIT binary patch delta 37 rcmaE)b4Q2!GcPX}0}w>4VBE;v#K`TUpOK%Ns-K&fm$!Ky;}H=6(b)^5 delta 43 xcmcbk^GJvLGcPX}0}ym_GH>K=Vife!4=qkDD%Q_U%*!iCEJ@ATJcIFw2mlnr4ZHvV From 6ef4a22e9f224b57a8edb164c6f6724ff4dfae1b Mon Sep 17 00:00:00 2001 From: Mann Patel <130435633+MannPatel0@users.noreply.github.com> Date: Sun, 20 Apr 2025 12:50:46 -0600 Subject: [PATCH 3/7] update on category --- backend/controllers/category.js | 1 - backend/controllers/product.js | 32 ++++-- frontend/src/pages/Home.jsx | 4 +- frontend/src/pages/ProductDetail.jsx | 35 +++--- frontend/src/pages/Selling.jsx | 164 ++++++++++++++------------- 5 files changed, 127 insertions(+), 109 deletions(-) diff --git a/backend/controllers/category.js b/backend/controllers/category.js index 98a6c25..31776f7 100644 --- a/backend/controllers/category.js +++ b/backend/controllers/category.js @@ -4,7 +4,6 @@ exports.getAllCategory = async (req, res) => { try { const [data, fields] = await db.execute(`SELECT * FROM Category`); - // Format as { ID: "", ID: "" } const formattedData = {}; data.forEach((row) => { formattedData[row.CategoryID] = row.Name; diff --git a/backend/controllers/product.js b/backend/controllers/product.js index 9684513..bbe4f1f 100644 --- a/backend/controllers/product.js +++ b/backend/controllers/product.js @@ -1,22 +1,34 @@ const db = require("../utils/database"); exports.addProduct = async (req, res) => { - const { userID, name, price, stockQty, Description } = req.body; - console.log(userID); + const { userID, name, price, qty, description, category, images } = req.body; + try { - // Use parameterized query to prevent SQL injection const [result] = await db.execute( - `INSERT INTO Favorites (UserID, ProductID) VALUES (?, ?)`, - [userID, productID], + `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 added to favorites successfully", + message: "Product and images added successfully", }); } catch (error) { - console.error("Error adding favorite product:", error); - return res.json({ error: "Could not add favorite product" }); + console.error("Error adding product or images:", error); + console.log(error); + return res.json({ error: "Could not add product or images" }); } }; @@ -64,7 +76,7 @@ exports.myProduct = async (req, res) => { const { userID } = req.body; try { - const [favorites] = await db.execute( + const [result] = await db.execute( ` SELECT p.ProductID, @@ -95,7 +107,7 @@ exports.myProduct = async (req, res) => { res.json({ success: true, - favorites: favorites, + data: result, }); } catch (error) { console.error("Error retrieving favorites:", error); diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index 22dc205..9782f87 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -201,7 +201,7 @@ const Home = () => { University of Calgary {/* Dark overlay for better text readability */}

@@ -217,7 +217,7 @@ const Home = () => {

diff --git a/frontend/src/pages/ProductDetail.jsx b/frontend/src/pages/ProductDetail.jsx index a5f0d73..ebec8f2 100644 --- a/frontend/src/pages/ProductDetail.jsx +++ b/frontend/src/pages/ProductDetail.jsx @@ -1,7 +1,6 @@ import { useState, useEffect } from "react"; import { useParams, Link } from "react-router-dom"; import { - Heart, ArrowLeft, Tag, User, @@ -26,7 +25,6 @@ const ProductDetail = () => { reviews: null, submit: null, }); - const [isFavorite, setIsFavorite] = useState(false); const [showContactOptions, setShowContactOptions] = useState(false); const [currentImage, setCurrentImage] = useState(0); const [reviews, setReviews] = useState([]); @@ -52,7 +50,6 @@ const ProductDetail = () => { if (data.success) { setShowAlert(true); } - console.log(`Add Product -> History: ${id}`); }; const [reviewForm, setReviewForm] = useState({ @@ -248,7 +245,7 @@ const ProductDetail = () => { if (loading.product) { return (
-
+
); } @@ -262,7 +259,7 @@ const ProductDetail = () => {

{error.product}

Back to Listings @@ -279,7 +276,7 @@ const ProductDetail = () => {

Product Not Found

Back to Listings @@ -291,15 +288,15 @@ const ProductDetail = () => { // Render product details return (
-
+ {/*
Back -
+
*/} {showAlert && ( { {product.images.map((image, index) => (
selectImage(index)} > { e.preventDefault(); toggleFavorite(product.ProductID); }} - className="top-0 p-2 rounded-bl-md bg-emerald-600 hover:bg-emerald-500 transition shadow-sm" + className="top-0 p-2 rounded-bl-md bg-emerald-700 hover:bg-emerald-600 transition shadow-sm" >
-
+
$ {typeof product.Price === "number" ? product.Price.toFixed(2) @@ -418,7 +415,7 @@ const ProductDetail = () => {
@@ -430,7 +427,7 @@ const ProductDetail = () => { href={`tel:${product.SellerPhone}`} className="flex items-center gap-2 p-3 hover:bg-gray-50 border-b border-gray-100" > - + Call Seller )} @@ -440,7 +437,7 @@ const ProductDetail = () => { href={`mailto:${product.SellerEmail}`} className="flex items-center gap-2 p-3 hover:bg-gray-50" > - + Email Seller )} @@ -477,7 +474,7 @@ const ProductDetail = () => {
{loading.reviews ? (
-
+
) : error.reviews ? (
@@ -524,7 +521,7 @@ const ProductDetail = () => {
@@ -582,7 +579,7 @@ const ProductDetail = () => { id="comment" value={reviewForm.comment} onChange={handleReviewInputChange} - className="w-full p-3 border border-gray-300 focus:outline-none focus:border-green-500" + className="w-full p-3 border border-gray-300 focus:outline-none focus:border-emerald-600" rows="4" required > @@ -598,7 +595,7 @@ const ProductDetail = () => { - + {product.categories && product.categories.length > 0 && ( +
+ {product.CategoryID.map((category) => ( + + {category} + + ))} +
+ )} + +

+ {product.Description} +

+ +
+ + +
-
+ ))}
)} From 0c08dbc5cef43c25873a0a7c8b20f6576507bff6 Mon Sep 17 00:00:00 2001 From: Mann Patel <130435633+MannPatel0@users.noreply.github.com> Date: Sun, 20 Apr 2025 17:46:00 -0600 Subject: [PATCH 4/7] updating products --- backend/controllers/product.js | 124 ++++-- backend/index.js | 1 - backend/routes/product.js | 5 + frontend/src/components/ProductForm.jsx | 403 ------------------ frontend/src/pages/Home.jsx | 1 - frontend/src/pages/Selling.jsx | 542 +++++++++++++++++++++--- 6 files changed, 590 insertions(+), 486 deletions(-) delete mode 100644 frontend/src/components/ProductForm.jsx diff --git a/backend/controllers/product.js b/backend/controllers/product.js index bbe4f1f..f385270 100644 --- a/backend/controllers/product.js +++ b/backend/controllers/product.js @@ -32,11 +32,49 @@ exports.addProduct = async (req, res) => { } }; +exports.removeProduct = async (req, res) => { + const { userID, productID } = req.body; + console.log(userID); + + try { + // First delete images + await db.execute(`DELETE FROM Image_URL WHERE ProductID = ?`, [productID]); + await db.execute(`DELETE FROM History WHERE ProductID = ?`, [productID]); + await db.execute(`DELETE FROM Favorites WHERE ProductID = ?`, [productID]); + await db.execute(`DELETE FROM Product_Category WHERE ProductID = ?`, [ + productID, + ]); + await db.execute(`DELETE FROM Product_Category WHERE ProductID = ?`, [ + productID, + ]); + await db.execute(`DELETE FROM Transaction WHERE ProductID = ?`, [ + productID, + ]); + await db.execute( + `DELETE FROM Recommendation WHERE RecommendedProductID = ?`, + [productID], + ); + + // Then delete the product + await db.execute(`DELETE FROM Product WHERE UserID = ? AND ProductID = ?`, [ + userID, + productID, + ]); + + res.json({ + success: true, + message: "Product removed successfully", + }); + } catch (error) { + console.error("Error removing product:", error); + return res.json({ error: "Could not remove product" }); + } +}; + exports.addFavorite = async (req, res) => { const { userID, productID } = req.body; console.log(userID); try { - // Use parameterized query to prevent SQL injection const [result] = await db.execute( `INSERT INTO Favorites (UserID, ProductID) VALUES (?, ?)`, [userID, productID], @@ -72,6 +110,60 @@ exports.removeFavorite = async (req, res) => { } }; +exports.updateProduct = async (req, res) => { + const { productId } = req.params; + const { name, description, price, category, images } = req.body; + + console.log(productId); + + const connection = await db.getConnection(); + try { + await connection.beginTransaction(); + + // Step 1: Check if the product exists + const [checkProduct] = await connection.execute( + "SELECT * FROM Product WHERE ProductID = ?", + [productId], + ); + if (checkProduct.length === 0) { + await connection.rollback(); + return res.status(404).json({ error: "Product not found" }); + } + + // Step 2: Update the product + await connection.execute( + ` + UPDATE Product + SET Name = ?, Description = ?, Price = ?, CategoryID = ? + WHERE ProductID = ? + `, + [name, description, price, category, productId], + ); + + // Step 3: Delete existing images + await connection.execute(`DELETE FROM Image_URL WHERE ProductID = ?`, [ + productId, + ]); + + // Step 4: Insert new image URLs + for (const imageUrl of images) { + await connection.execute( + `INSERT INTO Image_URL (ProductID, URL) VALUES (?, ?)`, + [productId, imageUrl], + ); + } + + await connection.commit(); + res.json({ success: true, message: "Product updated successfully" }); + } catch (error) { + await connection.rollback(); + console.error("Update product error:", error); + res.status(500).json({ error: "Failed to update product" }); + } finally { + connection.release(); + } +}; + exports.myProduct = async (req, res) => { const { userID } = req.body; @@ -253,33 +345,3 @@ exports.getProductById = async (req, res) => { }); } }; - -// db_con.query( -// "SELECT ProductID FROM product WHERE ProductID = ?", -// [productID], -// (err, results) => { -// if (err) { -// console.error("Error checking product:", err); -// return res.json({ error: "Database error" }); -// } - -// if (results.length === 0) { -// return res.json({ error: "Product does not exist" }); -// } -// }, -// ); - -// db_con.query( -// "INSERT INTO Favorites (UserID, ProductID) VALUES (?, ?)", -// [userID, productID], -// (err, result) => { -// if (err) { -// console.error("Error adding favorite product:", err); -// return res.json({ error: "Could not add favorite product" }); -// } -// res.json({ -// success: true, -// message: "Product added to favorites successfully", -// }); -// }, -// ); diff --git a/backend/index.js b/backend/index.js index 04c623e..3080f33 100644 --- a/backend/index.js +++ b/backend/index.js @@ -16,7 +16,6 @@ const { cleanupExpiredCodes, checkDatabaseConnection, } = require("./utils/helper"); -const { getAllCategory } = require("./controllers/category"); const app = express(); diff --git a/backend/routes/product.js b/backend/routes/product.js index 8343405..a3c75ba 100644 --- a/backend/routes/product.js +++ b/backend/routes/product.js @@ -8,6 +8,8 @@ const { getProductById, addProduct, myProduct, + removeProduct, + updateProduct, } = require("../controllers/product"); const router = express.Router(); @@ -21,9 +23,12 @@ router.post("/addFavorite", addFavorite); router.post("/getFavorites", getFavorites); router.post("/delFavorite", removeFavorite); +router.post("/delProduct", removeProduct); router.post("/myProduct", myProduct); router.post("/addProduct", addProduct); router.get("/getProduct", getAllProducts); router.get("/:id", getProductById); // Simplified route +router.put("/update/:productId", updateProduct); + module.exports = router; diff --git a/frontend/src/components/ProductForm.jsx b/frontend/src/components/ProductForm.jsx deleted file mode 100644 index 1461c1b..0000000 --- a/frontend/src/components/ProductForm.jsx +++ /dev/null @@ -1,403 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { X, ChevronLeft, Plus, Trash2 } from "lucide-react"; - -const ProductForm = ({ - editingProduct, - setEditingProduct, - onSave, - onCancel, -}) => { - const [selectedCategory, setSelectedCategory] = useState(""); - const [categories, setCategories] = useState([]); - const [categoryMapping, setCategoryMapping] = useState({}); - const storedUser = JSON.parse(sessionStorage.getItem("user")); - - // Fetch categories from API - useEffect(() => { - const fetchCategories = async () => { - try { - const response = await fetch("http://localhost:3030/api/category"); - if (!response.ok) throw new Error("Failed to fetch categories"); - - const responseJson = await response.json(); - const data = responseJson.data; - - // Create an array of category names for the dropdown - // Transform the object into an array of category names - const categoryNames = []; - const mapping = {}; - - // Process the data properly to avoid rendering objects - Object.entries(data).forEach(([id, name]) => { - // Make sure each category name is a string - const categoryName = String(name); - categoryNames.push(categoryName); - mapping[categoryName] = parseInt(id); - }); - - setCategories(categoryNames); - setCategoryMapping(mapping); - } catch (error) { - console.error("Error fetching categories:", error); - } - }; - - fetchCategories(); - }, []); - - 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 - if (editingProduct.images && editingProduct.images.length > 0) { - // Simulating image paths for demo purposes - editingProduct.images.forEach((file) => { - 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] || 1; // Default to 3 if not found - - // Prepare payload according to API expectations - const payload = { - name: editingProduct.name || "", - price: parseFloat(editingProduct.price) || 0, - qty: 1, - 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 markAsSold = async () => { - // This would call an API to move the product to the transaction table - try { - // API call would go here - console.log("Moving product to transaction table:", editingProduct.id); - - // Toggle the sold status in the UI - setEditingProduct((prev) => ({ - ...prev, - isSold: !prev.isSold, - })); - - // You would add your API call here to update the backend - } catch (error) { - console.error("Error marking product as sold:", error); - } - }; - - const addCategory = () => { - if ( - selectedCategory && - !(editingProduct.categories || []).includes(selectedCategory) - ) { - setEditingProduct((prev) => ({ - ...prev, - categories: [...(prev.categories || []), selectedCategory], - })); - setSelectedCategory(""); - } - }; - - const removeCategory = (categoryToRemove) => { - setEditingProduct((prev) => ({ - ...prev, - categories: (prev.categories || []).filter( - (cat) => cat !== categoryToRemove, - ), - })); - }; - - return ( -
- {/* Back Button */} - - -

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

- -
- {/* Product Name */} -
- - - setEditingProduct({ ...editingProduct, name: e.target.value }) - } - className="w-full px-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, index) => ( - - {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 */} -
- - - {editingProduct.id && ( - - )} - - -
-
- ); -}; - -export default ProductForm; diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index 9782f87..9acdd69 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -66,7 +66,6 @@ const Home = () => { location.reload(); } } - reloadPage(); useEffect(() => { const fetchrecomProducts = async () => { diff --git a/frontend/src/pages/Selling.jsx b/frontend/src/pages/Selling.jsx index 4fdcde1..d5926fe 100644 --- a/frontend/src/pages/Selling.jsx +++ b/frontend/src/pages/Selling.jsx @@ -1,13 +1,14 @@ import { useState, useEffect } from "react"; import { useLocation, Link } from "react-router-dom"; - -import ProductForm from "../components/ProductForm"; -import { X } from "lucide-react"; +import { X, ChevronLeft, Plus, Trash2 } from "lucide-react"; const Selling = () => { const [products, setProducts] = useState([]); const [showForm, setShowForm] = useState(false); const storedUser = JSON.parse(sessionStorage.getItem("user")); + const [categories, setCategories] = useState([]); + const [categoryMapping, setCategoryMapping] = useState({}); + const [selectedCategory, setSelectedCategory] = useState(""); const [editingProduct, setEditingProduct] = useState({ name: "", @@ -17,6 +18,47 @@ const Selling = () => { images: [], }); + function reloadPage() { + var doctTimestamp = new Date(performance.timing.domLoading).getTime(); + var now = Date.now(); + var tenSec = 10 * 1000; + if (now > doctTimestamp + tenSec) { + location.reload(); + } + } + + // Fetch categories from API + useEffect(() => { + const fetchCategories = async () => { + try { + const response = await fetch("http://localhost:3030/api/category"); + if (!response.ok) throw new Error("Failed to fetch categories"); + + const responseJson = await response.json(); + const data = responseJson.data; + + // Create an array of category names for the dropdown + const categoryNames = []; + const mapping = {}; + + // Process the data properly to avoid rendering objects + Object.entries(data).forEach(([id, name]) => { + // Make sure each category name is a string + const categoryName = String(name); + categoryNames.push(categoryName); + mapping[categoryName] = parseInt(id); + }); + + setCategories(categoryNames); + setCategoryMapping(mapping); + } catch (error) { + console.error("Error fetching categories:", error); + } + }; + + fetchCategories(); + }, []); + // Simulate fetching products from API/database on component mount useEffect(() => { const fetchProducts = async () => { @@ -30,7 +72,7 @@ const Selling = () => { "Content-Type": "application/json", }, body: JSON.stringify({ - userID: storedUser.ID, // Assuming you have userId defined elsewhere in your component + userID: storedUser.ID, }), }, ); @@ -51,48 +93,137 @@ const Selling = () => { }, []); // Add userId to dependency array if it might change // Handle creating or updating a product - const handleSaveProduct = () => { - if (editingProduct.id) { - // Update existing product - setProducts( - products.map((p) => (p.id === editingProduct.id ? editingProduct : p)), - ); - } else { - // Create new product - const newProduct = { - ...editingProduct, - id: Date.now().toString(), // Generate a temporary ID - }; - setProducts([...products, newProduct]); + const handleSaveProduct = async () => { + if (!(editingProduct.categories || []).length) { + alert("Please select at least one category"); + return; } - // Reset form and hide it - setShowForm(false); - setEditingProduct({ - name: "", - price: "", - description: "", - categories: [], - images: [], - }); + try { + const imagePaths = []; + + if (editingProduct.images && editingProduct.images.length > 0) { + editingProduct.images.forEach((file) => { + const simulatedPath = `/public/uploads/${file.name}`; + imagePaths.push(simulatedPath); + }); + } + + const categoryName = (editingProduct.categories || [])[0]; + const categoryID = categoryMapping[categoryName] || 1; + + const payload = { + name: editingProduct.name || "", + price: parseFloat(editingProduct.price) || 0, + qty: 1, + userID: storedUser.ID, + description: editingProduct.description || "", + category: categoryID, + images: imagePaths, + }; + + console.log("Sending payload:", payload); + + const endpoint = editingProduct.ProductID + ? `http://localhost:3030/api/product/update/${editingProduct.ProductID}` + : "http://localhost:3030/api/product/addProduct"; + + const method = editingProduct.ProductID ? "PUT" : "POST"; + + const response = await fetch(endpoint, { + method, + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + const errorData = await response.text(); + throw new Error( + `${ + editingProduct.ProductID ? "Failed to update" : "Failed to add" + } product: ${errorData}`, + ); + } + + const data = await response.json(); + console.log("Product saved:", data); + + // Reset form and hide it + setShowForm(false); + setEditingProduct({ + name: "", + price: "", + description: "", + categories: [], + images: [], + }); + + // Reload products + reloadPage(); + } catch (error) { + console.error("Error saving product:", error); + alert(`Error saving product: ${error.message}`); + } }; // Handle product deletion - const handleDeleteProduct = (productId) => { - if (window.confirm("Are you sure you want to delete this product?")) { - setProducts(products.filter((p) => p.id !== productId)); + const handleDeleteProduct = async (productId) => { + try { + // Replace with your actual API endpoint + const response = await fetch( + "http://localhost:3030/api/product/delProduct", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + userID: storedUser.ID, + productID: productId, + }), + }, + ); + console.log("deleteproodidt"); + reloadPage(); + + if (!response.ok) { + throw new Error("Network response was not ok"); + } + } catch (error) { + console.error("Error fetching products:", error); + // You might want to set an error state here } }; // Handle editing a product const handleEditProduct = (product) => { + // Convert category ID to category name if needed + const categoryName = getCategoryNameById(product.CategoryID); + setEditingProduct({ ...product, + categories: categoryName ? [categoryName] : [], images: product.images || [], // Ensure images array exists }); + setShowForm(true); }; + // Helper function to get category name from ID + const getCategoryNameById = (categoryId) => { + if (!categoryId || !categoryMapping) return null; + + // Find the category name by ID + for (const [name, id] of Object.entries(categoryMapping)) { + if (id === categoryId) { + return name; + } + } + return null; + }; + // Handle adding a new product const handleAddProduct = () => { setEditingProduct({ @@ -105,6 +236,49 @@ const Selling = () => { setShowForm(true); }; + 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 markAsSold = async () => { + // This would call an API to move the product to the transaction table + try { + // API call would go here + console.log( + "Moving product to transaction table:", + editingProduct.ProductID, + ); + + // Toggle the sold status in the UI + setEditingProduct((prev) => ({ + ...prev, + isSold: !prev.isSold, + })); + + // You would add your API call here to update the backend + } catch (error) { + console.error("Error marking product as sold:", error); + } + }; + return (
@@ -120,12 +294,279 @@ const Selling = () => {
{showForm ? ( - setShowForm(false)} - /> +
+ {/* Back Button */} + + +

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

+ +
+ {/* Product Name */} +
+ + + setEditingProduct({ + ...editingProduct, + Name: e.target.value, + 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, + 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, index) => ( + + {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 + +
+ ))} +
+
+ )} + + {/* Show current image if editing */} + {editingProduct.image_url && ( +
+

Current image:

+
+ Current product +
+
+ )} +
+
+ + {/* Actions */} +
+ + + {editingProduct.ProductID && ( + + )} + + +
+
) : ( <> {products.length === 0 ? ( @@ -147,10 +588,7 @@ const Selling = () => { key={product.ProductID} to={`/product/${product.ProductID}`} > -
+
{product.image_url && product.image_url.length > 0 ? ( { ${product.Price}

- {product.categories && product.categories.length > 0 && ( + {product.CategoryID && (
- {product.CategoryID.map((category) => ( - - {category} - - ))} + + {getCategoryNameById(product.CategoryID) || + product.CategoryID} +
)} @@ -193,13 +627,21 @@ const Selling = () => {
+
+ +
+

+ {product.title} +

+ + ${product.price} + + +
+ + {product.category} +
+ +
+ {product.datePosted} + + {product.seller} + +
+
+ + ); + + // Scrollable product list component to reduce duplication + const ScrollableProductList = ({ + containerId, + products, + children, + isLoading, + addToHistory = false, + }) => ( +
+ {children} + +
+ + +
+ {isLoading ? ( + + ) : products.length > 0 ? ( + products.map((product) => ( + + )) + ) : ( +
+ No products available +
+ )} +
+ + +
+
+ ); + return (
{/* 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 @@ -223,290 +360,53 @@ const Home = () => {

- {/* Recent Listings */} + {/* Floating Alert */} {showAlert && ( setShowAlert(false)} /> )} -
+ + {/* Recommendations Section */} +

- Recommendation + Recommended For You

+
-
- {/* 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} - -
- -
-

- {recommended.title} -

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

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.length > 0 || isLoading.history) && ( + +

+ Your Browsing History +

+
)} -
-

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 */} + {/* Footer */}