Untitled
unknown
plain_text
2 months ago
32 kB
3
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