update to listingspage
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 23 KiB |
@@ -12,7 +12,6 @@ import Selling from "./pages/Selling";
|
||||
import Transactions from "./pages/Transactions";
|
||||
import Favorites from "./pages/Favorites";
|
||||
import ProductDetail from "./pages/ProductDetail";
|
||||
import ItemForm from "./pages/MyListings";
|
||||
import SearchPage from "./pages/SearchPage"; // Make sure to import the SearchPage
|
||||
|
||||
function App() {
|
||||
@@ -712,27 +711,6 @@ function App() {
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
{/* Add new selling routes */}
|
||||
<Route
|
||||
path="/selling/create"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
<ItemForm />
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/selling/edit/:id"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
<ItemForm />
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/transactions"
|
||||
element={
|
||||
|
||||
@@ -35,7 +35,7 @@ const Navbar = ({ onLogout, userName }) => {
|
||||
alt="Campus Plug"
|
||||
className="h-8 px-2"
|
||||
/>
|
||||
<span className="hidden md:block text-green-600 font-bold text-xl">
|
||||
<span className="hidden md:block [color:#009966] font-bold text-xl">
|
||||
Campus Plug
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
const ProductForm = ({
|
||||
editingProduct,
|
||||
@@ -6,17 +6,60 @@ const ProductForm = ({
|
||||
onSave,
|
||||
onCancel,
|
||||
}) => {
|
||||
const [selectedCategory, setSelectedCategory] = useState("");
|
||||
|
||||
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",
|
||||
];
|
||||
|
||||
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 (
|
||||
<div className="bg-white border border-gray-300 rounded-lg p-6 shadow-md">
|
||||
<div className="bg-white border-2 border-gray-200 rounded-md p-6 shadow-lg">
|
||||
{/* Back Button */}
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="mb-4 text-sm text-blue-600 hover:underline flex items-center"
|
||||
className="mb-4 text-sm text-emerald-600 hover:text-emerald-800 flex items-center font-medium"
|
||||
>
|
||||
← Back to Listings
|
||||
</button>
|
||||
|
||||
<h3 className="text-xl font-bold text-gray-800 mb-6">
|
||||
<h3 className="text-xl font-bold text-gray-800 mb-6 border-b-2 border-gray-100 pb-3">
|
||||
{editingProduct?.id ? "Edit Your Product" : "List a New Product"}
|
||||
</h3>
|
||||
|
||||
@@ -32,7 +75,7 @@ const ProductForm = ({
|
||||
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"
|
||||
className="w-full px-4 py-2 border-2 border-gray-200 rounded-md focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -50,11 +93,71 @@ const ProductForm = ({
|
||||
price: e.target.value,
|
||||
})
|
||||
}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500"
|
||||
className="w-full px-4 py-2 border-2 border-gray-200 rounded-md focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
{/* Categories - Dropdown with Add button */}
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Categories
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
value={selectedCategory}
|
||||
onChange={(e) => setSelectedCategory(e.target.value)}
|
||||
className="flex-1 px-4 py-2 border-2 border-gray-200 rounded-md focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
|
||||
>
|
||||
<option value="" disabled>
|
||||
Select a category
|
||||
</option>
|
||||
{categories
|
||||
.filter(
|
||||
(cat) => !(editingProduct.categories || []).includes(cat),
|
||||
)
|
||||
.map((category) => (
|
||||
<option key={category} value={category}>
|
||||
{category}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addCategory}
|
||||
disabled={!selectedCategory}
|
||||
className="px-4 py-2 bg-emerald-600 text-white rounded-md hover:bg-emerald-700 disabled:bg-gray-300 disabled:cursor-not-allowed"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Selected Categories */}
|
||||
{(editingProduct.categories || []).length > 0 ? (
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{(editingProduct.categories || []).map((category) => (
|
||||
<span
|
||||
key={category}
|
||||
className="inline-flex items-center px-3 py-1 rounded-md text-sm font-medium bg-emerald-100 text-emerald-800"
|
||||
>
|
||||
{category}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeCategory(category)}
|
||||
className="ml-2 text-emerald-600 hover:text-emerald-800"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
Please select at least one category
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Status - Updated to Unsold/Sold */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Status
|
||||
@@ -67,18 +170,40 @@ const ProductForm = ({
|
||||
status: e.target.value,
|
||||
})
|
||||
}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-green-500"
|
||||
className="w-full px-4 py-2 border-2 border-gray-200 rounded-md focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
|
||||
>
|
||||
<option value="Active">Active</option>
|
||||
<option value="Inactive">Inactive</option>
|
||||
<option value="Unsold">Unsold</option>
|
||||
<option value="Sold">Sold</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Images */}
|
||||
{/* Description - New Field */}
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Product Images (1–5)
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
value={editingProduct.description || ""}
|
||||
onChange={(e) =>
|
||||
setEditingProduct({
|
||||
...editingProduct,
|
||||
description: e.target.value,
|
||||
})
|
||||
}
|
||||
rows="4"
|
||||
className="w-full px-4 py-2 border-2 border-gray-200 rounded-md focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
|
||||
placeholder="Describe your product in detail..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
{/* Simplified Image Upload */}
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Product Images{" "}
|
||||
<span className="text-gray-500 text-sm">(Max 5)</span>
|
||||
</label>
|
||||
|
||||
{/* Simple file input */}
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@@ -90,53 +215,80 @@ const ProductForm = ({
|
||||
images: [...prev.images, ...files].slice(0, 5),
|
||||
}));
|
||||
}}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-md"
|
||||
className="hidden"
|
||||
id="image-upload"
|
||||
/>
|
||||
<label
|
||||
htmlFor="image-upload"
|
||||
className="block w-full p-3 border-2 border-dashed border-emerald-200 bg-emerald-50 rounded-md text-center cursor-pointer hover:bg-emerald-100 transition-colors"
|
||||
>
|
||||
<span className="text-emerald-700 font-medium">
|
||||
Click to upload images
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div className="flex flex-wrap gap-4 mt-4">
|
||||
{editingProduct.images.length > 0 &&
|
||||
editingProduct.images.map((img, idx) => (
|
||||
{/* Image previews */}
|
||||
{editingProduct.images.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<p className="text-sm text-gray-600 mb-2">
|
||||
{editingProduct.images.length}{" "}
|
||||
{editingProduct.images.length === 1 ? "image" : "images"}{" "}
|
||||
selected
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{editingProduct.images.map((img, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="relative group w-24 h-24 border border-gray-300 overflow-hidden"
|
||||
className="relative w-20 h-20 border-2 border-gray-200 rounded-md overflow-hidden"
|
||||
>
|
||||
<img
|
||||
src={URL.createObjectURL(img)}
|
||||
alt={`Preview ${idx}`}
|
||||
alt={`Product image ${idx + 1}`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
const updated = editingProduct.images.filter(
|
||||
(_, i) => i !== idx,
|
||||
);
|
||||
const updated = [...editingProduct.images];
|
||||
updated.splice(idx, 1);
|
||||
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"
|
||||
className="absolute top-0 right-0 bg-white bg-opacity-80 w-6 h-6 flex items-center justify-center text-gray-700 hover:text-red-600"
|
||||
title="Remove image"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{editingProduct.images.length > 0 && (
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingProduct((prev) => ({ ...prev, images: [] }))
|
||||
}
|
||||
className="text-sm text-red-600 hover:text-red-800 mt-2"
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="mt-6 flex justify-end gap-4">
|
||||
<div className="mt-8 flex justify-end gap-4 border-t-2 border-gray-100 pt-4">
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="bg-gray-200 text-gray-700 px-5 py-2 rounded-md hover:bg-gray-300"
|
||||
className="bg-gray-100 text-gray-700 px-6 py-2 rounded-md hover:bg-gray-200 font-medium"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={onSave}
|
||||
className="bg-green-600 text-white px-6 py-2 rounded-md hover:bg-green-700"
|
||||
className="bg-emerald-600 text-white px-8 py-2 rounded-md hover:bg-emerald-700 font-medium"
|
||||
>
|
||||
{editingProduct.id ? "Update Product" : "Add Product"}
|
||||
</button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Heart, Tag, Trash2, Filter, ChevronDown } from "lucide-react";
|
||||
import { Heart, Tag, Trash2 } from "lucide-react";
|
||||
|
||||
const Favorites = () => {
|
||||
const [favorites, setFavorites] = useState([]);
|
||||
|
||||
@@ -1,550 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Link, useParams, useNavigate } from "react-router-dom";
|
||||
import { ArrowLeft, Plus, X, Save, Trash } from "lucide-react";
|
||||
|
||||
const ItemForm = () => {
|
||||
const { id } = useParams(); // If id exists, we are editing, otherwise creating
|
||||
const navigate = useNavigate();
|
||||
const isEditing = !!id;
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
title: "",
|
||||
price: "",
|
||||
category: "",
|
||||
condition: "",
|
||||
shortDescription: "",
|
||||
description: "",
|
||||
images: [],
|
||||
status: "active",
|
||||
});
|
||||
|
||||
const [originalData, setOriginalData] = useState(null);
|
||||
const [errors, setErrors] = useState({});
|
||||
const [imagePreviewUrls, setImagePreviewUrls] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(isEditing);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
|
||||
// Categories with icons
|
||||
const categories = [
|
||||
"Electronics",
|
||||
"Furniture",
|
||||
"Books",
|
||||
"Kitchen",
|
||||
"Collectibles",
|
||||
"Clothing",
|
||||
"Sports & Outdoors",
|
||||
"Tools",
|
||||
"Toys & Games",
|
||||
"Other",
|
||||
];
|
||||
|
||||
// Condition options
|
||||
const conditions = ["New", "Like New", "Good", "Fair", "Poor"];
|
||||
|
||||
// Status options
|
||||
const statuses = ["active", "inactive", "sold", "pending"];
|
||||
|
||||
// Fetch item data if editing
|
||||
useEffect(() => {
|
||||
if (isEditing) {
|
||||
// This would be an API call in a real app
|
||||
// Simulating API call with timeout
|
||||
setTimeout(() => {
|
||||
// Sample data for item being edited
|
||||
const itemData = {
|
||||
id: parseInt(id),
|
||||
title: "Dell XPS 13 Laptop - 2023 Model",
|
||||
price: 850,
|
||||
category: "Electronics",
|
||||
condition: "Like New",
|
||||
shortDescription:
|
||||
"Dell XPS 13 laptop in excellent condition. Intel Core i7, 16GB RAM, 512GB SSD. Includes charger and original box.",
|
||||
description:
|
||||
"Selling my Dell XPS 13 laptop. Only 6 months old and in excellent condition. Intel Core i7 processor, 16GB RAM, 512GB SSD. Battery life is still excellent (around 10 hours of regular use). Comes with original charger and box. Selling because I'm upgrading to a MacBook for design work.\n\nSpecs:\n- Intel Core i7 11th Gen\n- 16GB RAM\n- 512GB NVMe SSD\n- 13.4\" FHD+ Display (1920x1200)\n- Windows 11 Pro\n- Backlit Keyboard\n- Thunderbolt 4 ports",
|
||||
images: ["/image1.avif", "/image2.avif", "/image3.avif"],
|
||||
status: "active",
|
||||
datePosted: "2023-03-02",
|
||||
};
|
||||
|
||||
setFormData(itemData);
|
||||
setOriginalData(itemData);
|
||||
setImagePreviewUrls(itemData.images);
|
||||
setIsLoading(false);
|
||||
}, 1000);
|
||||
}
|
||||
}, [id, isEditing]);
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData({
|
||||
...formData,
|
||||
[name]: value,
|
||||
});
|
||||
|
||||
// Clear error when field is edited
|
||||
if (errors[name]) {
|
||||
setErrors({
|
||||
...errors,
|
||||
[name]: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleImageChange = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const files = Array.from(e.target.files);
|
||||
|
||||
if (formData.images.length + files.length > 5) {
|
||||
setErrors({
|
||||
...errors,
|
||||
images: "Maximum 5 images allowed",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Create preview URLs for the images
|
||||
const newImagePreviewUrls = [...imagePreviewUrls];
|
||||
const newImages = [...formData.images];
|
||||
|
||||
files.forEach((file) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
newImagePreviewUrls.push(reader.result);
|
||||
setImagePreviewUrls(newImagePreviewUrls);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
newImages.push(file);
|
||||
});
|
||||
|
||||
setFormData({
|
||||
...formData,
|
||||
images: newImages,
|
||||
});
|
||||
|
||||
// Clear error if any
|
||||
if (errors.images) {
|
||||
setErrors({
|
||||
...errors,
|
||||
images: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const removeImage = (index) => {
|
||||
const newImages = [...formData.images];
|
||||
const newImagePreviewUrls = [...imagePreviewUrls];
|
||||
|
||||
newImages.splice(index, 1);
|
||||
newImagePreviewUrls.splice(index, 1);
|
||||
|
||||
setFormData({
|
||||
...formData,
|
||||
images: newImages,
|
||||
});
|
||||
setImagePreviewUrls(newImagePreviewUrls);
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors = {};
|
||||
|
||||
if (!formData.title.trim()) newErrors.title = "Title is required";
|
||||
if (!formData.price) newErrors.price = "Price is required";
|
||||
if (isNaN(formData.price) || formData.price <= 0)
|
||||
newErrors.price = "Price must be a positive number";
|
||||
if (!formData.category) newErrors.category = "Category is required";
|
||||
if (!formData.condition) newErrors.condition = "Condition is required";
|
||||
if (!formData.shortDescription.trim())
|
||||
newErrors.shortDescription = "Short description is required";
|
||||
if (!formData.description.trim())
|
||||
newErrors.description = "Description is required";
|
||||
if (formData.images.length === 0)
|
||||
newErrors.images = "At least one image is required";
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) {
|
||||
// Scroll to the first error
|
||||
const firstErrorField = Object.keys(errors)[0];
|
||||
document
|
||||
.getElementsByName(firstErrorField)[0]
|
||||
?.scrollIntoView({ behavior: "smooth" });
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
// Simulate API call to post/update the item
|
||||
setTimeout(() => {
|
||||
console.log("Form submitted:", formData);
|
||||
setIsSubmitting(false);
|
||||
|
||||
// Show success and redirect to listings
|
||||
alert(`Item successfully ${isEditing ? "updated" : "created"}!`);
|
||||
navigate("/selling");
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
setIsSubmitting(true);
|
||||
|
||||
// Simulate API call to delete the item
|
||||
setTimeout(() => {
|
||||
console.log("Item deleted:", id);
|
||||
setIsSubmitting(false);
|
||||
setShowDeleteModal(false);
|
||||
|
||||
// Show success and redirect to listings
|
||||
alert("Item successfully deleted!");
|
||||
navigate("/selling");
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
// Show loading state if necessary
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto px-4 py-8">
|
||||
<div className="flex items-center mb-6">
|
||||
<Link to="/selling" className="text-green-600 hover:text-green-700">
|
||||
<ArrowLeft className="h-4 w-4 mr-1" />
|
||||
Back to listings
|
||||
</Link>
|
||||
</div>
|
||||
<div className="animate-pulse">
|
||||
<div className="h-8 bg-gray-200 rounded w-1/3 mb-8"></div>
|
||||
<div className="h-64 bg-gray-200 rounded mb-4"></div>
|
||||
<div className="h-32 bg-gray-200 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto px-4 py-8">
|
||||
{/* Breadcrumb & Back Link */}
|
||||
<div className="mb-6">
|
||||
<Link
|
||||
to="/selling"
|
||||
className="flex items-center text-green-600 hover:text-green-700"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-1" />
|
||||
<span>Back to listings</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-3xl font-bold text-gray-800">
|
||||
{isEditing ? "Edit Item" : "Create New Listing"}
|
||||
</h1>
|
||||
|
||||
{isEditing && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowDeleteModal(true)}
|
||||
className="bg-red-500 hover:bg-red-600 text-white font-medium py-2 px-4 flex items-center"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<Trash className="h-5 w-5 mr-1" />
|
||||
Delete Item
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="bg-white border border-gray-200 p-6 rounded-md"
|
||||
>
|
||||
{/* Title */}
|
||||
<div className="mb-6">
|
||||
<label
|
||||
htmlFor="title"
|
||||
className="block text-gray-700 font-medium mb-2"
|
||||
>
|
||||
Title <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
name="title"
|
||||
value={formData.title}
|
||||
onChange={handleChange}
|
||||
className={`w-full px-4 py-2 border ${errors.title ? "border-red-500" : "border-gray-300"} focus:outline-none focus:border-green-500`}
|
||||
placeholder="e.g., Dell XPS 13 Laptop - 2023 Model"
|
||||
/>
|
||||
{errors.title && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.title}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Price, Category, Status (side by side on larger screens) */}
|
||||
<div className="flex flex-col md:flex-row gap-6 mb-6">
|
||||
<div className="w-full md:w-1/3">
|
||||
<label
|
||||
htmlFor="price"
|
||||
className="block text-gray-700 font-medium mb-2"
|
||||
>
|
||||
Price ($) <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id="price"
|
||||
name="price"
|
||||
value={formData.price}
|
||||
onChange={handleChange}
|
||||
className={`w-full px-4 py-2 border ${errors.price ? "border-red-500" : "border-gray-300"} focus:outline-none focus:border-green-500`}
|
||||
placeholder="e.g., 850"
|
||||
min="0"
|
||||
step="0.01"
|
||||
/>
|
||||
{errors.price && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.price}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="w-full md:w-1/3">
|
||||
<label
|
||||
htmlFor="category"
|
||||
className="block text-gray-700 font-medium mb-2"
|
||||
>
|
||||
Category <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
id="category"
|
||||
name="category"
|
||||
value={formData.category}
|
||||
onChange={handleChange}
|
||||
className={`w-full px-4 py-2 border ${errors.category ? "border-red-500" : "border-gray-300"} focus:outline-none focus:border-green-500 bg-white`}
|
||||
>
|
||||
<option value="">Select a category</option>
|
||||
{categories.map((category) => (
|
||||
<option key={category} value={category}>
|
||||
{category}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.category && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.category}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isEditing && (
|
||||
<div className="w-full md:w-1/3">
|
||||
<label
|
||||
htmlFor="status"
|
||||
className="block text-gray-700 font-medium mb-2"
|
||||
>
|
||||
Status
|
||||
</label>
|
||||
<select
|
||||
id="status"
|
||||
name="status"
|
||||
value={formData.status}
|
||||
onChange={handleChange}
|
||||
className="w-full px-4 py-2 border border-gray-300 focus:outline-none focus:border-green-500 bg-white"
|
||||
>
|
||||
{statuses.map((status) => (
|
||||
<option key={status} value={status}>
|
||||
{status.charAt(0).toUpperCase() + status.slice(1)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Condition */}
|
||||
<div className="mb-6">
|
||||
<label className="block text-gray-700 font-medium mb-2">
|
||||
Condition <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{conditions.map((condition) => (
|
||||
<label
|
||||
key={condition}
|
||||
className={`px-4 py-2 border cursor-pointer ${
|
||||
formData.condition === condition
|
||||
? "bg-green-50 border-green-500 text-green-700"
|
||||
: "border-gray-300 text-gray-700 hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="condition"
|
||||
value={condition}
|
||||
checked={formData.condition === condition}
|
||||
onChange={handleChange}
|
||||
className="sr-only"
|
||||
/>
|
||||
{condition}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
{errors.condition && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.condition}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Short Description */}
|
||||
<div className="mb-6">
|
||||
<label
|
||||
htmlFor="shortDescription"
|
||||
className="block text-gray-700 font-medium mb-2"
|
||||
>
|
||||
Short Description <span className="text-red-500">*</span>
|
||||
<span className="text-sm font-normal text-gray-500 ml-2">
|
||||
(Brief summary that appears in listings)
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="shortDescription"
|
||||
name="shortDescription"
|
||||
value={formData.shortDescription}
|
||||
onChange={handleChange}
|
||||
className={`w-full px-4 py-2 border ${errors.shortDescription ? "border-red-500" : "border-gray-300"} focus:outline-none focus:border-green-500`}
|
||||
placeholder="e.g., Dell XPS 13 laptop in excellent condition. Intel Core i7, 16GB RAM, 512GB SSD."
|
||||
maxLength="150"
|
||||
/>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
{formData.shortDescription.length}/150 characters
|
||||
</p>
|
||||
{errors.shortDescription && (
|
||||
<p className="text-red-500 text-sm">{errors.shortDescription}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Full Description */}
|
||||
<div className="mb-6">
|
||||
<label
|
||||
htmlFor="description"
|
||||
className="block text-gray-700 font-medium mb-2"
|
||||
>
|
||||
Full Description <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="description"
|
||||
name="description"
|
||||
value={formData.description}
|
||||
onChange={handleChange}
|
||||
className={`w-full px-4 py-2 border ${errors.description ? "border-red-500" : "border-gray-300"} focus:outline-none focus:border-green-500 h-40`}
|
||||
placeholder="Describe your item in detail. Include specs, condition, reason for selling, etc."
|
||||
></textarea>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Use blank lines to separate paragraphs.
|
||||
</p>
|
||||
{errors.description && (
|
||||
<p className="text-red-500 text-sm">{errors.description}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Image Upload */}
|
||||
<div className="mb-8">
|
||||
<label className="block text-gray-700 font-medium mb-2">
|
||||
Images <span className="text-red-500">*</span>
|
||||
<span className="text-sm font-normal text-gray-500 ml-2">
|
||||
(Up to 5 images)
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{/* Image Preview Area */}
|
||||
<div className="flex flex-wrap gap-4 mb-4">
|
||||
{imagePreviewUrls.map((url, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="relative w-24 h-24 border border-gray-300"
|
||||
>
|
||||
<img
|
||||
src={url}
|
||||
alt={`Preview ${index + 1}`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeImage(index)}
|
||||
className="absolute -top-2 -right-2 bg-white rounded-full p-1 shadow-md border border-gray-300"
|
||||
>
|
||||
<X className="h-4 w-4 text-gray-600" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Upload Button (only show if less than 5 images) */}
|
||||
{formData.images.length < 5 && (
|
||||
<label className="w-24 h-24 border-2 border-dashed border-gray-300 flex flex-col items-center justify-center text-gray-500 cursor-pointer hover:bg-gray-50">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
onChange={handleImageChange}
|
||||
className="sr-only"
|
||||
/>
|
||||
<Plus className="h-6 w-6 mb-1" />
|
||||
<span className="text-xs">Add Image</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
{errors.images && (
|
||||
<p className="text-red-500 text-sm">{errors.images}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<div className="mt-8">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className={`w-full py-3 px-4 text-white font-medium flex items-center justify-center ${
|
||||
isSubmitting ? "bg-gray-400" : "bg-green-500 hover:bg-green-600"
|
||||
}`}
|
||||
>
|
||||
<Save className="h-5 w-5 mr-2" />
|
||||
{isSubmitting
|
||||
? "Saving..."
|
||||
: isEditing
|
||||
? "Save Changes"
|
||||
: "Create Listing"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
{showDeleteModal && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white p-6 rounded-md max-w-md w-full">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
Delete Listing
|
||||
</h3>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Are you sure you want to delete <strong>{formData.title}</strong>?
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
<div className="flex justify-end space-x-3">
|
||||
<button
|
||||
onClick={() => setShowDeleteModal(false)}
|
||||
className="px-4 py-2 border border-gray-300 text-gray-700 font-medium rounded-md hover:bg-gray-50"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className="px-4 py-2 bg-red-600 text-white font-medium rounded-md hover:bg-red-700 flex items-center"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Deleting..." : "Delete"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ItemForm;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Filter, Grid, Heart, Tag, X } from "lucide-react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { X } from "lucide-react";
|
||||
import { useLocation, Link } from "react-router-dom";
|
||||
import axios from "axios";
|
||||
|
||||
|
||||
@@ -1,152 +1,221 @@
|
||||
import { useState } from "react";
|
||||
import { Pencil, Trash2, Plus } from "lucide-react";
|
||||
import { useState, useEffect } from "react";
|
||||
import ProductForm from "../components/ProductForm";
|
||||
|
||||
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,
|
||||
// State to store user's products
|
||||
const [products, setProducts] = useState([]);
|
||||
// State to control when editing form is shown
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
// State to store the product being edited (or empty for new product)
|
||||
const [editingProduct, setEditingProduct] = useState({
|
||||
name: "",
|
||||
price: "",
|
||||
status: "Active",
|
||||
description: "",
|
||||
categories: [],
|
||||
status: "Unsold",
|
||||
images: [],
|
||||
});
|
||||
setView("form");
|
||||
|
||||
// Simulate fetching products from API/database on component mount
|
||||
useEffect(() => {
|
||||
// This would be replaced with a real API call
|
||||
const fetchProducts = async () => {
|
||||
// Mock data
|
||||
const mockProducts = [
|
||||
{
|
||||
id: "1",
|
||||
name: "Vintage Camera",
|
||||
price: "299.99",
|
||||
description: "A beautiful vintage film camera in excellent condition",
|
||||
categories: ["Electronics", "Art & Collectibles"],
|
||||
status: "Unsold",
|
||||
images: ["/public/Pictures/Dell1.jpg"],
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Leather Jacket",
|
||||
price: "149.50",
|
||||
description: "Genuine leather jacket, worn only a few times",
|
||||
categories: ["Clothing"],
|
||||
status: "Unsold",
|
||||
images: [],
|
||||
},
|
||||
];
|
||||
|
||||
setProducts(mockProducts);
|
||||
};
|
||||
|
||||
const handleDelete = (id) => {
|
||||
setProducts((prev) => prev.filter((p) => p.id !== id));
|
||||
};
|
||||
fetchProducts();
|
||||
}, []);
|
||||
|
||||
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) {
|
||||
// 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(),
|
||||
id: Date.now().toString(), // Generate a temporary ID
|
||||
};
|
||||
setProducts((prev) => [newProduct, ...prev]);
|
||||
} else {
|
||||
setProducts((prev) =>
|
||||
prev.map((p) => (p.id === editingProduct.id ? editingProduct : p)),
|
||||
);
|
||||
setProducts([...products, newProduct]);
|
||||
}
|
||||
|
||||
setEditingProduct(null);
|
||||
setView("list");
|
||||
// Reset form and hide it
|
||||
setShowForm(false);
|
||||
setEditingProduct({
|
||||
name: "",
|
||||
price: "",
|
||||
description: "",
|
||||
categories: [],
|
||||
status: "Unsold",
|
||||
images: [],
|
||||
});
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setEditingProduct(null);
|
||||
setView("list");
|
||||
// Handle product deletion
|
||||
const handleDeleteProduct = (productId) => {
|
||||
if (window.confirm("Are you sure you want to delete this product?")) {
|
||||
setProducts(products.filter((p) => p.id !== productId));
|
||||
}
|
||||
};
|
||||
|
||||
// Handle editing a product
|
||||
const handleEditProduct = (product) => {
|
||||
setEditingProduct({
|
||||
...product,
|
||||
images: product.images || [], // Ensure images array exists
|
||||
});
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
// Handle adding a new product
|
||||
const handleAddProduct = () => {
|
||||
setEditingProduct({
|
||||
name: "",
|
||||
price: "",
|
||||
description: "",
|
||||
categories: [],
|
||||
status: "Unsold",
|
||||
images: [],
|
||||
});
|
||||
setShowForm(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 max-w-4xl mx-auto">
|
||||
{view === "list" && (
|
||||
<>
|
||||
<div className="container mx-auto p-4 max-w-6xl">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-xl font-semibold text-gray-800">My Listings</h2>
|
||||
<h1 className="text-2xl font-bold text-gray-800">My Listings</h1>
|
||||
{!showForm && (
|
||||
<button
|
||||
onClick={handleAddNew}
|
||||
className="bg-green-500 text-white px-4 py-2 hover:bg-green-600 transition-all"
|
||||
onClick={handleAddProduct}
|
||||
className="bg-emerald-600 text-white px-4 py-2 hover:bg-emerald-700"
|
||||
>
|
||||
<Plus className="inline-block mr-2" size={18} /> Add New Product
|
||||
+ 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" && (
|
||||
{showForm ? (
|
||||
<ProductForm
|
||||
editingProduct={editingProduct}
|
||||
setEditingProduct={setEditingProduct}
|
||||
onSave={handleSave}
|
||||
onCancel={handleCancel}
|
||||
onSave={handleSaveProduct}
|
||||
onCancel={() => setShowForm(false)}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{products.length === 0 ? (
|
||||
<div className="text-center py-10 bg-gray-50">
|
||||
<p className="text-gray-500 mb-4">
|
||||
You don't have any listings yet
|
||||
</p>
|
||||
<button
|
||||
onClick={handleAddProduct}
|
||||
className="bg-emerald-600 text-white px-4 py-2 hover:bg-emerald-700"
|
||||
>
|
||||
Create Your First Listing
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{products.map((product) => (
|
||||
<div
|
||||
key={product.id}
|
||||
className="border-2 border-gray-200 overflow-hidden hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="h-48 bg-gray-200 flex items-center justify-center">
|
||||
{product.images && product.images.length > 0 ? (
|
||||
<img
|
||||
src={product.images[0] || ""}
|
||||
alt={product.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-gray-400">No image</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="flex justify-between items-start">
|
||||
<h3 className="text-lg font-semibold text-gray-800">
|
||||
{product.name}
|
||||
</h3>
|
||||
<span
|
||||
className={`px-2 py-1 text-xs ${
|
||||
product.status === "Sold"
|
||||
? "bg-gray-200 text-gray-700"
|
||||
: "bg-emerald-100 text-emerald-800"
|
||||
}`}
|
||||
>
|
||||
{product.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-emerald-600 font-bold mt-1">
|
||||
${product.price}
|
||||
</p>
|
||||
|
||||
{product.categories && product.categories.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{product.categories.map((category) => (
|
||||
<span
|
||||
key={category}
|
||||
className="text-xs bg-gray-100 text-gray-600 px-2 py-1 "
|
||||
>
|
||||
{category}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-gray-500 text-sm mt-2 line-clamp-2">
|
||||
{product.description}
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex justify-end gap-2">
|
||||
<button
|
||||
onClick={() => handleDeleteProduct(product.id)}
|
||||
className="text-red-600 hover:text-red-800"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleEditProduct(product)}
|
||||
className="text-emerald-600 hover:text-emerald-800 font-medium"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Tag, Book, Laptop, Sofa, Utensils, Gift, Heart } from 'lucide-react';
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const Transactions = () => {
|
||||
return (
|
||||
<div>
|
||||
|
||||
</div>
|
||||
);
|
||||
return <div></div>;
|
||||
};
|
||||
|
||||
export default Transactions;
|
||||