From b11416b342c512ac2b41748ab2ec46b93cfac8f8 Mon Sep 17 00:00:00 2001 From: Mann Patel <130435633+MannPatel0@users.noreply.github.com> Date: Tue, 18 Mar 2025 18:09:15 -0600 Subject: [PATCH] last update --- SQL_code/Schema.sql | 7 +- backend/index.js | 78 +++- frontend/src/App.jsx | 25 +- frontend/src/pages/Home.jsx | 63 +-- frontend/src/pages/MyListings.jsx | 550 +++++++++++++++++++++++++++ frontend/src/pages/ProductDetail.jsx | 202 ++++++---- frontend/src/pages/Settings.jsx | 3 +- 7 files changed, 821 insertions(+), 107 deletions(-) create mode 100644 frontend/src/pages/MyListings.jsx diff --git a/SQL_code/Schema.sql b/SQL_code/Schema.sql index 644a086..97eb86b 100644 --- a/SQL_code/Schema.sql +++ b/SQL_code/Schema.sql @@ -32,7 +32,10 @@ CREATE TABLE Product ( ); -- Category Entity -CREATE TABLE Category (CategoryID INT PRIMARY KEY, Name VARCHAR(255)); +CREATE TABLE Category ( + CategoryID INT PRIMARY KEY, + Name VARCHAR(255) NOT NULL +); -- Review Entity (Many-to-One with User, Many-to-One with Product) CREATE TABLE Review ( @@ -81,7 +84,7 @@ CREATE TABLE History ( -- Favorites Entity (Many-to-One with User, Many-to-One with Product) CREATE TABLE Favorites ( - FavoriteID INT PRIMARY KEY, + FavoriteID INT AUTO_INCREMENT PRIMARY KEY, UserID INT, ProductID INT, FOREIGN KEY (UserID) REFERENCES User (UserID), diff --git a/backend/index.js b/backend/index.js index 3aa7b6c..ecf858a 100644 --- a/backend/index.js +++ b/backend/index.js @@ -213,11 +213,6 @@ app.post("/complete-signup", (req, res) => { return res.status(500).json({ error: "Could not create role" }); } - db_con.query( - `SELECT * FROM User WHERE Email='${data.Email}'`, - (err, results) => {}, - ); - // Delete verification record db_con.query( `DELETE FROM AuthVerification WHERE Email = '${data.email}'`, @@ -228,7 +223,6 @@ app.post("/complete-signup", (req, res) => { res.json({ success: true, message: "User registration completed successfully", - userID: results.UserID, name: data.name, email: data.email, UCID: data.UCID, @@ -320,7 +314,7 @@ app.post("/find_user", (req, res) => { }); }); -//TODO: Update A uses Data: +//Update A uses Data: app.post("/update", (req, res) => { const { userId, ...updateData } = req.body; @@ -328,7 +322,7 @@ app.post("/update", (req, res) => { return res.status(400).json({ error: "User ID is required" }); } - // Create query dynamically based on provided fields + //query dynamically based on provided fields const updateFields = []; const values = []; @@ -398,6 +392,74 @@ app.post("/delete", (req, res) => { }); }); +app.post("/add_fav_product", (req, res) => { + const { userID, productsID } = req.body; + + // Use parameterized query to prevent SQL injection + db_con.query( + "INSERT INTO Favorites (UserID, ProductID) VALUES (?, ?)", + [userID, productsID], + (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", + }); + }, + ); +}); + +app.get("/get_product", (req, res) => { + const query = "SELECT * FROM Product"; + db_con.query(query, (err, data) => { + if (err) { + console.error("Error finding user:", err); + return res.status(500).json({ + found: false, + error: "Database error occurred", + }); + } + res.json({ + success: true, + message: "Product added to favorites successfully", + data, + }); + }); +}); + +// 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", +// }); +// }, +// ); + app.listen(3030, () => { console.log(`Running Backend on http://localhost:3030/`); console.log(`Send verification code: POST /send-verification`); diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 57c38bb..a194491 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -12,6 +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"; function App() { // Authentication state - initialize from localStorage if available @@ -161,11 +162,11 @@ function App() { } const result = await response.json(); + console.log(result); if (result.success) { // Create user object from API response const newUser = { - ID: result.userID, name: result.name || userData.name, email: result.email || userData.email, UCID: result.UCID || userData.ucid, @@ -640,7 +641,27 @@ function App() { } /> - + {/* Add new selling routes */} + +
+ +
+ + } + /> + +
+ +
+ + } + /> { const navigate = useNavigate(); - // Same categories - const categories = [ - { id: 1, name: "Textbooks", icon: }, - { id: 2, name: "Electronics", icon: }, - { id: 3, name: "Furniture", icon: }, - { id: 4, name: "Kitchen", icon: }, - { id: 5, name: "Other", icon: }, - ]; + const [listings, setListings] = useState([]); + const [error, setError] = useState(null); - // Same listings data - const [listings, setListings] = useState([ - { - id: 0, - title: "Dell XPS 16 Laptop", - price: 850, - category: "Electronics", - image: "image1.avif", - condition: "Good", - seller: "Michael T.", - datePosted: "5d ago", - isFavorite: true, - }, - ]); + useEffect(() => { + const fetchProducts = async () => { + try { + const response = await fetch("http://localhost:3030/get_product"); + 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.Name, + price: product.Price, + category: product.CategoryID, + image: product.ImageURL, + condition: "New", // Modify based on actual data + seller: "Unknown", // Modify if seller info is available + datePosted: "Just now", + isFavorite: false, + })), + ); + } else { + throw new Error(data.message || "Error fetching products"); + } + } catch (error) { + console.error("Error fetching products:", error); + setError(error.message); + } + }; + + fetchProducts(); + }, []); // Toggle favorite status const toggleFavorite = (id, e) => { e.preventDefault(); // Prevent navigation when clicking the heart icon - setListings( - listings.map((listing) => + setListings((prevListings) => + prevListings.map((listing) => listing.id === id ? { ...listing, isFavorite: !listing.isFavorite } : listing, diff --git a/frontend/src/pages/MyListings.jsx b/frontend/src/pages/MyListings.jsx new file mode 100644 index 0000000..6cafcc7 --- /dev/null +++ b/frontend/src/pages/MyListings.jsx @@ -0,0 +1,550 @@ +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 42f8304..6b32e8e 100644 --- a/frontend/src/pages/ProductDetail.jsx +++ b/frontend/src/pages/ProductDetail.jsx @@ -1,76 +1,93 @@ -import { useState } from 'react'; -import { useParams, Link } from 'react-router-dom'; -import { Heart, ArrowLeft, Tag, User, Calendar, Share, Flag } from 'lucide-react'; +import { useState } from "react"; +import { useParams, Link } from "react-router-dom"; +import { + Heart, + ArrowLeft, + Tag, + User, + Calendar, + Share, + Flag, +} from "lucide-react"; const ProductDetail = () => { const { id } = useParams(); const [isFavorite, setIsFavorite] = useState(false); const [showContactForm, setShowContactForm] = useState(false); - const [message, setMessage] = useState(''); + const [message, setMessage] = useState(""); const [currentImage, setCurrentImage] = useState(0); - + // Sample data for demonstration const product = [ { id: 0, - title: 'Dell XPS 13 Laptop - 2023 Model', + title: "Dell XPS 13 Laptop - 2023 Model", price: 850, - 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', - condition: 'Like New', - category: 'Electronics', - datePosted: '2023-03-02', + 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", + condition: "Like New", + category: + "Electronics, Electronics, Electronics, Electronics , Electronics , Electronics, Electronicss", + datePosted: "2023-03-02", images: [ - '/image1.avif', - '/image2.avif', - '/image3.avif' + "/image1.avif", + "/image2.avif", + "/image3.avif", + "/image3.avif", + "/image3.avif", ], seller: { - name: 'Michael T.', + name: "Michael T.", rating: 4.8, - memberSince: 'January 2022', - avatar: '/Profile.jpg' - } - }, - ]; + memberSince: "January 2022", + avatar: "/Profile.jpg", + }, + }, + ]; - console.log(product[id]) + console.log(product[id]); const toggleFavorite = () => { setIsFavorite(!isFavorite); }; - + const handleSendMessage = (e) => { e.preventDefault(); // TODO: this would send the message to the seller - console.log('Message sent:', message); - setMessage(''); + console.log("Message sent:", message); + setMessage(""); setShowContactForm(false); // Show confirmation or success message - alert('Message sent to seller!'); + alert("Message sent to seller!"); }; // Function to split description into paragraphs const formatDescription = (text) => { - return text.split('\n\n').map((paragraph, index) => ( + return text.split("\n\n").map((paragraph, index) => (

- {paragraph.split('\n').map((line, i) => ( + {paragraph.split("\n").map((line, i) => ( {line} - {i < paragraph.split('\n').length - 1 &&
} + {i < paragraph.split("\n").length - 1 &&
}
))}

)); }; - // Handle image navigation + // image navigation const nextImage = () => { - setCurrentImage((prev) => (prev === product.images.length - 1 ? 0 : prev + 1)); + setCurrentImage((prev) => + prev === product.images.length - 1 ? 0 : prev + 1, + ); }; const prevImage = () => { - setCurrentImage((prev) => (prev === 0 ? product.images.length - 1 : prev - 1)); + setCurrentImage((prev) => + prev === 0 ? product.images.length - 1 : prev - 1, + ); }; const selectImage = (index) => { @@ -81,7 +98,10 @@ const ProductDetail = () => {
{/* Breadcrumb & Back Link */}
- + Back to listings @@ -92,26 +112,26 @@ const ProductDetail = () => {
{/* Main Image */}
- {product[id].title}
- + {/* Thumbnail Images */} {product[id].images.length > 1 && (
{product[id].images.map((image, index) => ( -
selectImage(index)} > - {`${product[id].title}
@@ -125,21 +145,23 @@ const ProductDetail = () => { {/* Product Info Card */}
-

{product[id].title}

-
- +
${product[id].price}
- +
@@ -154,12 +176,12 @@ const ProductDetail = () => { Posted on {product[id].datePosted}
- + {/* Short Description */}

{product[id].shortDescription}

- + {/* Contact Button */} - + {/* TODO:Contact Form */} {showContactForm && (
-

Message Seller

+

+ Contact Seller +

- +
+ + setEmail(e.target.value)} + className="w-full p-3 border border-gray-300 focus:outline-none focus:border-green-500" + required + /> +
+
+ + setPhone(e.target.value)} + className="w-full p-3 border border-gray-300 focus:outline-none focus:border-green-500" + required + /> +
+
+ + setContactMessage(e.target.value)} + placeholder="Hi, is this item still available?" + className="w-full p-3 border border-gray-300 focus:outline-none focus:border-green-500" + /> +
)} - + {/* Seller Info */}
{product[id].seller.avatar ? ( - Seller ) : ( @@ -207,20 +266,25 @@ const ProductDetail = () => { )}
-

{product[id].seller.name}

-

Member since {product[id].seller.memberSince}

+

+ {product[id].seller.name} +

+

+ Member since {product[id].seller.memberSince} +

- Rating: {product[id].seller.rating}/5 + Rating:{" "} + {product[id].seller.rating}/5
- + {/* Description Section */}

Description

@@ -234,4 +298,4 @@ const ProductDetail = () => { ); }; -export default ProductDetail; \ No newline at end of file +export default ProductDetail; diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index f0754e2..bb09f74 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -189,7 +189,8 @@ const Settings = () => { } } catch (error) { console.error("Error deleting account:", error); - alert("Failed to delete account: " + error.message); + + alert("Cannot delete account, Please logout and retry:"); } } };