2025-03-24 23:04:12 -06:00
|
|
|
import { useState, useEffect } from "react";
|
2025-03-18 18:09:15 -06:00
|
|
|
import { useParams, Link } from "react-router-dom";
|
2025-03-24 23:04:12 -06:00
|
|
|
import { Heart, ArrowLeft, Tag, User, Calendar } from "lucide-react";
|
2025-03-05 22:30:52 -07:00
|
|
|
|
|
|
|
|
const ProductDetail = () => {
|
|
|
|
|
const { id } = useParams();
|
2025-03-24 23:04:12 -06:00
|
|
|
const [product, setProduct] = useState(null);
|
2025-03-25 14:47:54 -06:00
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [error, setError] = useState(null);
|
2025-03-05 22:30:52 -07:00
|
|
|
const [isFavorite, setIsFavorite] = useState(false);
|
|
|
|
|
const [showContactForm, setShowContactForm] = useState(false);
|
2025-03-18 18:09:15 -06:00
|
|
|
const [message, setMessage] = useState("");
|
2025-03-05 22:30:52 -07:00
|
|
|
const [currentImage, setCurrentImage] = useState(0);
|
2025-03-18 18:09:15 -06:00
|
|
|
|
2025-03-24 23:04:12 -06:00
|
|
|
// Fetch product details
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const fetchProduct = async () => {
|
|
|
|
|
try {
|
2025-03-25 14:47:54 -06:00
|
|
|
setLoading(true);
|
|
|
|
|
const response = await fetch(`http://localhost:3030/api/product/${id}`);
|
2025-03-05 22:30:52 -07:00
|
|
|
|
2025-03-25 14:47:54 -06:00
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error("Failed to fetch product");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
console.log(result);
|
|
|
|
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
setProduct(result.data);
|
|
|
|
|
setError(null);
|
2025-03-24 23:04:12 -06:00
|
|
|
} else {
|
2025-03-25 14:47:54 -06:00
|
|
|
throw new Error(result.message || "Error fetching product");
|
2025-03-24 23:04:12 -06:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Error fetching product:", error);
|
2025-03-25 14:47:54 -06:00
|
|
|
setError(error.message);
|
|
|
|
|
setProduct(null);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
2025-03-24 23:04:12 -06:00
|
|
|
}
|
|
|
|
|
};
|
2025-03-05 22:30:52 -07:00
|
|
|
|
2025-03-24 23:04:12 -06:00
|
|
|
fetchProduct();
|
|
|
|
|
}, [id]);
|
|
|
|
|
|
|
|
|
|
// Handle favorite toggle
|
2025-03-25 14:47:54 -06:00
|
|
|
const toggleFavorite = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(
|
|
|
|
|
"http://localhost:3030/api/product/add_to_favorite",
|
|
|
|
|
{
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
userID: 1, // Replace with actual user ID
|
|
|
|
|
productsID: id,
|
|
|
|
|
}),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
if (result.success) {
|
|
|
|
|
setIsFavorite(!isFavorite);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Error toggling favorite:", error);
|
|
|
|
|
}
|
2025-03-05 22:30:52 -07:00
|
|
|
};
|
2025-03-18 18:09:15 -06:00
|
|
|
|
2025-03-24 23:04:12 -06:00
|
|
|
// Handle message submission
|
2025-03-05 22:30:52 -07:00
|
|
|
const handleSendMessage = (e) => {
|
|
|
|
|
e.preventDefault();
|
2025-03-25 14:47:54 -06:00
|
|
|
// TODO: Implement actual message sending logic
|
2025-03-18 18:09:15 -06:00
|
|
|
console.log("Message sent:", message);
|
|
|
|
|
setMessage("");
|
2025-03-05 22:30:52 -07:00
|
|
|
setShowContactForm(false);
|
2025-03-18 18:09:15 -06:00
|
|
|
alert("Message sent to seller!");
|
2025-03-05 22:30:52 -07:00
|
|
|
};
|
|
|
|
|
|
2025-03-24 23:04:12 -06:00
|
|
|
// Image navigation
|
2025-03-05 22:30:52 -07:00
|
|
|
const nextImage = () => {
|
2025-03-25 14:47:54 -06:00
|
|
|
if (product && product.images) {
|
|
|
|
|
setCurrentImage((prev) =>
|
|
|
|
|
prev === product.images.length - 1 ? 0 : prev + 1,
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-03-05 22:30:52 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const prevImage = () => {
|
2025-03-25 14:47:54 -06:00
|
|
|
if (product && product.images) {
|
|
|
|
|
setCurrentImage((prev) =>
|
|
|
|
|
prev === 0 ? product.images.length - 1 : prev - 1,
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-03-05 22:30:52 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const selectImage = (index) => {
|
|
|
|
|
setCurrentImage(index);
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-25 14:47:54 -06:00
|
|
|
// Render loading state
|
|
|
|
|
if (loading) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex justify-center items-center h-screen">
|
|
|
|
|
<div className="animate-spin rounded-full h-32 w-32 border-t-2 border-green-500"></div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Render error state
|
|
|
|
|
if (error) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex justify-center items-center h-screen">
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<h2 className="text-2xl text-red-500 mb-4">Error Loading Product</h2>
|
|
|
|
|
<p className="text-gray-600">{error}</p>
|
|
|
|
|
<Link
|
|
|
|
|
to="/"
|
|
|
|
|
className="mt-4 inline-block bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600"
|
|
|
|
|
>
|
|
|
|
|
Back to Listings
|
|
|
|
|
</Link>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-03-24 23:04:12 -06:00
|
|
|
|
2025-03-25 14:47:54 -06:00
|
|
|
// Render product details
|
2025-03-05 22:30:52 -07:00
|
|
|
return (
|
|
|
|
|
<div className="max-w-6xl mx-auto px-4 py-8">
|
|
|
|
|
<div className="mb-6">
|
2025-03-18 18:09:15 -06:00
|
|
|
<Link
|
|
|
|
|
to="/"
|
|
|
|
|
className="flex items-center text-green-600 hover:text-green-700"
|
|
|
|
|
>
|
2025-03-05 22:30:52 -07:00
|
|
|
<ArrowLeft className="h-4 w-4 mr-1" />
|
|
|
|
|
<span>Back to listings</span>
|
|
|
|
|
</Link>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-col md:flex-row gap-8">
|
|
|
|
|
<div className="md:w-3/5">
|
|
|
|
|
<div className="bg-white border border-gray-200 mb-4 relative">
|
2025-03-25 14:47:54 -06:00
|
|
|
{product.images && product.images.length > 0 ? (
|
|
|
|
|
<img
|
|
|
|
|
src={product.images[currentImage]}
|
|
|
|
|
alt={product.Name}
|
|
|
|
|
className="w-full h-auto object-contain cursor-pointer"
|
|
|
|
|
onClick={nextImage}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="w-full h-96 flex items-center justify-center bg-gray-200 text-gray-500">
|
|
|
|
|
No Image Available
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-03-05 22:30:52 -07:00
|
|
|
</div>
|
2025-03-18 18:09:15 -06:00
|
|
|
|
2025-03-25 14:47:54 -06:00
|
|
|
{product.images && product.images.length > 1 && (
|
2025-03-05 22:30:52 -07:00
|
|
|
<div className="flex gap-2 overflow-x-auto pb-2">
|
2025-03-24 23:04:12 -06:00
|
|
|
{product.images.map((image, index) => (
|
2025-03-18 18:09:15 -06:00
|
|
|
<div
|
|
|
|
|
key={index}
|
|
|
|
|
className={`bg-white border ${currentImage === index ? "border-green-500" : "border-gray-200"} min-w-[100px] cursor-pointer`}
|
2025-03-05 22:30:52 -07:00
|
|
|
onClick={() => selectImage(index)}
|
|
|
|
|
>
|
2025-03-18 18:09:15 -06:00
|
|
|
<img
|
|
|
|
|
src={image}
|
2025-03-25 14:47:54 -06:00
|
|
|
alt={`${product.Name} - view ${index + 1}`}
|
2025-03-05 22:30:52 -07:00
|
|
|
className="w-full h-auto object-cover"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="md:w-2/5">
|
|
|
|
|
<div className="bg-white border border-gray-200 p-6 mb-6">
|
|
|
|
|
<div className="flex justify-between items-start mb-4">
|
2025-03-18 18:09:15 -06:00
|
|
|
<h1 className="text-2xl font-bold text-gray-800">
|
2025-03-25 14:47:54 -06:00
|
|
|
{product.Name}
|
2025-03-18 18:09:15 -06:00
|
|
|
</h1>
|
|
|
|
|
<button
|
2025-03-05 22:30:52 -07:00
|
|
|
onClick={toggleFavorite}
|
|
|
|
|
className="p-2 hover:bg-gray-100"
|
|
|
|
|
>
|
2025-03-18 18:09:15 -06:00
|
|
|
<Heart
|
|
|
|
|
className={`h-6 w-6 ${isFavorite ? "text-red-500 fill-red-500" : "text-gray-400"}`}
|
2025-03-05 22:30:52 -07:00
|
|
|
/>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2025-03-18 18:09:15 -06:00
|
|
|
|
2025-03-05 22:30:52 -07:00
|
|
|
<div className="text-2xl font-bold text-green-600 mb-4">
|
2025-03-25 14:47:54 -06:00
|
|
|
${product.Price}
|
2025-03-05 22:30:52 -07:00
|
|
|
</div>
|
|
|
|
|
<div className="flex flex-wrap gap-x-4 gap-y-2 mb-6 text-sm">
|
|
|
|
|
<div className="flex items-center text-gray-600">
|
|
|
|
|
<Tag className="h-4 w-4 mr-1" />
|
2025-03-25 14:47:54 -06:00
|
|
|
<span>{product.Category}</span>
|
2025-03-05 22:30:52 -07:00
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center text-gray-600">
|
|
|
|
|
<span className="font-medium">Condition:</span>
|
2025-03-24 23:04:12 -06:00
|
|
|
<span className="ml-1">{product.condition}</span>
|
2025-03-05 22:30:52 -07:00
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center text-gray-600">
|
|
|
|
|
<Calendar className="h-4 w-4 mr-1" />
|
2025-03-25 14:47:54 -06:00
|
|
|
<span>Posted on {product.Date}</span>
|
2025-03-05 22:30:52 -07:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-03-18 18:09:15 -06:00
|
|
|
|
2025-03-05 22:30:52 -07:00
|
|
|
<div className="bg-gray-50 p-4 mb-6 border border-gray-200">
|
2025-03-25 14:47:54 -06:00
|
|
|
<p className="text-gray-700">{product.Description}</p>
|
2025-03-05 22:30:52 -07:00
|
|
|
</div>
|
2025-03-18 18:09:15 -06:00
|
|
|
|
2025-03-05 22:30:52 -07:00
|
|
|
<button
|
|
|
|
|
onClick={() => setShowContactForm(!showContactForm)}
|
|
|
|
|
className="w-full bg-green-500 hover:bg-green-600 text-white font-medium py-3 px-4 mb-3"
|
|
|
|
|
>
|
|
|
|
|
Contact Seller
|
|
|
|
|
</button>
|
2025-03-18 18:09:15 -06:00
|
|
|
|
2025-03-05 22:30:52 -07:00
|
|
|
{showContactForm && (
|
|
|
|
|
<div className="border border-gray-200 p-4 mb-4">
|
2025-03-18 18:09:15 -06:00
|
|
|
<h3 className="font-medium text-gray-800 mb-2">
|
|
|
|
|
Contact Seller
|
|
|
|
|
</h3>
|
2025-03-05 22:30:52 -07:00
|
|
|
<form onSubmit={handleSendMessage}>
|
2025-03-18 18:09:15 -06:00
|
|
|
<div className="mb-3">
|
|
|
|
|
<label htmlFor="email" className="block text-gray-700 mb-1">
|
|
|
|
|
Email
|
|
|
|
|
</label>
|
|
|
|
|
<input
|
|
|
|
|
type="email"
|
|
|
|
|
id="email"
|
2025-03-24 23:04:12 -06:00
|
|
|
value={message}
|
|
|
|
|
onChange={(e) => setMessage(e.target.value)}
|
2025-03-18 18:09:15 -06:00
|
|
|
className="w-full p-3 border border-gray-300 focus:outline-none focus:border-green-500"
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mb-3">
|
|
|
|
|
<label htmlFor="phone" className="block text-gray-700 mb-1">
|
|
|
|
|
Phone Number
|
|
|
|
|
</label>
|
|
|
|
|
<input
|
|
|
|
|
type="tel"
|
|
|
|
|
id="phone"
|
|
|
|
|
className="w-full p-3 border border-gray-300 focus:outline-none focus:border-green-500"
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mb-3">
|
|
|
|
|
<label
|
|
|
|
|
htmlFor="contactMessage"
|
|
|
|
|
className="block text-gray-700 mb-1"
|
|
|
|
|
>
|
|
|
|
|
Message (Optional)
|
|
|
|
|
</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
id="contactMessage"
|
2025-03-24 23:04:12 -06:00
|
|
|
value={message}
|
|
|
|
|
onChange={(e) => setMessage(e.target.value)}
|
2025-03-18 18:09:15 -06:00
|
|
|
placeholder="Hi, is this item still available?"
|
|
|
|
|
className="w-full p-3 border border-gray-300 focus:outline-none focus:border-green-500"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2025-03-05 22:30:52 -07:00
|
|
|
<button
|
|
|
|
|
type="submit"
|
|
|
|
|
className="bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-4"
|
|
|
|
|
>
|
2025-03-18 18:09:15 -06:00
|
|
|
Send Contact Info
|
2025-03-05 22:30:52 -07:00
|
|
|
</button>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-03-18 18:09:15 -06:00
|
|
|
|
2025-03-05 22:30:52 -07:00
|
|
|
<div className="pt-4 border-t border-gray-200">
|
|
|
|
|
<div className="flex items-center mb-3">
|
|
|
|
|
<div className="mr-3">
|
2025-03-25 14:47:54 -06:00
|
|
|
<div className="h-12 w-12 rounded-full bg-gray-200 flex items-center justify-center">
|
|
|
|
|
<User className="h-6 w-6 text-gray-600" />
|
|
|
|
|
</div>
|
2025-03-05 22:30:52 -07:00
|
|
|
</div>
|
|
|
|
|
<div>
|
2025-03-18 18:09:15 -06:00
|
|
|
<h3 className="font-medium text-gray-800">
|
2025-03-25 14:47:54 -06:00
|
|
|
{product.UserID || "Unknown Seller"}
|
2025-03-18 18:09:15 -06:00
|
|
|
</h3>
|
|
|
|
|
<p className="text-sm text-gray-500">
|
2025-03-25 14:47:54 -06:00
|
|
|
Member since{" "}
|
|
|
|
|
{product.seller ? product.seller.memberSince : "N/A"}
|
2025-03-18 18:09:15 -06:00
|
|
|
</p>
|
2025-03-05 22:30:52 -07:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-sm text-gray-600">
|
|
|
|
|
<div>
|
2025-03-18 18:09:15 -06:00
|
|
|
<span className="font-medium">Rating:</span>{" "}
|
2025-03-25 14:47:54 -06:00
|
|
|
{product.seller ? `${product.seller.rating}/5` : "N/A"}
|
2025-03-05 22:30:52 -07:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-03-18 18:09:15 -06:00
|
|
|
|
2025-03-25 14:47:54 -06:00
|
|
|
{/* <div className="mt-8">
|
2025-03-05 22:30:52 -07:00
|
|
|
<h2 className="text-xl font-bold text-gray-800 mb-4">Description</h2>
|
|
|
|
|
<div className="bg-white border border-gray-200 p-6">
|
2025-03-25 14:47:54 -06:00
|
|
|
<div className="text-gray-700">{product.Description}</div>
|
2025-03-05 22:30:52 -07:00
|
|
|
</div>
|
2025-03-25 14:47:54 -06:00
|
|
|
</div> */}
|
2025-03-05 22:30:52 -07:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-18 18:09:15 -06:00
|
|
|
export default ProductDetail;
|