selling Pg UI and Fav is done

This commit is contained in:
Mann Patel
2025-04-12 18:33:13 -06:00
parent 0f8bb622a4
commit 814c24c83f
5 changed files with 425 additions and 66 deletions

View File

@@ -1,13 +1,13 @@
const db = require("../utils/database"); const db = require("../utils/database");
exports.addFavorite = async (req, res) => { exports.addFavorite = async (req, res) => {
const { userID, productsID } = req.body; const { userID, productID } = req.body;
console.log(userID);
try { try {
// Use parameterized query to prevent SQL injection // Use parameterized query to prevent SQL injection
const [result] = await db.execute( const [result] = await db.execute(
"INSERT INTO Favorites (UserID, ProductID) VALUES (?, ?)", `INSERT INTO Favorites (UserID, ProductID) VALUES (?, ?)`,
[userID, productsID], [userID, productID],
); );
res.json({ res.json({

View File

@@ -0,0 +1,148 @@
import React from "react";
const ProductForm = ({
editingProduct,
setEditingProduct,
onSave,
onCancel,
}) => {
return (
<div className="bg-white border border-gray-300 rounded-lg p-6 shadow-md">
{/* Back Button */}
<button
onClick={onCancel}
className="mb-4 text-sm text-blue-600 hover:underline flex items-center"
>
Back to Listings
</button>
<h3 className="text-xl font-bold text-gray-800 mb-6">
{editingProduct?.id ? "Edit Your Product" : "List a New Product"}
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Product Name */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Product Name
</label>
<input
type="text"
value={editingProduct.name}
onChange={(e) =>
setEditingProduct({ ...editingProduct, name: e.target.value })
}
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500"
/>
</div>
{/* Price */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Price ($)
</label>
<input
type="number"
value={editingProduct.price}
onChange={(e) =>
setEditingProduct({
...editingProduct,
price: e.target.value,
})
}
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500"
/>
</div>
{/* Status */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Status
</label>
<select
value={editingProduct.status}
onChange={(e) =>
setEditingProduct({
...editingProduct,
status: e.target.value,
})
}
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500"
>
<option value="Active">Active</option>
<option value="Inactive">Inactive</option>
</select>
</div>
{/* Images */}
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-2">
Product Images (15)
</label>
<input
type="file"
accept="image/*"
multiple
onChange={(e) => {
const files = Array.from(e.target.files).slice(0, 5);
setEditingProduct((prev) => ({
...prev,
images: [...prev.images, ...files].slice(0, 5),
}));
}}
className="w-full px-4 py-2 border border-gray-300 rounded-md"
/>
<div className="flex flex-wrap gap-4 mt-4">
{editingProduct.images.length > 0 &&
editingProduct.images.map((img, idx) => (
<div
key={idx}
className="relative group w-24 h-24 border border-gray-300 overflow-hidden"
>
<img
src={URL.createObjectURL(img)}
alt={`Preview ${idx}`}
className="w-full h-full object-cover"
/>
<button
onClick={() => {
const updated = editingProduct.images.filter(
(_, i) => i !== idx,
);
setEditingProduct((prev) => ({
...prev,
images: updated,
}));
}}
className="absolute top-1 right-1 bg-white bg-opacity-90 rounded-full p-1 shadow hover:bg-red-500 hover:text-white transition-all text-gray-700 group-hover:opacity-100 opacity-0"
title="Remove image"
>
&times;
</button>
</div>
))}
</div>
</div>
</div>
{/* Actions */}
<div className="mt-6 flex justify-end gap-4">
<button
onClick={onCancel}
className="bg-gray-200 text-gray-700 px-5 py-2 rounded-md hover:bg-gray-300"
>
Cancel
</button>
<button
onClick={onSave}
className="bg-green-600 text-white px-6 py-2 rounded-md hover:bg-green-700"
>
{editingProduct.id ? "Update Product" : "Add Product"}
</button>
</div>
</div>
);
};
export default ProductForm;

View File

@@ -1,63 +1,113 @@
import { useState } from 'react'; import { useState, useEffect } from "react";
import { Link } from 'react-router-dom'; import { Link } from "react-router-dom";
import { Heart, Tag, Trash2, Filter, ChevronDown } from 'lucide-react'; import { Heart, Tag, Trash2, Filter, ChevronDown } from "lucide-react";
const Favorites = () => { const Favorites = () => {
const [favorites, setFavorites] = useState([ const [favorites, setFavorites] = useState([]);
{
id: 0,
title: 'Dell XPS 16 Laptop',
price: 850,
category: 'Electronics',
image: '/image1.avif',
condition: 'Like New',
seller: 'Michael T.',
datePosted: '5d ago',
dateAdded: '2023-03-08',
},
]);
const [showFilters, setShowFilters] = useState(false); const [showFilters, setShowFilters] = useState(false);
const [sortBy, setSortBy] = useState('dateAdded'); const [sortBy, setSortBy] = useState("dateAdded");
const [filterCategory, setFilterCategory] = useState('All'); const [filterCategory, setFilterCategory] = useState("All");
// Function to remove item from favorites const mapCategory = (id) => {
const removeFromFavorites = (id) => { const categories = {
setFavorites(favorites.filter(item => item.id !== id)); 1: "Electronics",
2: "Textbooks",
3: "Furniture",
4: "Clothing",
5: "Kitchen",
6: "Other",
};
return categories[id] || "Other";
}; };
// Available categories for filtering useEffect(() => {
const categories = ['All', 'Electronics', 'Textbooks', 'Furniture', 'Kitchen', 'Other']; const fetchFavorites = async () => {
try {
const user = JSON.parse(sessionStorage.getItem("user"));
const response = await fetch(
"http://localhost:3030/api/product/getFavorites",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ userID: user.ID }),
},
);
const data = await response.json();
console.log(user.ID);
console.log(data);
const favoritesData = data.favorites;
if (!Array.isArray(favoritesData)) {
console.error("Expected an array but got:", favoritesData);
return;
}
const transformed = favoritesData.map((item) => ({
id: item.ProductID,
title: item.Name,
price: parseFloat(item.Price),
category: mapCategory(item.CategoryID), // 👈 map numeric category to a string
image: item.image_url || "/default-image.jpg",
condition: "Used", // or another field if you add `Condition` to your DB
seller: item.SellerName,
datePosted: formatDatePosted(item.Date),
dateAdded: item.Date || new Date().toISOString(),
}));
setFavorites(transformed);
} catch (error) {
console.error("Failed to fetch favorites:", error);
}
};
fetchFavorites();
}, []);
const formatDatePosted = (dateString) => {
const postedDate = new Date(dateString);
const today = new Date();
const diffInMs = today - postedDate;
const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24));
return `${diffInDays}d ago`;
};
const removeFromFavorites = (id) => {
setFavorites(favorites.filter((item) => item.id !== id));
// Optional: Send DELETE request to backend here
};
// Sort favorites based on selected sort option
const sortedFavorites = [...favorites].sort((a, b) => { const sortedFavorites = [...favorites].sort((a, b) => {
if (sortBy === 'dateAdded') { if (sortBy === "dateAdded")
return new Date(b.dateAdded) - new Date(a.dateAdded); return new Date(b.dateAdded) - new Date(a.dateAdded);
} else if (sortBy === 'priceHigh') { if (sortBy === "priceHigh") return b.price - a.price;
return b.price - a.price; if (sortBy === "priceLow") return a.price - b.price;
} else if (sortBy === 'priceLow') {
return a.price - b.price;
}
return 0; return 0;
}); });
// Filter favorites based on selected category const filteredFavorites =
const filteredFavorites = filterCategory === 'All' filterCategory === "All"
? sortedFavorites ? sortedFavorites
: sortedFavorites.filter(item => item.category === filterCategory); : sortedFavorites.filter((item) => item.category === filterCategory);
// rest of the JSX remains unchanged...
return ( return (
<div className="max-w-6xl mx-auto"> <div className="max-w-6xl mx-auto">
<div className="flex justify-between items-center mb-6"> <div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold text-gray-800">My Favorites</h1> <h1 className="text-2xl font-bold text-gray-800">My Favorites</h1>
<button <button
className="flex items-center text-gray-600 hover:text-gray-800" className="flex items-center text-gray-600 hover:text-gray-800"
onClick={() => setShowFilters(!showFilters)} onClick={() => setShowFilters(!showFilters)}
> >
<Filter className="h-5 w-5 mr-1" /> <Filter className="h-5 w-5 mr-1" />
<span>Filter & Sort</span> <span>Filter & Sort</span>
<ChevronDown className={`h-4 w-4 ml-1 transition-transform ${showFilters ? 'rotate-180' : ''}`} /> <ChevronDown
className={`h-4 w-4 ml-1 transition-transform ${showFilters ? "rotate-180" : ""}`}
/>
</button> </button>
</div> </div>
@@ -89,7 +139,9 @@ const Favorites = () => {
className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500" className="w-full p-2 border border-gray-300 focus:outline-none focus:border-green-500"
> >
{categories.map((category) => ( {categories.map((category) => (
<option key={category} value={category}>{category}</option> <option key={category} value={category}>
{category}
</option>
))} ))}
</select> </select>
</div> </div>
@@ -101,12 +153,15 @@ const Favorites = () => {
{filteredFavorites.length === 0 ? ( {filteredFavorites.length === 0 ? (
<div className="bg-white border border-gray-200 p-8 text-center"> <div className="bg-white border border-gray-200 p-8 text-center">
<Heart className="h-12 w-12 text-gray-300 mx-auto mb-4" /> <Heart className="h-12 w-12 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-medium text-gray-700 mb-2">No favorites yet</h3> <h3 className="text-xl font-medium text-gray-700 mb-2">
No favorites yet
</h3>
<p className="text-gray-500 mb-4"> <p className="text-gray-500 mb-4">
Items you save will appear here. Start browsing to add items to your favorites. Items you save will appear here. Start browsing to add items to your
favorites.
</p> </p>
<Link <Link
to="/" to="/"
className="inline-block bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-4" className="inline-block bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-4"
> >
Browse Listings Browse Listings
@@ -115,7 +170,10 @@ const Favorites = () => {
) : ( ) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredFavorites.map((item) => ( {filteredFavorites.map((item) => (
<div key={item.id} className="bg-white border border-gray-200 hover:shadow-md transition-shadow relative"> <div
key={item.id}
className="bg-white border border-gray-200 hover:shadow-md transition-shadow relative"
>
<button <button
onClick={() => removeFromFavorites(item.id)} onClick={() => removeFromFavorites(item.id)}
className="absolute top-2 right-2 p-1 bg-white rounded-full shadow-sm text-red-500 hover:bg-red-50" className="absolute top-2 right-2 p-1 bg-white rounded-full shadow-sm text-red-500 hover:bg-red-50"
@@ -123,28 +181,38 @@ const Favorites = () => {
> >
<Trash2 className="h-5 w-5" /> <Trash2 className="h-5 w-5" />
</button> </button>
<Link to={`/product/${item.id}`}> <Link to={`/product/${item.id}`}>
<img src={item.image} alt={item.title} className="w-full h-48 object-cover" /> <img
src={item.image}
alt={item.title}
className="w-full h-48 object-cover"
/>
<div className="p-4"> <div className="p-4">
<div className="flex justify-between items-start mb-2"> <div className="flex justify-between items-start mb-2">
<h3 className="text-lg font-medium text-gray-800 leading-tight"> <h3 className="text-lg font-medium text-gray-800 leading-tight">
{item.title} {item.title}
</h3> </h3>
<span className="font-semibold text-green-600">${item.price}</span> <span className="font-semibold text-green-600">
${item.price}
</span>
</div> </div>
<div className="flex items-center text-sm text-gray-500 mb-3"> <div className="flex items-center text-sm text-gray-500 mb-3">
<Tag className="h-4 w-4 mr-1" /> <Tag className="h-4 w-4 mr-1" />
<span>{item.category}</span> <span>{item.category}</span>
<span className="mx-2"></span> <span className="mx-2"></span>
<span>{item.condition}</span> <span>{item.condition}</span>
</div> </div>
<div className="flex justify-between items-center pt-2 border-t border-gray-100"> <div className="flex justify-between items-center pt-2 border-t border-gray-100">
<span className="text-xs text-gray-500">Listed {item.datePosted}</span> <span className="text-xs text-gray-500">
<span className="text-sm font-medium text-gray-700">{item.seller}</span> Listed {item.datePosted}
</span>
<span className="text-sm font-medium text-gray-700">
{item.seller}
</span>
</div> </div>
</div> </div>
</Link> </Link>
@@ -156,12 +224,13 @@ const Favorites = () => {
{/* Show count if there are favorites */} {/* Show count if there are favorites */}
{filteredFavorites.length > 0 && ( {filteredFavorites.length > 0 && (
<div className="mt-6 text-sm text-gray-500"> <div className="mt-6 text-sm text-gray-500">
Showing {filteredFavorites.length} {filteredFavorites.length === 1 ? 'item' : 'items'} Showing {filteredFavorites.length}{" "}
{filterCategory !== 'All' && ` in ${filterCategory}`} {filteredFavorites.length === 1 ? "item" : "items"}
{filterCategory !== "All" && ` in ${filterCategory}`}
</div> </div>
)} )}
</div> </div>
); );
}; };
export default Favorites; export default Favorites;

View File

@@ -1,13 +1,155 @@
import { useState } from 'react'; import { useState } from "react";
import { Link } from 'react-router-dom'; import { Pencil, Trash2, Plus } from "lucide-react";
import { Tag, Book, Laptop, Sofa, Utensils, Gift, Heart } from 'lucide-react'; import ProductForm from "../components/ProductForm";
const Selling = () => { const Selling = () => {
const [products, setProducts] = useState([
{
id: 1,
name: "Green Sofa",
price: 299,
status: "Active",
images: [],
},
{
id: 2,
name: "Wooden Table",
price: 150,
status: "Inactive",
images: [],
},
]);
const [editingProduct, setEditingProduct] = useState(null);
const [view, setView] = useState("list"); // "list" or "form"
const handleEdit = (product) => {
setEditingProduct({ ...product });
setView("form");
};
const handleAddNew = () => {
setEditingProduct({
id: null,
name: "",
price: "",
status: "Active",
images: [],
});
setView("form");
};
const handleDelete = (id) => {
setProducts((prev) => prev.filter((p) => p.id !== id));
};
const handleSave = () => {
if (!editingProduct.name || !editingProduct.price) {
alert("Please enter a name and price.");
return;
}
if (editingProduct.images.length < 1) {
alert("Please upload at least one image.");
return;
}
if (editingProduct.id === null) {
const newProduct = {
...editingProduct,
id: Date.now(),
};
setProducts((prev) => [newProduct, ...prev]);
} else {
setProducts((prev) =>
prev.map((p) => (p.id === editingProduct.id ? editingProduct : p)),
);
}
setEditingProduct(null);
setView("list");
};
const handleCancel = () => {
setEditingProduct(null);
setView("list");
};
return ( return (
<div> <div className="p-4 max-w-4xl mx-auto">
{view === "list" && (
<>
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-semibold text-gray-800">My Listings</h2>
<button
onClick={handleAddNew}
className="bg-green-500 text-white px-4 py-2 hover:bg-green-600 transition-all"
>
<Plus className="inline-block mr-2" size={18} /> Add New Product
</button>
</div>
<ul className="space-y-4">
{products.map((product) => (
<li
key={product.id}
className="border border-gray-300 p-4 flex flex-col sm:flex-row justify-between items-start sm:items-center"
>
<div className="flex items-start sm:items-center space-x-4 w-full sm:w-auto">
<div className="h-20 w-20 bg-gray-100 flex items-center justify-center border border-gray-200 shrink-0">
{product.images.length > 0 ? (
<img
src={URL.createObjectURL(product.images[0])}
alt="Product"
className="h-full w-full object-cover"
/>
) : (
<span className="text-gray-400 text-sm">No Image</span>
)}
</div>
<div>
<p className="font-medium text-gray-800">{product.name}</p>
<p className="text-sm text-gray-600">${product.price}</p>
<p
className={`text-xs mt-1 ${
product.status === "Active"
? "text-green-600"
: "text-red-500"
}`}
>
{product.status}
</p>
</div>
</div>
<div className="flex space-x-2 mt-4 sm:mt-0">
<button
onClick={() => handleEdit(product)}
className="text-blue-600 hover:underline"
>
<Pencil size={18} />
</button>
<button
onClick={() => handleDelete(product.id)}
className="text-red-500 hover:underline"
>
<Trash2 size={18} />
</button>
</div>
</li>
))}
</ul>
</>
)}
{view === "form" && (
<ProductForm
editingProduct={editingProduct}
setEditingProduct={setEditingProduct}
onSave={handleSave}
onCancel={handleCancel}
/>
)}
</div> </div>
); );
}; };
export default Selling; export default Selling;

View File

@@ -102,7 +102,7 @@ CREATE TABLE Favorites (
ProductID INT, ProductID INT,
FOREIGN KEY (UserID) REFERENCES User (UserID), FOREIGN KEY (UserID) REFERENCES User (UserID),
FOREIGN KEY (ProductID) REFERENCES Product (ProductID), FOREIGN KEY (ProductID) REFERENCES Product (ProductID),
UNIQUE (UserID, ProductID) -- Prevents duplicate favorites UNIQUE (UserID, ProductID)
); );
-- Product-Category Junction Table (Many-to-Many) -- Product-Category Junction Table (Many-to-Many)