Files
Campus-Plug/frontend/src/pages/ProductDetail.jsx

615 lines
20 KiB
React
Raw Normal View History

2025-04-12 13:10:17 -06:00
import { useState, useEffect } from "react";
import { useParams, Link } from "react-router-dom";
import {
Heart,
ArrowLeft,
Tag,
User,
Calendar,
Star,
Phone,
Mail,
} from "lucide-react";
2025-04-15 00:18:19 -06:00
import FloatingAlert from "../components/FloatingAlert"; // adjust path if needed
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-04-04 00:02:04 -06:00
const [loading, setLoading] = useState({
product: true,
reviews: true,
2025-04-12 13:10:17 -06:00
submitting: false,
2025-04-04 00:02:04 -06:00
});
const [error, setError] = useState({
product: null,
reviews: null,
2025-04-12 13:10:17 -06:00
submit: null,
2025-04-04 00:02:04 -06:00
});
2025-03-05 22:30:52 -07:00
const [isFavorite, setIsFavorite] = useState(false);
2025-04-12 13:10:17 -06:00
const [showContactOptions, setShowContactOptions] = useState(false);
2025-03-05 22:30:52 -07:00
const [currentImage, setCurrentImage] = useState(0);
2025-04-04 00:02:04 -06:00
const [reviews, setReviews] = useState([]);
const [showReviewForm, setShowReviewForm] = useState(false);
2025-04-15 00:18:19 -06:00
const [showAlert, setShowAlert] = useState(false);
2025-04-12 13:10:17 -06:00
const storedUser = JSON.parse(sessionStorage.getItem("user"));
2025-03-18 18:09:15 -06:00
2025-04-18 10:37:19 -06:00
const toggleFavorite = async (id) => {
const response = await fetch(
"http://localhost:3030/api/product/addFavorite",
{
method: "POST",
headers: {
"Content-Type": "application/json",
2025-04-15 00:18:19 -06:00
},
2025-04-18 10:37:19 -06:00
body: JSON.stringify({
userID: storedUser.ID,
productID: id,
}),
},
);
const data = await response.json();
if (data.success) {
setShowAlert(true);
}
console.log(`Add Product -> History: ${id}`);
};
2025-04-15 00:18:19 -06:00
2025-04-04 00:02:04 -06:00
const [reviewForm, setReviewForm] = useState({
rating: 3,
comment: "",
name: "",
});
// Add this function to handle review input changes
const handleReviewInputChange = (e) => {
const { id, value } = e.target;
setReviewForm((prev) => ({
...prev,
[id]: value,
}));
};
// Add this function to handle star rating selection
const handleRatingChange = (rating) => {
setReviewForm((prev) => ({
...prev,
rating,
}));
};
2025-04-12 13:10:17 -06:00
const handleSubmitReview = async (e) => {
e.preventDefault(); // Prevent form default behavior
2025-04-04 00:02:04 -06:00
try {
2025-04-12 13:10:17 -06:00
setLoading((prev) => ({ ...prev, submitting: true }));
setError((prev) => ({ ...prev, submit: null }));
2025-04-04 00:02:04 -06:00
2025-04-12 13:10:17 -06:00
const reviewData = {
productId: id,
rating: reviewForm.rating,
comment: reviewForm.comment,
userId: storedUser.ID,
};
2025-04-04 00:02:04 -06:00
2025-04-18 10:37:19 -06:00
const response = await fetch(
`http://localhost:3030/api/review/addReview`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(reviewData),
},
);
2025-04-04 00:02:04 -06:00
const result = await response.json();
2025-04-12 13:10:17 -06:00
// Check if API returned an error message even with 200 status
if (!result.success) {
throw new Error(result.message || "Failed to submit review");
2025-04-04 00:02:04 -06:00
}
2025-04-12 13:10:17 -06:00
alert("Review submitted successfully!");
setReviewForm({
rating: 3,
comment: "",
name: "",
});
setShowReviewForm(false);
try {
setLoading((prev) => ({ ...prev, reviews: true }));
const reviewsResponse = await fetch(
`http://localhost:3030/api/review/${id}`,
);
const reviewsResult = await reviewsResponse.json();
if (reviewsResult.success) {
setReviews(reviewsResult.data || []);
setError((prev) => ({ ...prev, reviews: null }));
} else {
throw new Error(reviewsResult.message || "Error fetching reviews");
}
} catch (reviewsError) {
console.error("Error fetching reviews:", reviewsError);
setError((prev) => ({ ...prev, reviews: reviewsError.message }));
} finally {
setLoading((prev) => ({ ...prev, reviews: false }));
}
2025-04-04 00:02:04 -06:00
} catch (error) {
2025-04-12 13:10:17 -06:00
console.error("Error submitting review:", error);
alert(`Error: ${error.message}`);
setError((prev) => ({
...prev,
submit: error.message,
}));
2025-04-04 00:02:04 -06:00
} finally {
2025-04-12 13:10:17 -06:00
setLoading((prev) => ({ ...prev, submitting: false }));
2025-04-04 00:02:04 -06:00
}
};
// Fetch product data
2025-03-24 23:04:12 -06:00
useEffect(() => {
const fetchProduct = async () => {
try {
2025-04-04 00:02:04 -06:00
setLoading((prev) => ({ ...prev, product: true }));
2025-03-25 14:47:54 -06:00
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) {
2025-04-04 00:02:04 -06:00
throw new Error(`HTTP error! Status: ${response.status}`);
2025-03-25 14:47:54 -06:00
}
const result = await response.json();
if (result.success) {
setProduct(result.data);
2025-04-04 00:02:04 -06:00
setError((prev) => ({ ...prev, product: 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-04-04 00:02:04 -06:00
setError((prev) => ({ ...prev, product: error.message }));
2025-03-25 14:47:54 -06:00
} finally {
2025-04-04 00:02:04 -06:00
setLoading((prev) => ({ ...prev, product: 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]);
2025-04-04 00:02:04 -06:00
// Fetch reviews data
useEffect(() => {
const fetchReviews = async () => {
try {
setLoading((prev) => ({ ...prev, reviews: true }));
const response = await fetch(`http://localhost:3030/api/review/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const result = await response.json();
if (result.success) {
setReviews(result.data || []);
setError((prev) => ({ ...prev, reviews: null }));
} else {
throw new Error(result.message || "Error fetching reviews");
}
} catch (error) {
console.error("Error fetching reviews:", error);
setError((prev) => ({ ...prev, reviews: error.message }));
setReviews([]);
} finally {
setLoading((prev) => ({ ...prev, reviews: false }));
}
};
fetchReviews();
}, [id]);
2025-03-24 23:04:12 -06:00
// Image navigation
2025-03-05 22:30:52 -07:00
const nextImage = () => {
2025-04-04 00:02:04 -06:00
if (product?.images?.length > 0) {
2025-03-25 14:47:54 -06:00
setCurrentImage((prev) =>
prev === product.images.length - 1 ? 0 : prev + 1,
);
}
2025-03-05 22:30:52 -07:00
};
const prevImage = () => {
2025-04-04 00:02:04 -06:00
if (product?.images?.length > 0) {
2025-03-25 14:47:54 -06:00
setCurrentImage((prev) =>
prev === 0 ? product.images.length - 1 : prev - 1,
);
}
2025-03-05 22:30:52 -07:00
};
const selectImage = (index) => {
setCurrentImage(index);
};
2025-04-04 00:02:04 -06:00
// Function to render stars based on rating
const renderStars = (rating) => {
const stars = [];
for (let i = 1; i <= 5; i++) {
stars.push(
<Star
key={i}
className={`h-4 w-4 ${i <= rating ? "text-yellow-400 fill-yellow-400" : "text-gray-300"}`}
/>,
);
}
return stars;
};
// Render loading state for the entire page
if (loading.product) {
2025-03-25 14:47:54 -06:00
return (
<div className="flex justify-center items-center h-screen">
2025-04-18 10:37:19 -06:00
<div className="animate-spin h-32 w-32 border-t-2 border-green-500"></div>
2025-03-25 14:47:54 -06:00
</div>
);
}
2025-04-04 00:02:04 -06:00
// Render error state for product
if (error.product) {
2025-03-25 14:47:54 -06:00
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>
2025-04-04 00:02:04 -06:00
<p className="text-gray-600">{error.product}</p>
<Link
to="/"
2025-04-18 10:37:19 -06:00
className="mt-4 inline-block bg-green-500 text-white px-4 py-2 hover:bg-green-600"
2025-04-04 00:02:04 -06:00
>
Back to Listings
</Link>
</div>
</div>
);
}
// Safety check for product
if (!product) {
return (
<div className="flex justify-center items-center h-screen">
<div className="text-center">
<h2 className="text-2xl text-red-500 mb-4">Product Not Found</h2>
2025-03-25 14:47:54 -06:00
<Link
to="/"
2025-04-18 10:37:19 -06:00
className="mt-4 inline-block bg-green-500 text-white px-4 py-2 hover:bg-green-600"
2025-03-25 14:47:54 -06:00
>
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
2025-03-29 17:28:09 -06:00
to="/search"
2025-03-18 18:09:15 -06:00
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" />
2025-03-29 17:28:09 -06:00
<span>Back</span>
2025-03-05 22:30:52 -07:00
</Link>
</div>
2025-04-15 00:18:19 -06:00
{showAlert && (
<FloatingAlert
message="Product added to favorites!"
onClose={() => setShowAlert(false)}
/>
)}
2025-03-05 22:30:52 -07:00
<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 ? (
2025-04-04 00:02:04 -06:00
<>
<img
src={product.images[currentImage]}
alt={product.Name}
className="w-full h-auto object-contain cursor-pointer"
onClick={nextImage}
onError={(e) => {
e.target.onerror = null;
2025-04-18 10:37:19 -06:00
e.target.src = "https://via.placeholder.com/400x300";
2025-04-04 00:02:04 -06:00
}}
/>
{product.images.length > 1 && (
<div className="absolute inset-x-0 bottom-0 flex justify-between p-2">
<button
onClick={(e) => {
e.stopPropagation();
prevImage();
}}
2025-04-18 10:37:19 -06:00
className="bg-white/70 p-1"
2025-04-04 00:02:04 -06:00
>
<ArrowLeft className="h-5 w-5" />
</button>
2025-04-18 10:37:19 -06:00
<div className="text-sm bg-white/70 px-2 py-1 ">
2025-04-04 00:02:04 -06:00
{currentImage + 1}/{product.images.length}
</div>
</div>
)}
</>
2025-03-25 14:47:54 -06:00
) : (
<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}
2025-04-18 10:37:19 -06:00
className={`bg-white border ${currentImage === index ? "border-green-500 border-2" : "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"
2025-04-04 00:02:04 -06:00
onError={(e) => {
e.target.onerror = null;
e.target.src =
"https://via.placeholder.com/100x100?text=Error";
}}
2025-03-05 22:30:52 -07:00
/>
</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-04-04 00:02:04 -06:00
{product.Name || "Unnamed Product"}
2025-03-18 18:09:15 -06:00
</h1>
<button
2025-04-15 00:18:19 -06:00
onClick={() => toggleFavorite(product.ProductID)}
2025-04-18 10:37:19 -06:00
className="p-2 hover:bg-gray-100"
2025-04-04 00:02:04 -06:00
aria-label={
isFavorite ? "Remove from favorites" : "Add to favorites"
}
2025-03-05 22:30:52 -07:00
>
2025-04-18 10:37:19 -06:00
<Heart className={`h-6 w-6 ext-red-500 fill-red-500`} />
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-04-04 00:02:04 -06:00
$
{typeof product.Price === "number"
? product.Price.toFixed(2)
: 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">
2025-04-04 00:02:04 -06:00
{product.Category && (
<div className="flex items-center text-gray-600">
<Tag className="h-4 w-4 mr-1" />
<span>{product.Category}</span>
</div>
)}
2025-04-03 18:56:39 -06:00
2025-04-04 00:02:04 -06:00
{product.Date && (
<div className="flex items-center text-gray-600">
<Calendar className="h-4 w-4 mr-1" />
<span>Posted on {product.Date}</span>
</div>
)}
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
<div className="bg-gray-50 p-4 mb-6 border border-gray-200">
2025-04-04 00:02:04 -06:00
<p className="text-gray-700">
{product.Description || "No description available"}
</p>
2025-03-05 22:30:52 -07:00
</div>
2025-03-18 18:09:15 -06:00
2025-04-12 13:10:17 -06:00
<div className="relative">
<button
onClick={() => setShowContactOptions(!showContactOptions)}
className="w-full bg-green-500 hover:bg-green-600 text-white font-medium py-3 px-4 mb-3"
>
Contact Seller
</button>
{showContactOptions && (
<div className="absolute z-10 w-full bg-white border border-gray-200 shadow-md">
{product.SellerPhone && (
<a
href={`tel:${product.SellerPhone}`}
className="flex items-center gap-2 p-3 hover:bg-gray-50 border-b border-gray-100"
>
<Phone className="h-5 w-5 text-green-500" />
<span>Call Seller</span>
</a>
)}
{product.SellerEmail && (
<a
href={`mailto:${product.SellerEmail}`}
className="flex items-center gap-2 p-3 hover:bg-gray-50"
>
<Mail className="h-5 w-5 text-green-500" />
<span>Email Seller</span>
</a>
)}
</div>
)}
</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">
2025-04-12 11:27:27 -06:00
{/* Seller Info */}
2025-03-05 22:30:52 -07:00
<div className="flex items-center mb-3">
<div className="mr-3">
2025-04-18 10:37:19 -06:00
<div className="h-12 w-12 bg-gray-200 flex items-center justify-center">
2025-03-25 14:47:54 -06:00
<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-04-03 18:56:39 -06:00
{product.SellerName || "Unknown Seller"}
2025-03-18 18:09:15 -06:00
</h3>
<p className="text-sm text-gray-500">
2025-04-04 00:02:04 -06:00
Member since {product.SellerJoinDate || "N/A"}
2025-03-18 18:09:15 -06:00
</p>
2025-03-05 22:30:52 -07:00
</div>
</div>
</div>
</div>
</div>
</div>
2025-03-18 18:09:15 -06:00
2025-04-04 00:02:04 -06:00
{/* Reviews Section */}
<div className="mt-8">
<h2 className="text-xl font-bold text-gray-800 mb-4">Reviews</h2>
2025-03-05 22:30:52 -07:00
<div className="bg-white border border-gray-200 p-6">
2025-04-04 00:02:04 -06:00
{loading.reviews ? (
<div className="flex justify-center py-8">
2025-04-18 10:37:19 -06:00
<div className="animate-spin h-8 w-8 border-t-2 border-green-500"></div>
2025-04-04 00:02:04 -06:00
</div>
) : error.reviews ? (
<div className="text-red-500 mb-4">
Error loading reviews: {error.reviews}
</div>
) : reviews.length === 0 ? (
<div className="text-gray-500 text-center py-6">
No reviews yet for this product
</div>
) : (
<div className="space-y-6">
{reviews.map((review) => (
<div
key={review.id || `review-${Math.random()}`}
className="border-b border-gray-200 pb-6 last:border-0 last:pb-0"
>
<div className="flex justify-between mb-2">
<div className="font-medium text-gray-800">
2025-04-12 13:10:17 -06:00
{review.ReviewerName}
2025-04-04 00:02:04 -06:00
</div>
<div className="text-sm text-gray-500">
{review.ReviewDate
? new Date(review.ReviewDate).toLocaleDateString()
: "Unknown date"}
</div>
</div>
<div className="flex items-center mb-2">
{renderStars(review.Rating || 0)}
<span className="ml-2 text-sm text-gray-600">
{review.Rating || 0}/5
</span>
</div>
<div className="text-gray-700">
{review.Comment || "No comment provided"}
</div>
</div>
))}
</div>
)}
2025-03-05 22:30:52 -07:00
</div>
2025-04-04 00:02:04 -06:00
<div className="mt-4">
<button
onClick={() => setShowReviewForm(true)}
2025-04-18 10:37:19 -06:00
className="bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-4"
2025-04-04 00:02:04 -06:00
>
Write a Review
</button>
</div>
{/* Review Popup Form */}
{showReviewForm && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
2025-04-18 10:37:19 -06:00
<div className="bg-white shadow-xl max-w-md w-full p-6">
2025-04-04 00:02:04 -06:00
<div className="flex justify-between items-center mb-4">
<h3 className="text-xl font-bold text-gray-800">
Write a Review
</h3>
<button
onClick={() => setShowReviewForm(false)}
className="text-gray-500 hover:text-gray-700"
>
</button>
</div>
<form onSubmit={handleSubmitReview}>
<div className="mb-4">
<label className="block text-gray-700 mb-1">
Rating <span className="text-red-500">*</span>
</label>
<div className="flex items-center">
{[1, 2, 3, 4, 5].map((star) => (
<button
key={star}
type="button"
onClick={() => handleRatingChange(star)}
className="focus:outline-none mr-1"
>
<Star
className={`h-6 w-6 ${
star <= reviewForm.rating
? "text-yellow-400 fill-yellow-400"
: "text-gray-300"
}`}
/>
</button>
))}
<span className="ml-2 text-gray-600">
{reviewForm.rating}/5
</span>
</div>
</div>
<div className="mb-4">
<label htmlFor="comment" className="block text-gray-700 mb-1">
Your Review <span className="text-red-500">*</span>
</label>
<textarea
id="comment"
value={reviewForm.comment}
onChange={handleReviewInputChange}
2025-04-18 10:37:19 -06:00
className="w-full p-3 border border-gray-300 focus:outline-none focus:border-green-500"
2025-04-04 00:02:04 -06:00
rows="4"
required
></textarea>
</div>
<div className="flex justify-end space-x-3">
<button
type="button"
onClick={() => setShowReviewForm(false)}
2025-04-18 10:37:19 -06:00
className="px-4 py-2 border border-gray-300 text-gray-700 hover:bg-gray-100"
2025-04-04 00:02:04 -06:00
>
Cancel
</button>
<button
type="submit"
2025-04-18 10:37:19 -06:00
className="px-4 py-2 bg-green-500 text-white hover:bg-green-600"
2025-04-12 13:10:17 -06:00
disabled={loading.submitting}
2025-04-04 00:02:04 -06:00
>
2025-04-12 13:10:17 -06:00
{loading.submitting ? "Submitting..." : "Submit Review"}
2025-04-04 00:02:04 -06:00
</button>
</div>
</form>
</div>
</div>
)}
</div>
2025-03-05 22:30:52 -07:00
</div>
);
};
2025-03-18 18:09:15 -06:00
export default ProductDetail;