Untitled
unknown
plain_text
10 months ago
32 kB
6
Indexable
import { useParams, useNavigate } from "react-router-dom";
import "../assets/styles/ProductDetail.scss";
import Main from "../utils/container";
import { useState, useEffect } from "react";
import { Icon } from "../assets/icon/icons";
import { productService, cartService } from "../services/api.service";
import { useNotification } from "../components/Notification/Notification";
import OptionCart from "../pages/User/OptionCart";
export default function ProductDetail() {
const { productId } = useParams();
const navigate = useNavigate();
const { showNotification } = useNotification();
// All state hooks
const [activeTab, setActiveTab] = useState('details');
const [selectedFilter, setSelectedFilter] = useState("All");
const [showWithMedia, setShowWithMedia] = useState(false);
const [liked, setLiked] = useState(false);
const [product, setProduct] = useState(null);
const [variants, setVariants] = useState([]);
const [selectedVariant, setSelectedVariant] = useState(null);
const [showOptions, setShowOptions] = useState(false);
const [selectedTags, setSelectedTags] = useState({
Size: "",
Color: "",
Material: ""
});
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [quantity, setQuantity] = useState(1);
const [currentImageIndex, setCurrentImageIndex] = useState(0);
// useEffect hook
useEffect(() => {
const fetchProduct = async () => {
try {
const [productData, variantsData] = await Promise.all([
productService.getProductDetail(productId),
cartService.getProductVariants(productId)
]);
setProduct(productData);
const variantArray = variantsData?.productVariants || [];
setVariants(variantArray);
if (variantArray.length > 0) {
setSelectedVariant(variantArray[0]);
setSelectedTags({
Size: variantArray[0].optionValue1,
Color: variantArray[0].optionValue2,
Material: variantArray[0].optionValue3
});
}
const userId = localStorage.getItem('userId');
if (userId) {
const wishlistData = await productService.getWishlistByUser(userId);
setLiked(wishlistData.some(item => item.productId === productId));
}
} catch (err) {
console.error('Failed to fetch product:', err);
setError(err.message || 'Failed to load product details');
} finally {
setLoading(false);
}
};
fetchProduct();
}, [productId]);
// Computed values
const filteredReviews = product?.reviews ? product.reviews.filter((review) => {
const matchRating = selectedFilter === "All" || review.rating === Number(selectedFilter);
const matchMedia = !showWithMedia || review.images?.length > 0;
return matchRating && matchMedia;
}) : [];
// Event handlers and other functions
const getCategories = () => {
if (!variants || variants.length === 0) return [];
const categories = [];
const optionMap = new Map();
// Collect all unique options and their values from variants
variants.forEach(variant => {
for (let i = 1; i <= 5; i++) {
const optionName = variant[`option${i}`];
const optionValue = variant[`optionValue${i}`];
if (optionName && optionValue) {
if (!optionMap.has(optionName)) {
optionMap.set(optionName, new Set());
}
optionMap.get(optionName).add(optionValue);
}
}
});
// Convert to categories format
optionMap.forEach((values, name) => {
// Filter variants based on currently selected options
const compatibleVariants = variants.filter(variant => {
return Object.entries(selectedTags).every(([tagName, tagValue]) => {
if (!tagValue || tagName === name) return true;
// Find which option number this tag corresponds to
for (let i = 1; i <= 5; i++) {
if (variant[`option${i}`] === tagName) {
return variant[`optionValue${i}`] === tagValue;
}
}
return true;
});
});
// Get available values for this option based on compatible variants
const availableValues = new Set();
compatibleVariants.forEach(variant => {
for (let i = 1; i <= 5; i++) {
if (variant[`option${i}`] === name && variant.quantity > 0) {
availableValues.add(variant[`optionValue${i}`]);
}
}
});
categories.push({
name,
tags: Array.from(values),
availableTags: Array.from(availableValues)
});
});
return categories;
};
const handleOptionChange = (optionName, value) => {
const newSelectedTags = {
...selectedTags,
[optionName]: value
};
// Reset all options that come after the current one in the variant's order
const currentOptionIndex = variants.find(v => {
for (let i = 1; i <= 5; i++) {
if (v[`option${i}`] === optionName) return true;
}
return false;
});
if (currentOptionIndex) {
variants.forEach(variant => {
for (let i = 1; i <= 5; i++) {
const optName = variant[`option${i}`];
if (optName && optName !== optionName) {
let shouldReset = true;
// Check if this option comes after the current one in any variant
for (let j = 1; j <= 5; j++) {
if (variant[`option${j}`] === optionName) {
shouldReset = false;
break;
}
if (variant[`option${j}`] === optName) {
shouldReset = true;
break;
}
}
if (shouldReset) {
newSelectedTags[optName] = "";
}
}
}
});
}
setSelectedTags(newSelectedTags);
};
const handleOptionSave = () => {
const matchingVariant = variants.find(variant => {
return Object.entries(selectedTags).every(([tagName, tagValue]) => {
if (!tagValue) return true;
// Find which option number this tag corresponds to
for (let i = 1; i <= 5; i++) {
if (variant[`option${i}`] === tagName) {
return variant[`optionValue${i}`] === tagValue;
}
}
return true;
});
});
if (matchingVariant) {
setSelectedVariant(matchingVariant);
setShowOptions(false);
showNotification("Options updated successfully", "success");
}
};
const handleAddToCart = async () => {
const userId = localStorage.getItem('userId');
if (!userId) {
showNotification("Please login to add items to cart!", "error");
navigate('/login');
return;
}
if (!selectedVariant) {
showNotification("Please select product options!", "error");
return;
}
if (quantity > selectedVariant.quantity) {
showNotification("Sorry, we don't have enough stock for that quantity.", "error");
return;
}
try {
await cartService.addCart({
productId: product.productId,
variantId: selectedVariant.variantId,
quantity: quantity
});
showNotification("Product added to cart successfully!", "success");
navigate("/cart");
} catch (err) {
console.error('Failed to add to cart:', err);
showNotification(err.message || "Failed to add to cart. Please try again.", "error");
}
};
const handleLike = async () => {
const userId = localStorage.getItem('userId');
if (!userId) {
showNotification("Please login to add items to wishlist", "error");
navigate('/login');
return;
}
try {
if (!liked) {
await productService.createWishlist(userId, product.productId);
setLiked(true);
showNotification("Product added to Wishlist!", "success");
} else {
await productService.removeWishlist(userId, product.productId);
setLiked(false);
showNotification("Product removed from Wishlist!", "success");
}
} catch (err) {
console.error('Failed to update wishlist:', err);
showNotification(err.message || "Failed to update wishlist. Please try again.", "error");
}
};
// Loading and error states
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
if (!product) return <p>Product not found</p>;
return (
<Main>
<div className="product-container">
{/* Breadcrumb Navigation */}
<div className="breadcrumb">
<span onClick={() => navigate('/')}>Home</span>
<span className="separator">/</span>
<span onClick={() => navigate('/products')}>Products</span>
<span className="separator">/</span>
<span className="current">{product.productName}</span>
</div>
{/* Product Main Section */}
<div className="product-detail">
{/* Left Column - Images */}
<div className="product-detail__image">
{selectedVariant ? (
<>
<div className="main-image">
<img
src={selectedVariant.urlImage}
alt={`${product.productName} - ${selectedVariant.optionValue2}`}
/>
</div>
</>
) : (
<div className="no-image-message">
<span className="no-image-icon">🖼️</span>
<p>Please select product options to view image</p>
</div>
)}
</div>
{/* Right Column - Product Info */}
<div className="product-detail__info">
<div className="info-header">
<h1 className="product-name">{product.productName}</h1>
<div className="price-container">
<span className="price-label">Price:</span>
<span className="price-value">
{selectedVariant ? `${selectedVariant.price.toLocaleString()} VND` : 'Select options'}
</span>
</div>
</div>
<div className="info-section description-section">
<h2 className="section-title">Description</h2>
<p className="description-text">{product.description}</p>
</div>
<div className="info-section options-section">
<h2 className="section-title" style={{
textAlign: "center",
marginBottom: "20px",
fontSize: "20px",
color: "#333"
}}>Product Options</h2>
{variants && variants.length > 0 ? (
<>
<button
className="select-options-button"
onClick={() => setShowOptions(true)}
style={{
border: selectedVariant
? "1px solid #ff385c"
: "1px solid #d9d9d9",
padding: "4px 12px",
borderRadius: "16px",
cursor: "pointer",
transition: "all 0.3s",
backgroundColor: selectedVariant
? "#ff385c"
: "transparent",
color: selectedVariant ? "white" : "#333",
marginBottom: "4px",
fontSize: "14px",
display: "flex",
alignItems: "center",
gap: "8px",
width: "fit-content"
}}
>
<span className="button-icon">⚙️</span>
<span className="button-text">
{selectedVariant ? 'Change Options' : 'Select Options'}
</span>
</button>
{showOptions && (
<div className="options-overlay">
<OptionCart
categories={getCategories()}
selectedTags={selectedTags}
onChange={handleOptionChange}
onSave={handleOptionSave}
onCancel={() => setShowOptions(false)}
currentVariants={variants}
/>
</div>
)}
{selectedVariant && (
<div className="selected-options" style={{
marginTop: "1rem",
padding: "12px",
backgroundColor: "#f5f5f5",
borderRadius: "8px"
}}>
{selectedVariant.optionValue1 && (
<div className="option-item" style={{
display: "flex",
justifyContent: "space-between",
marginBottom: "8px",
padding: "4px 0"
}}>
<span className="option-label" style={{
color: "#333",
fontWeight: "500"
}}>Size:</span>
<span className="option-value" style={{
color: "#ff385c",
fontWeight: "600"
}}>{selectedVariant.optionValue1}</span>
</div>
)}
{selectedVariant.optionValue2 && (
<div className="option-item" style={{
display: "flex",
justifyContent: "space-between",
marginBottom: "8px",
padding: "4px 0"
}}>
<span className="option-label" style={{
color: "#333",
fontWeight: "500"
}}>Color:</span>
<span className="option-value" style={{
color: "#ff385c",
fontWeight: "600"
}}>{selectedVariant.optionValue2}</span>
</div>
)}
{selectedVariant.optionValue3 && (
<div className="option-item" style={{
display: "flex",
justifyContent: "space-between",
marginBottom: "8px",
padding: "4px 0"
}}>
<span className="option-label" style={{
color: "#333",
fontWeight: "500"
}}>Material:</span>
<span className="option-value" style={{
color: "#ff385c",
fontWeight: "600"
}}>{selectedVariant.optionValue3}</span>
</div>
)}
<div style={{
display: "flex",
justifyContent: "space-between",
marginTop: "12px",
paddingTop: "12px",
borderTop: "1px solid #ddd"
}}>
<span style={{ color: "#333", fontWeight: "500" }}>Available:</span>
<span style={{ color: "#333" }}>
{selectedVariant.quantity} items
</span>
</div>
</div>
)}
</>
) : (
<div className="no-options-message" style={{
padding: "1.5rem",
background: "#f8f8f8",
borderRadius: "8px",
textAlign: "center",
margin: "1rem 0",
border: "1px dashed #ddd"
}}>
<span className="no-options-icon" style={{
fontSize: "2rem",
display: "block",
marginBottom: "0.5rem"
}}>ℹ️</span>
<p style={{
margin: 0,
color: "#666",
fontSize: "0.95rem"
}}>This product does not have any variants or options available.</p>
</div>
)}
<div className="quantity-section" style={{
marginTop: "1.5rem"
}}>
<div className="quantity-container" style={{
display: "flex",
alignItems: "center",
gap: "1rem"
}}>
<label className="quantity-label" style={{
display: "flex",
alignItems: "center",
gap: "0.5rem"
}}>
<span className="label-text" style={{
color: "#333",
fontWeight: "500"
}}>Quantity:</span>
<div className="quantity-input-group" style={{
display: "flex",
alignItems: "center",
border: "1px solid #d9d9d9",
borderRadius: "16px",
overflow: "hidden"
}}>
<button
className="quantity-btn"
onClick={() => quantity > 1 && setQuantity(quantity - 1)}
disabled={quantity <= 1}
style={{
padding: "4px 12px",
border: "none",
background: "transparent",
cursor: "pointer",
color: "#ff385c"
}}
>
-
</button>
<input
type="number"
min="1"
max={selectedVariant?.quantity || 1}
value={quantity}
onChange={(e) => setQuantity(Number(e.target.value))}
className="quantity-input"
style={{
width: "50px",
border: "none",
textAlign: "center",
fontSize: "14px"
}}
/>
<button
className="quantity-btn"
onClick={() => quantity < (selectedVariant?.quantity || 1) && setQuantity(quantity + 1)}
disabled={quantity >= (selectedVariant?.quantity || 1)}
style={{
padding: "4px 12px",
border: "none",
background: "transparent",
cursor: "pointer",
color: "#ff385c"
}}
>
+
</button>
</div>
</label>
</div>
</div>
</div>
<div className="info-section action-section" style={{
marginTop: "1.5rem",
display: "flex",
gap: "1rem"
}}>
<button
className="add-to-cart-button"
onClick={handleAddToCart}
disabled={!selectedVariant || selectedVariant.quantity === 0}
style={{
flex: 1,
padding: "12px 24px",
borderRadius: "20px",
border: "none",
background: selectedVariant ? "#ff385c" : "#f5f5f5",
color: selectedVariant ? "white" : "#999",
cursor: selectedVariant ? "pointer" : "not-allowed",
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: "8px",
fontSize: "16px",
fontWeight: "500",
transition: "all 0.3s"
}}
>
<span className="button-icon">🛒</span>
<span className="button-text">
{!selectedVariant ? "Select Options" :
selectedVariant.quantity === 0 ? "Out of Stock" :
"Add to Cart"}
</span>
</button>
<div className="action-buttons" style={{
display: "flex",
gap: "0.5rem"
}}>
<button
className="like-button"
onClick={handleLike}
title={liked ? "Remove from Wishlist" : "Add to Wishlist"}
style={{
padding: "12px",
borderRadius: "50%",
border: "1px solid #d9d9d9",
background: liked ? "#ff385c" : "transparent",
color: liked ? "white" : "#666",
cursor: "pointer",
transition: "all 0.3s"
}}
>
<span className="button-icon">{liked ? '❤️' : '🤍'}</span>
</button>
<button
className="share-button"
onClick={() => showNotification("Sharing feature coming soon!", "info")}
title="Share Product"
style={{
padding: "12px",
borderRadius: "50%",
border: "1px solid #d9d9d9",
background: "transparent",
color: "#666",
cursor: "pointer",
transition: "all 0.3s"
}}
>
<span className="button-icon">🔗</span>
</button>
</div>
</div>
</div>
</div>
{/* Product Details Tabs */}
<div className="product-tabs">
<div className="tab-headers">
<button
className={`tab-button ${activeTab === 'details' ? 'active' : ''}`}
onClick={() => setActiveTab('details')}
>
Product Details
</button>
<button
className={`tab-button ${activeTab === 'reviews' ? 'active' : ''}`}
onClick={() => setActiveTab('reviews')}
>
Reviews ({filteredReviews.length})
</button>
<button
className={`tab-button ${activeTab === 'shipping' ? 'active' : ''}`}
onClick={() => setActiveTab('shipping')}
>
Shipping Info
</button>
</div>
<div className="tab-content">
{activeTab === 'details' && (
<div className="details-tab">
<div className="product-features">
<h3>Product Features</h3>
<ul>
<li>High-quality materials</li>
<li>Comfortable fit</li>
<li>Easy to maintain</li>
<li>Stylish design</li>
</ul>
</div>
<div className="product-specs">
<h3>Specifications</h3>
<table>
<tbody>
<tr>
<td>Brand</td>
<td>{product.brand}</td>
</tr>
<tr>
<td>Material</td>
<td>{selectedVariant?.optionValue3 || 'Various options available'}</td>
</tr>
<tr>
<td>Available Colors</td>
<td>{Array.from(new Set(variants.map(v => v.optionValue2))).join(', ')}</td>
</tr>
</tbody>
</table>
</div>
</div>
)}
{activeTab === 'reviews' && (
<div className="reviews-tab">
<div className="reviews-overview">
<div className="review-header">
<h2>Reviews</h2>
<div className="rating-summary">
<h3>Average Rating</h3>
{filteredReviews.length > 0 ? (
<div className="rating-stats">
<div className="rating-number">
{(
filteredReviews.reduce(
(total, review) => total + review.rating,
0
) / filteredReviews.length
).toFixed(1)}
<span className="rating-max">★/5.0★</span>
</div>
<div className="review-count">
({filteredReviews.length} reviews)
</div>
<div className="star-display">
{Array.from({ length: 5 }).map((_, index) => {
const rating =
filteredReviews.reduce(
(total, review) => total + review.rating,
0
) / filteredReviews.length;
return (
<span key={index} className="star">
{index < Math.floor(rating) ? "★" : "☆"}
</span>
);
})}
</div>
</div>
) : (
<div className="no-ratings">No ratings yet</div>
)}
</div>
</div>
<div className="filters">
<div className="filter-group">
<label className="filter-rating">
<span className="filter-label">Filter by Rating:</span>
<select
className="rating-select"
value={selectedFilter}
onChange={(e) => setSelectedFilter(e.target.value)}
>
<option value="All">All Ratings</option>
{[5, 4, 3, 2, 1].map((star) => {
const count = filteredReviews.filter(
(review) => review.rating === star
).length;
return (
<option key={star} value={star}>
{star} ★ ({count})
</option>
);
})}
</select>
</label>
<label className="show-with-media-container">
<input
type="checkbox"
className="show-with-media"
checked={showWithMedia}
onChange={(e) => setShowWithMedia(e.target.checked)}
/>
<span>Show Reviews with Media</span>
</label>
</div>
</div>
<div className="reviews-list">
{filteredReviews.length > 0 ? (
filteredReviews.map((review, index) => (
<div key={index} className="review-card">
<div className="review-header">
<div className="reviewer-info">
<strong>{review.username}</strong>
<div className="review-rating">
{Array.from({ length: 5 }).map((_, i) => (
<span key={i} className={`star ${i < review.rating ? 'filled' : ''}`}>
{i < review.rating ? '★' : '☆'}
</span>
))}
</div>
</div>
</div>
<div className="review-content">
<p>{review.comment}</p>
{review.images?.length > 0 && (
<div className="review-images">
{review.images.map((img, imgIndex) => (
<img
key={imgIndex}
src={img}
alt="Review Media"
onClick={() => {/* TODO: Add image preview */}}
/>
))}
</div>
)}
</div>
</div>
))
) : (
<div className="no-reviews">
<p>No reviews match your filters.</p>
</div>
)}
</div>
</div>
</div>
)}
{activeTab === 'shipping' && (
<div className="shipping-tab">
<h3>Shipping Information</h3>
<div className="shipping-info">
<div className="shipping-item">
<span className="icon">🚚</span>
<h4>Free Shipping</h4>
<p>On orders over 1,000,000 VND</p>
</div>
<div className="shipping-item">
<span className="icon">⏱️</span>
<h4>Delivery Time</h4>
<p>2-4 business days</p>
</div>
<div className="shipping-item">
<span className="icon">↩️</span>
<h4>Returns</h4>
<p>30-day easy returns</p>
</div>
</div>
</div>
)}
</div>
</div>
{/* Related Products Section */}
<div className="related-products">
<h2>You May Also Like</h2>
<div className="related-products-grid">
{/* Add related products here */}
</div>
</div>
</div>
</Main>
);
}
Editor is loading...
Leave a Comment