diff --git a/backend/controllers/category.js b/backend/controllers/category.js index 90cbd94..4764dbf 100644 --- a/backend/controllers/category.js +++ b/backend/controllers/category.js @@ -7,7 +7,7 @@ exports.getAllCategoriesWithPagination = async (req, res) => { try { const [data, _] = await db.execute( "SELECT * FROM Category C ORDER BY C.CategoryID ASC LIMIT ? OFFSET ?", - [limit.toString(), offset.toString()] + [limit.toString(), offset.toString()], ); const [result] = await db.execute("SELECT COUNT(*) AS count FROM Category"); @@ -24,7 +24,7 @@ exports.addCategory = async (req, res) => { try { const [result] = await db.execute( "INSERT INTO Category (Name) VALUES (?)", - [name] + [name], ); res.json({ message: "Adding new category successfully!" }); } catch (error) { @@ -38,10 +38,33 @@ exports.removeCategory = async (req, res) => { try { const [result] = await db.execute( `DELETE FROM Category WHERE CategoryID = ?`, - [id] + [id], ); res.json({ message: "Delete category successfully!" }); } catch (error) { res.json({ error: "Cannot remove category from database!" }); } }; + +exports.getAllCategory = async (req, res) => { + try { + const [data, fields] = await db.execute(`SELECT * FROM Category`); + + 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/controllers/product.js b/backend/controllers/product.js index a606fa2..44185a5 100644 --- a/backend/controllers/product.js +++ b/backend/controllers/product.js @@ -6,7 +6,7 @@ exports.addProduct = async (req, res) => { try { const [result] = await db.execute( `INSERT INTO Product (Name, Price, StockQuantity, UserID, Description, CategoryID) VALUES (?, ?, ?, ?, ?, ?)`, - [name, price, qty, userID, description, category] + [name, price, qty, userID, description, category], ); const productID = result.insertId; @@ -15,7 +15,7 @@ exports.addProduct = async (req, res) => { db.execute(`INSERT INTO Image_URL (URL, ProductID) VALUES (?, ?)`, [ imagePath, productID, - ]) + ]), ); await Promise.all(imageInsertPromises); //perallel @@ -32,14 +32,52 @@ 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] + [userID, productID], ); res.json({ @@ -59,7 +97,7 @@ exports.removeFavorite = async (req, res) => { // Use parameterized query to prevent SQL injection const [result] = await db.execute( `DELETE FROM Favorites WHERE UserID = ? AND ProductID = ?`, - [userID, productID] + [userID, productID], ); res.json({ @@ -72,6 +110,103 @@ 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; + + try { + const [result] = 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, + data: result, + }); + } 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; @@ -103,7 +238,7 @@ exports.getFavorites = async (req, res) => { p.Date, u.Name; `, - [userID] + [userID], ); res.json({ @@ -168,7 +303,7 @@ exports.getProductById = async (req, res) => { JOIN User U ON p.UserID = U.UserID WHERE p.ProductID = ? `, - [id] + [id], ); // Log raw data for debugging @@ -242,11 +377,11 @@ exports.getProductWithPagination = async (req, res) => { ORDER BY P.ProductID ASC LIMIT ? OFFSET ? `, - [limit.toString(), offset.toString()] + [limit.toString(), offset.toString()], ); const [result] = await db.execute( - `SELECT COUNT(*) AS totalProd FROM Product` + `SELECT COUNT(*) AS totalProd FROM Product`, ); const { totalProd } = result[0]; @@ -262,40 +397,10 @@ exports.removeProduct = async (req, res) => { try { const [result] = await db.execute( `DELETE FROM Product WHERE ProductID = ?`, - [id] + [id], ); res.json({ message: "Delete product successfully!" }); } catch (error) { res.json({ error: "Cannot remove product from database!" }); } }; - -// 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 796860f..b06f1fc 100644 --- a/backend/index.js +++ b/backend/index.js @@ -46,6 +46,7 @@ app.use("/api/history", history); app.use("/api/review", review); app.use("/api/category", categoryRouter); app.use("/api/transaction", transactionRouter); +app.use("/api/category", categoryRouter); // Set up a scheduler to run cleanup every hour clean_up_time = 30 * 60 * 1000; diff --git a/backend/routes/category.js b/backend/routes/category.js index 9a04a08..49ebdcf 100644 --- a/backend/routes/category.js +++ b/backend/routes/category.js @@ -3,6 +3,7 @@ const { getAllCategoriesWithPagination, addCategory, removeCategory, + getAllCategory, } = require("../controllers/category"); const router = express.Router(); @@ -10,5 +11,6 @@ const router = express.Router(); router.get("/getCategories", getAllCategoriesWithPagination); router.post("/addCategory", addCategory); router.delete("/:id", removeCategory); +router.get("/", getAllCategory); module.exports = router; diff --git a/backend/routes/product.js b/backend/routes/product.js index 4b59e25..09000cd 100644 --- a/backend/routes/product.js +++ b/backend/routes/product.js @@ -9,6 +9,8 @@ const { addProduct, removeProduct, getProductWithPagination, + myProduct, + updateProduct, } = require("../controllers/product"); const router = express.Router(); @@ -22,6 +24,8 @@ 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); @@ -32,4 +36,6 @@ router.get("/getProductWithPagination", getProductWithPagination); router.get("/:id", getProductById); // Simplified route +router.put("/update/:productId", updateProduct); + module.exports = router; diff --git a/backend/utils/database.js b/backend/utils/database.js index 643b1f9..ffc5ef3 100644 --- a/backend/utils/database.js +++ b/backend/utils/database.js @@ -7,4 +7,8 @@ const pool = mysql.createPool({ password: "12345678", }); +// 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/frontend/public/20191227_012601_0000.png b/frontend/public/20191227_012601_0000.png deleted file mode 100644 index e4b0445..0000000 Binary files a/frontend/public/20191227_012601_0000.png and /dev/null differ diff --git a/frontend/public/Pictures/Acoustic-Guitar.jpg b/frontend/public/Uploads/Acoustic-Guitar.jpg similarity index 100% rename from frontend/public/Pictures/Acoustic-Guitar.jpg rename to frontend/public/Uploads/Acoustic-Guitar.jpg diff --git a/frontend/public/Pictures/Backpack.jpg b/frontend/public/Uploads/Backpack.jpg similarity index 100% rename from frontend/public/Pictures/Backpack.jpg rename to frontend/public/Uploads/Backpack.jpg diff --git a/frontend/public/Pictures/Basketball.jpg b/frontend/public/Uploads/Basketball.jpg similarity index 100% rename from frontend/public/Pictures/Basketball.jpg rename to frontend/public/Uploads/Basketball.jpg diff --git a/frontend/public/Pictures/Bluetooth-Speaker.jpg b/frontend/public/Uploads/Bluetooth-Speaker.jpg similarity index 100% rename from frontend/public/Pictures/Bluetooth-Speaker.jpg rename to frontend/public/Uploads/Bluetooth-Speaker.jpg diff --git a/frontend/public/Pictures/CS-Textbook.jpg b/frontend/public/Uploads/CS-Textbook.jpg similarity index 100% rename from frontend/public/Pictures/CS-Textbook.jpg rename to frontend/public/Uploads/CS-Textbook.jpg diff --git a/frontend/public/Pictures/Calculator.jpg b/frontend/public/Uploads/Calculator.jpg similarity index 100% rename from frontend/public/Pictures/Calculator.jpg rename to frontend/public/Uploads/Calculator.jpg diff --git a/frontend/public/Pictures/Calculus-Textbook.jpg b/frontend/public/Uploads/Calculus-Textbook.jpg similarity index 100% rename from frontend/public/Pictures/Calculus-Textbook.jpg rename to frontend/public/Uploads/Calculus-Textbook.jpg diff --git a/frontend/public/Pictures/Calculus-Textbook2.jpg b/frontend/public/Uploads/Calculus-Textbook2.jpg similarity index 100% rename from frontend/public/Pictures/Calculus-Textbook2.jpg rename to frontend/public/Uploads/Calculus-Textbook2.jpg diff --git a/frontend/public/Pictures/Calculus-Textbook3.jpg b/frontend/public/Uploads/Calculus-Textbook3.jpg similarity index 100% rename from frontend/public/Pictures/Calculus-Textbook3.jpg rename to frontend/public/Uploads/Calculus-Textbook3.jpg diff --git a/frontend/public/Pictures/Controller.jpg b/frontend/public/Uploads/Controller.jpg similarity index 100% rename from frontend/public/Pictures/Controller.jpg rename to frontend/public/Uploads/Controller.jpg diff --git a/frontend/public/Pictures/Dell1.jpg b/frontend/public/Uploads/Dell1.jpg similarity index 100% rename from frontend/public/Pictures/Dell1.jpg rename to frontend/public/Uploads/Dell1.jpg diff --git a/frontend/public/Pictures/Dell2.jpg b/frontend/public/Uploads/Dell2.jpg similarity index 100% rename from frontend/public/Pictures/Dell2.jpg rename to frontend/public/Uploads/Dell2.jpg diff --git a/frontend/public/Pictures/Dell3.jpg b/frontend/public/Uploads/Dell3.jpg similarity index 100% rename from frontend/public/Pictures/Dell3.jpg rename to frontend/public/Uploads/Dell3.jpg diff --git a/frontend/public/Pictures/Desk-Lamp.jpg b/frontend/public/Uploads/Desk-Lamp.jpg similarity index 100% rename from frontend/public/Pictures/Desk-Lamp.jpg rename to frontend/public/Uploads/Desk-Lamp.jpg diff --git a/frontend/public/Pictures/Dorm-Desk.jpg b/frontend/public/Uploads/Dorm-Desk.jpg similarity index 100% rename from frontend/public/Pictures/Dorm-Desk.jpg rename to frontend/public/Uploads/Dorm-Desk.jpg diff --git a/frontend/public/Pictures/HP-Calculator.jpg b/frontend/public/Uploads/HP-Calculator.jpg similarity index 100% rename from frontend/public/Pictures/HP-Calculator.jpg rename to frontend/public/Uploads/HP-Calculator.jpg diff --git a/frontend/public/Pictures/HP-Laptop1.jpg b/frontend/public/Uploads/HP-Laptop1.jpg similarity index 100% rename from frontend/public/Pictures/HP-Laptop1.jpg rename to frontend/public/Uploads/HP-Laptop1.jpg diff --git a/frontend/public/Pictures/HP-Laptop2.jpg b/frontend/public/Uploads/HP-Laptop2.jpg similarity index 100% rename from frontend/public/Pictures/HP-Laptop2.jpg rename to frontend/public/Uploads/HP-Laptop2.jpg diff --git a/frontend/public/Pictures/Lab-Coat.jpg b/frontend/public/Uploads/Lab-Coat.jpg similarity index 100% rename from frontend/public/Pictures/Lab-Coat.jpg rename to frontend/public/Uploads/Lab-Coat.jpg diff --git a/frontend/public/Pictures/Mini-Fridge.jpg b/frontend/public/Uploads/Mini-Fridge.jpg similarity index 100% rename from frontend/public/Pictures/Mini-Fridge.jpg rename to frontend/public/Uploads/Mini-Fridge.jpg diff --git a/frontend/public/Pictures/Mountain-Bike.jpg b/frontend/public/Uploads/Mountain-Bike.jpg similarity index 100% rename from frontend/public/Pictures/Mountain-Bike.jpg rename to frontend/public/Uploads/Mountain-Bike.jpg diff --git a/frontend/public/Pictures/Physics-Textbook.jpg b/frontend/public/Uploads/Physics-Textbook.jpg similarity index 100% rename from frontend/public/Pictures/Physics-Textbook.jpg rename to frontend/public/Uploads/Physics-Textbook.jpg diff --git a/frontend/public/Pictures/University-Hoodie.jpg b/frontend/public/Uploads/University-Hoodie.jpg similarity index 100% rename from frontend/public/Pictures/University-Hoodie.jpg rename to frontend/public/Uploads/University-Hoodie.jpg diff --git a/frontend/public/Pictures/Winter-Jacket.jpg b/frontend/public/Uploads/Winter-Jacket.jpg similarity index 100% rename from frontend/public/Pictures/Winter-Jacket.jpg rename to frontend/public/Uploads/Winter-Jacket.jpg diff --git a/frontend/public/Pictures/Wireless-Mouse.jpg b/frontend/public/Uploads/Wireless-Mouse.jpg similarity index 100% rename from frontend/public/Pictures/Wireless-Mouse.jpg rename to frontend/public/Uploads/Wireless-Mouse.jpg diff --git a/frontend/public/Pictures/Yoga-Mat.jpg b/frontend/public/Uploads/Yoga-Mat.jpg similarity index 100% rename from frontend/public/Pictures/Yoga-Mat.jpg rename to frontend/public/Uploads/Yoga-Mat.jpg diff --git a/frontend/public/market.png b/frontend/public/market.png deleted file mode 100644 index 9bfc35c..0000000 Binary files a/frontend/public/market.png and /dev/null differ diff --git a/frontend/public/university-of-calgary-logo.png b/frontend/public/university-of-calgary-logo.png deleted file mode 100644 index 875b098..0000000 Binary files a/frontend/public/university-of-calgary-logo.png and /dev/null differ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index ac28ad5..38d0009 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -37,7 +37,10 @@ function App() { const [error, setError] = useState(""); const [isLoading, setIsLoading] = useState(false); - const [userId, setUserId] = useState(null); + // Product recommendation states + const [isGeneratingRecommendations, setIsGeneratingRecommendations] = + useState(false); + const [recommendations, setRecommendations] = useState([]); // New verification states const [verificationStep, setVerificationStep] = useState("initial"); // 'initial', 'code-sent', 'verifying' @@ -60,8 +63,69 @@ function App() { }, []); useEffect(() => { - sendSessionDataToServer(); - }, []); + if (isAuthenticated && user) { + sendSessionDataToServer(); + } + }, [isAuthenticated, user]); + + // Generate product recommendations when user logs in + useEffect(() => { + if (isAuthenticated && user) { + generateProductRecommendations(); + } + }, [isAuthenticated, user]); + + // Generate product recommendations + const generateProductRecommendations = async () => { + try { + setIsGeneratingRecommendations(true); + + // Add a short delay to simulate calculation time + await new Promise((resolve) => setTimeout(resolve, 500)); + + console.log("Generating product recommendations for user:", user.ID); + + // Make API call to get recommendations + const response = await fetch( + "http://localhost:3030/api/recommendations/generate", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + userId: user.ID, + }), + }, + ); + + if (!response.ok) { + throw new Error("Failed to generate recommendations"); + } + + const result = await response.json(); + + if (result.success) { + console.log( + "Recommendations generated successfully:", + result.recommendations, + ); + setRecommendations(result.recommendations); + + // Store recommendations in session storage for access across the app + sessionStorage.setItem( + "userRecommendations", + JSON.stringify(result.recommendations), + ); + } else { + console.error("Error generating recommendations:", result.message); + } + } catch (err) { + console.error("Error generating product recommendations:", err); + } finally { + setIsGeneratingRecommendations(false); + } + }; const [isAdmin, setIsAdmin] = useState(false); const [showAdminDashboard, setShowAdminDashboard] = useState(false); @@ -103,7 +167,7 @@ function App() { email: userData.email, // Add any other required fields }), - } + }, ); if (!response.ok) { @@ -152,7 +216,7 @@ function App() { email: tempUserData.email, code: code, }), - } + }, ); if (!response.ok) { @@ -196,7 +260,7 @@ function App() { "Content-Type": "application/json", }, body: JSON.stringify(userData), - } + }, ); if (!response.ok) { @@ -209,6 +273,7 @@ function App() { if (result.success) { // Create user object from API response const newUser = { + ID: result.userID || result.ID, name: result.name || userData.name, email: result.email || userData.email, UCID: result.UCID || userData.ucid, @@ -223,13 +288,17 @@ function App() { sessionStorage.setItem("user", JSON.stringify(newUser)); // After successful signup, send session data to server - sendSessionDataToServer(); // Call it after signup + sendSessionDataToServer(); // Reset verification steps setVerificationStep("initial"); setTempUserData(null); console.log("Signup completed successfully"); + + // Generate recommendations for the new user + generateProductRecommendations(); + return true; } else { setError(result.message || "Failed to complete signup"); @@ -302,7 +371,7 @@ function App() { email: formValues.email, password: formValues.password, }), - } + }, ); if (!response.ok) { @@ -328,9 +397,11 @@ function App() { sessionStorage.setItem("isAuthenticated", "true"); sessionStorage.setItem("user", JSON.stringify(userObj)); - sessionStorage.getItem("user"); - console.log("Login successful for:", userData.email); + + // Start generating recommendations with a slight delay + // This will happen in the useEffect, but we set a loading state to show to the user + setIsGeneratingRecommendations(true); } else { // Show error message for invalid credentials setError("Invalid email or password"); @@ -364,11 +435,12 @@ function App() { setUser(null); setVerificationStep("initial"); setTempUserData(null); + setRecommendations([]); // Clear localStorage - // sessionStorage.removeItem("user"); sessionStorage.removeItem("isAuthenticated"); + sessionStorage.removeItem("userRecommendations"); console.log("User logged out"); }; @@ -396,8 +468,6 @@ function App() { 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"); @@ -411,8 +481,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", @@ -434,6 +502,13 @@ function App() { } }; + // Loading overlay component + const LoadingOverlay = () => ( +
+
+
+ ); + // Login component const LoginComponent = () => (
@@ -607,8 +682,8 @@ function App() { {isLoading ? "Please wait..." : isSignUp - ? "Create Account" - : "Sign In"} + ? "Create Account" + : "Sign In"}
@@ -725,6 +800,9 @@ function App() { return (
+ {/* Show loading overlay when generating recommendations */} + {isGeneratingRecommendations && } + {/* Only show navbar when authenticated */} {isAuthenticated && (
- +
} diff --git a/frontend/src/components/ProductForm.jsx b/frontend/src/components/ProductForm.jsx deleted file mode 100644 index 32ed01b..0000000 --- a/frontend/src/components/ProductForm.jsx +++ /dev/null @@ -1,410 +0,0 @@ -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/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/Home.jsx b/frontend/src/pages/Home.jsx index 22dc205..594148a 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -1,12 +1,6 @@ -import { useState, useEffect } from "react"; import { Link, useNavigate } from "react-router-dom"; -import { - Tag, - ChevronLeft, - ChevronRight, - Bookmark, - BookmarkCheck, -} from "lucide-react"; +import { useState, useEffect, useRef } from "react"; +import { Tag, ChevronLeft, ChevronRight, Bookmark, Loader } from "lucide-react"; import FloatingAlert from "../components/FloatingAlert"; // adjust path if needed @@ -14,38 +8,50 @@ const Home = () => { const navigate = useNavigate(); const [listings, setListings] = useState([]); const [recommended, setRecommended] = useState([]); - const [history, sethistory] = useState([]); + const [history, setHistory] = useState([]); const [error, setError] = useState(null); const [showAlert, setShowAlert] = useState(false); + const [isLoading, setIsLoading] = useState({ + recommendations: true, + listings: true, + history: true, + }); + const recommendationsFetched = useRef(false); + const historyFetched = useRef(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", + try { + 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, + }), }, - body: JSON.stringify({ - userID: storedUser.ID, - productID: id, - }), - }, - ); - const data = await response.json(); - if (data.success) { - setShowAlert(true); + ); + const data = await response.json(); + if (data.success) { + setShowAlert(true); + // Close alert after 3 seconds + setTimeout(() => setShowAlert(false), 3000); + } + console.log(`Add Product -> Favorites: ${id}`); + } catch (error) { + console.error("Error adding favorite:", error); } - console.log(`Add Product -> History: ${id}`); }; const addHistory = async (id) => { - const response = await fetch( - "http://localhost:3030/api/history/addHistory", - { + try { + await fetch("http://localhost:3030/api/history/addHistory", { method: "POST", headers: { "Content-Type": "application/json", @@ -54,23 +60,23 @@ const Home = () => { userID: storedUser.ID, productID: id, }), - }, - ); + }); + } catch (error) { + console.error("Error adding to history:", error); + } }; - 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(); - + // Fetch recommended products useEffect(() => { - const fetchrecomProducts = async () => { + const fetchRecommendedProducts = async () => { + // Skip if already fetched or no user data + if (recommendationsFetched.current || !storedUser || !storedUser.ID) + return; + + setIsLoading((prev) => ({ ...prev, recommendations: true })); try { + recommendationsFetched.current = true; // Mark as fetched before the API call + const response = await fetch( "http://localhost:3030/api/engine/recommended", { @@ -83,36 +89,42 @@ const Home = () => { }), }, ); - if (!response.ok) throw new Error("Failed to fetch products"); + if (!response.ok) throw new Error("Failed to fetch recommendations"); const data = await response.json(); if (data.success) { setRecommended( data.data.map((product) => ({ id: product.ProductID, - title: product.ProductName, // Use the alias from SQL + title: product.ProductName, 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 + category: product.Category, + image: product.ProductImage, + seller: product.SellerName, + datePosted: product.DateUploaded, + isFavorite: false, })), ); - reloadPage(); } else { - throw new Error(data.message || "Error fetching products"); + throw new Error(data.message || "Error fetching recommendations"); } } catch (error) { - console.error("Error fetching products:", error); + console.error("Error fetching recommendations:", error); setError(error.message); + // Reset the flag if there's an error so it can try again + recommendationsFetched.current = false; + } finally { + setIsLoading((prev) => ({ ...prev, recommendations: false })); } }; - fetchrecomProducts(); - }, []); + fetchRecommendedProducts(); + }, [storedUser]); // Keep dependency + + // Fetch all products useEffect(() => { const fetchProducts = async () => { + setIsLoading((prev) => ({ ...prev, listings: true })); try { const response = await fetch( "http://localhost:3030/api/product/getProduct", @@ -120,18 +132,17 @@ const Home = () => { if (!response.ok) throw new Error("Failed to fetch products"); const data = await response.json(); - if (data.success) { setListings( data.data.map((product) => ({ id: product.ProductID, - title: product.ProductName, // Use the alias from SQL + title: product.ProductName, 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 + category: product.Category, + image: product.ProductImage, + seller: product.SellerName, + datePosted: product.DateUploaded, + isFavorite: false, })), ); } else { @@ -140,15 +151,24 @@ const Home = () => { } catch (error) { console.error("Error fetching products:", error); setError(error.message); + } finally { + setIsLoading((prev) => ({ ...prev, listings: false })); } }; + fetchProducts(); }, []); + // Fetch user history useEffect(() => { - const fetchrecomProducts = async () => { - // Get the user's data from localStorage + const fetchUserHistory = async () => { + // Skip if already fetched or no user data + if (historyFetched.current || !storedUser || !storedUser.ID) return; + + setIsLoading((prev) => ({ ...prev, history: true })); try { + historyFetched.current = true; // Mark as fetched before the API call + const response = await fetch( "http://localhost:3030/api/history/getHistory", { @@ -161,52 +181,168 @@ const Home = () => { }), }, ); - if (!response.ok) throw new Error("Failed to fetch products"); + if (!response.ok) throw new Error("Failed to fetch history"); const data = await response.json(); if (data.success) { - sethistory( + setHistory( data.data.map((product) => ({ id: product.ProductID, - title: product.ProductName, // Use the alias from SQL + title: product.ProductName, 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 + category: product.Category, + image: product.ProductImage, + seller: product.SellerName, + datePosted: product.DateUploaded, })), ); } else { - throw new Error(data.message || "Error fetching products"); + throw new Error(data.message || "Error fetching history"); } } catch (error) { - console.error("Error fetching products:", error); + console.error("Error fetching history:", error); setError(error.message); + // Reset the flag if there's an error so it can try again + historyFetched.current = false; + } finally { + setIsLoading((prev) => ({ ...prev, history: false })); } }; - fetchrecomProducts(); - }, []); + + fetchUserHistory(); + }, [storedUser]); // Keep dependency const handleSelling = () => { navigate("/selling"); }; + // Loading indicator component + const LoadingSection = () => ( +
+ +
+ ); + + // Product card component to reduce duplication + const ProductCard = ({ product, addToHistory = false }) => ( + addHistory(product.id) : undefined} + className="bg-white border border-gray-200 hover:shadow-md transition-shadow w-70 flex-shrink-0 relative" + > +
+ {product.title} + +
+ +
+

+ {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 @@ -217,297 +353,60 @@ 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 */}