"use client" import { useState, useEffect } from "react" import { Minus, Plus, ArrowRight, X } from "lucide-react" import ProductVariantSelector from "./ProductVariantSelector" import NutritionalInfo from "./NutritionalInfo" import ImageGallery from "./ImageGallery" import "./product-details.css" import { useMediaQuery } from "react-responsive" import { useSearchParams } from "react-router-dom" import { useCart } from "../../context/CartContext" import Classic from "../../components/Classic/index" import 'react-loading-skeleton/dist/skeleton.css' import Preloader from "../Preloader" import { useSelector } from "react-redux" export default function Singleproduct() { // const [quantity, setQuantity] = useState(1) const [selectedVariant, setSelectedVariant] = useState(0) const [activeTab, setActiveTab] = useState("description") const [productData, setProductData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [animationClass, setAnimationClass] = useState("") const [relatedProducts, setRelatedProducts] = useState([]) const [loadingRelated, setLoadingRelated] = useState(true) const [showLoginModal, setShowLoginModal] = useState(false) const isDesktop = useMediaQuery({ minWidth: 768 }) const { cart, addToCart, updateQuantity } = useCart() const user = useSelector((state) => state.auth.user) // Get product ID from URL params const [searchParams] = useSearchParams() const productId = searchParams.get("id") const redirectToLogin = () => { window.location.href = "/login" } useEffect(() => { const fetchProductData = async () => { try { setLoading(true) const response = await fetch(`https://derbyb.ritaz.in/api/products/${productId}`) if (!response.ok) { throw new Error("Failed to fetch product data") } const data = await response.json() setProductData(data.product) setLoading(false) // After getting product data, fetch related products if (data.product && data.product.category) { fetchRelatedProducts(data.product.category, data.product._id) } } catch (err) { setError(err.message) setLoading(false) } } const fetchRelatedProducts = async (category, currentProductId) => { try { setLoadingRelated(true) // Fetch products with the same category but exclude the current product const response = await fetch(`https://derbyb.ritaz.in/api/products/category/${category}`, { headers: { "Content-Type": "application/json", }, method: "GET", }) if (!response.ok) { throw new Error("Failed to fetch related products") } const data = await response.json() // Filter out the current product from related products const filtered = data.products.filter(product => product._id !== currentProductId) setRelatedProducts(filtered) setLoadingRelated(false) } catch (err) { console.error("Error fetching related products:", err) setLoadingRelated(false) } } if (productId) { fetchProductData() } }, [productId]) if (loading) { return ( <> <Preloader /> </> ) } if (error) { return <div className="product_details_wrapper">Error: {error}</div> } if (!productData) { return <div className="product_details_wrapper">Product not found</div> } const currentVariant = productData.variants[selectedVariant] const handleAddToCart = () => { if (!user) { setShowLoginModal(true) return } // Add to cart using the context method addToCart(productData._id) // Set animation class setAnimationClass("counter-animation-up") // Reset animation after duration setTimeout(() => { setAnimationClass("") }, 500) } const increaseQuantity = () => { // Set animation class setAnimationClass("counter-animation-up") // Call the context method to update quantity updateQuantity(productData._id, (cart[productData._id] || 0) + 1) // Reset animation after duration setTimeout(() => { setAnimationClass("") }, 500) } const decreaseQuantity = () => { // Set animation class setAnimationClass("counter-animation-down") // Call the context method to update quantity updateQuantity(productData._id, (cart[productData._id] || 0) - 1) // Reset animation after duration setTimeout(() => { setAnimationClass("") }, 500) } const images = [productData.mainImage, ...productData.otherImages] return ( <div> {showLoginModal && ( <div className="login-modal-overlay"> <div className="login-modal"> <div className="login-modal-close" onClick={() => setShowLoginModal(false)}> <X size={24} /> </div> <h3 className="login-modal-title">Login Required</h3> <div className="login-modal-content"> <p>Please login to add items to your cart.</p> </div> <div className="login-modal-buttons"> <button className="login-button" onClick={redirectToLogin}> Login </button> <button className="cancel-button" onClick={() => setShowLoginModal(false)}> Cancel </button> </div> </div> </div> )} <div className="product_details_wrapper" style={{ margin: isDesktop ? "100px" : "10px" }}> {/* Main Product Section with Sticky Left Side */} <div className="product_details_main_section"> {/* Left Column - Product Images (Sticky) */} <div className="product_details_left_column"> <div className="product_details_sticky_container"> <div className="product_details_image_container"> <ImageGallery images={images} variant={currentVariant.flavor} /> </div> {/* Mobile-only tabs navigation */} <div className="product_details_mobile_tabs"> <div className="product_details_tabs_header"> <button className={`product_details_tab_btn ${activeTab === "description" ? "product_details_tab_active" : ""}`} onClick={() => setActiveTab("description")} > Description </button> <button className={`product_details_tab_btn ${activeTab === "additional" ? "product_details_tab_active" : ""}`} onClick={() => setActiveTab("additional")} > Additional Information </button> </div> </div> </div> </div> {/* Right Column - Product Info and Tabs */} <div className="product_details_right_column"> {/* Product Info */} <div className="product_details_info_container"> {/* Rating */} <div className="product_details_rating"> {[1, 2, 3, 4, 5].map((star) => ( <svg key={star} className="product_details_star" fill="currentColor" viewBox="0 0 20 20"> <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" /> </svg> ))} <span className="product_details_rating_text">(4.9/5)</span> </div> {/* Product Title */} <h1 className="product_details_title">{productData.name}</h1> {/* Price */} <div className="product_details_price_container"> <span className="product_details_current_price">₹{productData.sellingPrice}</span> <span className="product_details_original_price">₹{productData.mrp}</span> <span className="product_details_discount_badge"> {Math.round((1 - productData.sellingPrice / productData.mrp) * 100)}% OFF </span> </div> {/* Discount Badge */} {productData.discounts && productData.discounts !== "none" && ( <div className="product_details_offer_badge_container"> <span className="product_details_offer_badge">{productData.discounts}</span> </div> )} {/* Description */} <p className="product_details_description">{productData.description.substring(0, 150)}...</p> {/* Variant Selector */} <ProductVariantSelector variants={productData.variants.map((v) => v.flavor)} selectedVariant={selectedVariant} onSelectVariant={setSelectedVariant} /> {/* Quantity and Add to Cart */} <div className="product_details_actions"> {cart && cart[productData._id] ? ( <div className="quantity-selector" style={{ display: "flex", alignItems: "center", backgroundColor: "#fff", border: "1px solid #e0e0e0", borderRadius: "50px", padding: "3px", boxShadow: "0 2px 5px rgba(0,0,0,0.1)", width: "fit-content", }} > <button onClick={decreaseQuantity} style={{ width: "28px", height: "28px", border: "none", backgroundColor: "transparent", color: "#333", borderRadius: "50%", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", fontWeight: "bold", fontSize: "16px", transition: "all 0.2s ease", }} > <Minus size={16} /> </button> <span className={animationClass} style={{ margin: "0 15px", fontWeight: "400", color: "black", fontSize: "16px", }} > {cart[productData._id]} </span> <button onClick={increaseQuantity} style={{ width: "28px", height: "28px", border: "none", backgroundColor: "transparent", color: "#333", borderRadius: "50%", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", fontWeight: "bold", fontSize: "16px", transition: "all 0.2s ease", }} > <Plus size={16} /> </button> </div> ) : ( <button className="product_details_add_to_cart_btn" onClick={handleAddToCart} style={{ background: "linear-gradient(90deg, #E0A323 0%, #FEBA2A 100%)", color: "#fff", border: "none", borderRadius: "50px", padding: "8px 15px", fontSize: "14px", fontWeight: "bold", cursor: "pointer", transition: "all 0.3s ease", boxShadow: "0 2px 5px rgba(0,0,0,0.1)", display: "flex", alignItems: "center", gap: "6px", }} > Add to Cart <ArrowRight className="product_details_icon" /> </button> )} </div> </div> {/* Desktop Tabs - Now on the right side */} <div className="product_details_desktop_tabs"> <div className="product_details_tabs_container"> <div className="product_details_tabs_header"> <button className={`product_details_tab_btn ${activeTab === "description" ? "product_details_tab_active" : ""}`} onClick={() => setActiveTab("description")} > Description </button> <button className={`product_details_tab_btn ${activeTab === "additional" ? "product_details_tab_active" : ""}`} onClick={() => setActiveTab("additional")} > Additional Information </button> </div> <div className="product_details_tab_content"> {activeTab === "description" && ( <div className="product_details_description_content"> <h3 className="product_details_content_title">Product Description</h3> <p>{productData.description}</p> <p> Package Quantity: {productData.packageSize} Psc. <br /> Package Weight: {productData.packageWeight} </p> </div> )} {activeTab === "additional" && ( <div className="product_details_additional_content"> <div className="product_details_info_section"> <h3 className="product_details_content_title">Ingredients</h3> <p className="product_details_info_text">{currentVariant.ingredients}</p> </div> <div className="product_details_info_section"> <h3 className="product_details_content_title">Allergen Information</h3> <p className="product_details_allergen_text">{currentVariant.allergenInfo}</p> </div> <div className="product_details_info_section"> <h3 className="product_details_content_title">Packaging</h3> <p className="product_details_info_text">{currentVariant.packagingOption}</p> </div> <div className="product_details_info_section"> <h3 className="product_details_content_title">Nutritional Information</h3> <NutritionalInfo nutritionalFacts={currentVariant.nutritionalFacts} /> </div> </div> )} {activeTab === "reviews" && ( <div className="product_details_reviews_content"> <h3 className="product_details_content_title">Customer Reviews</h3> <p className="product_details_no_reviews">No reviews yet. Be the first to review this product!</p> </div> )} </div> </div> </div> </div> </div> {/* Mobile-only tab content */} <div className="product_details_mobile_tab_content"> {activeTab === "description" && ( <div className="product_details_description_content"> <h3 className="product_details_content_title">Product Description</h3> <p>{productData.description}</p> <p> Package Quantity: {productData.packageSize} <br /> Package Weight: {productData.packageWeight} </p> </div> )} {activeTab === "additional" && ( <div className="product_details_additional_content"> <div className="product_details_info_section"> <h3 className="product_details_content_title">Ingredients</h3> <p className="product_details_info_text">{currentVariant.ingredients}</p> </div> <div className="product_details_info_section"> <h3 className="product_details_content_title">Allergen Information</h3> <p className="product_details_allergen_text">{currentVariant.allergenInfo}</p> </div> <div className="product_details_info_section"> <h3 className="product_details_content_title">Packaging</h3> <p className="product_details_info_text">{currentVariant.packagingOption}</p> </div> <div className="product_details_info_section"> <h3 className="product_details_content_title">Nutritional Information</h3> <NutritionalInfo nutritionalFacts={currentVariant.nutritionalFacts} /> </div> </div> )} {activeTab === "reviews" && ( <div className="product_details_reviews_content"> <h3 className="product_details_content_title">Customer Reviews</h3> <p className="product_details_no_reviews">No reviews yet. Be the first to review this product!</p> </div> )} </div> <hr /> </div> {/* Related Products Section */} {relatedProducts.length > 0 && ( <div className="related-products-section"> <Classic title="single-product" products={relatedProducts} loading={loadingRelated} error={error} /> </div> )} </div> ) } for mobile Description Additional Information display after add to cart button only for mobile view and don't use next js and typwScript /* Main Container */ .product_details_container { max-width: 1200px; margin: 0 auto; padding: 2rem 1rem; } .product_details_wrapper { display: flex; flex-direction: column; gap: 2rem; } /* Main Product Section - Updated for side-by-side layout */ .product_details_main_section { display: flex; flex-direction: column; gap: 2rem; } @media (min-width: 1024px) { .product_details_main_section { flex-direction: row; align-items: flex-start; } } /* Left Column - Sticky */ .product_details_left_column { width: 100%; } @media (min-width: 1024px) { .product_details_left_column { width: 40%; position: sticky; top: 2rem; align-self: flex-start; height: fit-content; } } /* Right Column */ .product_details_right_column { width: 100%; } @media (min-width: 1024px) { .product_details_right_column { width: 60%; padding-left: 2rem; } } /* Product Images */ .product_details_image_container { width: 100%; margin-bottom: 1.5rem; } /* Image Gallery */ .product_details_gallery_container { display: flex; flex-direction: column; gap: 1rem; } .product_details_main_image_container { position: relative; overflow: hidden; background-color: #f9fafb; } .product_details_main_image_wrapper { position: relative; width: 100%; aspect-ratio: 1 / 1; cursor: zoom-in; border: 1px solid black; display: flex; align-items: center; justify-content: center; } .product_details_main_image { width: 100%; height: 100%; object-fit: contain; } .product_details_zoom_indicator { position: absolute; bottom: 0.75rem; right: 0.75rem; background-color: rgba(255, 255, 255, 0.8); padding: 0.5rem; border-radius: 9999px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); display: flex; align-items: center; justify-content: center; } @media (min-width: 768px) { .product_details_zoom_indicator { display: none; } } .product_details_zoom_icon { height: 1.25rem; width: 1.25rem; color: #374151; } .product_details_zoom_overlay { position: absolute; inset: 0; background-color: white; pointer-events: none; } .product_details_thumbnails_container { display: flex; gap: 0.5rem; overflow-x: auto; padding-bottom: 0.5rem; } @media (min-width: 768px) { .product_details_thumbnails_container { overflow-x: visible; padding-bottom: 0; } } .product_details_thumbnail_btn { position: relative; min-width: 4rem; width: 4rem; height: 4rem; border-radius: 0.375rem; overflow: hidden; transition: all 0.2s; border: 1px solid #e5e7eb; opacity: 0.7; } .product_details_thumbnail_selected { border: 2px solid #ec4899; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); opacity: 1; } .product_details_thumbnail_img { width: 100%; height: 100%; object-fit: cover; } /* Product Info */ .product_details_info_container { width: 100%; margin-bottom: 2rem; } .product_details_rating { display: flex; align-items: center; margin-bottom: 0.5rem; } .product_details_star { width: 1.25rem; height: 1.25rem; color: #fbbf24; } .product_details_rating_text { margin-left: 0.5rem; font-size: 0.875rem; color: #4b5563; } .product_details_title { font-size: 1.875rem; font-weight: 700; color: #1f2937; margin-bottom: 0.5rem; } .product_details_price_container { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 1rem; } .product_details_current_price { font-size: 1.5rem; font-weight: 700; color: #111827; } .product_details_original_price { font-size: 1.125rem; color: #6b7280; text-decoration: line-through; } .product_details_discount_badge { background-color: #fce7f3; color: #be185d; padding: 0.25rem 0.5rem; border-radius: 0.25rem; font-size: 0.75rem; font-weight: 500; } .product_details_offer_badge_container { margin-bottom: 1.5rem; } .product_details_offer_badge { border: 1px solid #ec4899; color: #ec4899; padding: 0.25rem 0.5rem; border-radius: 0.25rem; font-size: 0.875rem; } .product_details_description { color: #4b5563; margin-bottom: 1.5rem; line-height: 1.5; } /* Variant Selector */ .product_details_variant_container { display: flex; flex-direction: column; gap: 0.75rem; } .product_details_variant_title { font-weight: 500; } .product_details_variant_options { display: flex; flex-wrap: wrap; gap: 0.5rem; } .product_details_variant_btn { border-radius: 0.375rem; border: 1px solid #e5e7eb; background-color: transparent; /* cursor: pointer; */ transition: all 0.2s; font-size: 20px; } .product_details_variant_selected { /* background-color: #ec4899; */ color: #4b5563; border-color: transparent; } .product_details_variant_btn:hover:not(.product_details_variant_selected) { background-color: #f3f4f6; } /* Actions */ .product_details_actions { display: flex; align-items: center; gap: 1rem; margin-top: 2rem; } /* Login modal styles */ .login-modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 1000; } .login-modal { background-color: white; border-radius: 8px; padding: 20px; width: 90%; max-width: 400px; position: relative; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); } .login-modal-close { position: absolute; top: 10px; right: 10px; cursor: pointer; color: #666; } .login-modal-title { margin-top: 10px; margin-bottom: 20px; font-size: 24px; font-weight: 600; color: #333; } .login-modal-content p { margin-bottom: 20px; font-size: 16px; color: #555; } .login-modal-buttons { display: flex; justify-content: space-between; margin-top: 20px; } .login-button { padding: 10px 55px; background: linear-gradient(90deg, #E0A323 0%, #FEBA2A 100%); color: white; border: none; border-radius: 4px; font-weight: 600; cursor: pointer; min-width: 100px; } .cancel-button { padding: 10px 20px; background-color: #f1f1f1; color: #333; border: none; border-radius: 4px; font-weight: 600; cursor: pointer; min-width: 100px; } .product_details_quantity_selector { display: flex; align-items: center; border: 1px solid #e5e7eb; border-radius: 0.375rem; } .product_details_quantity_btn { padding: 0.5rem 0.75rem; display: flex; align-items: center; justify-content: center; cursor: pointer; background: none; border: none; } .product_details_quantity_btn:first-child { border-right: 1px solid #e5e7eb; } .product_details_quantity_btn:last-child { border-left: 1px solid #e5e7eb; } .product_details_quantity_value { padding: 0.5rem 1rem; } .product_details_add_to_cart_btn { background-image: linear-gradient(90deg, #E0A323 0%, #FEBA2A 100%); color: white; padding: 0.5rem 1.5rem; border-radius: 0.375rem; display: flex; align-items: center; gap: 0.5rem; cursor: pointer; border: none; font-weight: 500; transition: background-image 0.2s; border-radius: 30px; } .product_details_add_to_cart_btn:hover{ color: var(--e-global-color-white) !important; background-color: var(--e-global-color-dark-moderate-violet); } .product_details_add_to_cart_btn:hover .product_details_icon { transform: translateX(4px); transition: transform 0.5s; } .product_details_icon { height: 1rem; width: 1rem; } /* Tabs - Updated for desktop and mobile */ .product_details_desktop_tabs { display: none; } .product_details_mobile_tabs { display: block; margin-top: 1rem; } .product_details_mobile_tab_content { display: block; margin-top: 1rem; } @media (min-width: 1024px) { .product_details_desktop_tabs { display: block; } .product_details_mobile_tabs, .product_details_mobile_tab_content { display: none; } } .product_details_tabs_container { width: 100%; } .product_details_tabs_header { display: flex; border-bottom: 1px solid #e5e7eb; margin-bottom: 1.5rem; overflow-x: auto; } .product_details_tab_btn { padding: 0.75rem 1rem; background: transparent; border: none; cursor: pointer; font-size: 1rem; color: #6b7280; white-space: nowrap; } .product_details_tab_active { border-bottom: 2px solid #ec4899; color: #ec4899; font-weight: 500; } .product_details_tab_content { padding-top: 1rem; } .product_details_content_title { font-size: 1.125rem; font-weight: 500; margin-bottom: 0.5rem; } .product_details_description_content, .product_details_additional_content, .product_details_reviews_content { display: flex; flex-direction: column; gap: 1rem; } .product_details_info_section { margin-bottom: 1.5rem; } .product_details_info_text { color: #4b5563; line-height: 1.5; } .product_details_allergen_text { color: #4b5563; font-weight: 600; line-height: 1.5; } .product_details_no_reviews { color: #6b7280; text-align: center; padding: 2rem 0; } /* Nutritional Table */ .product_details_nutrition_table_container { border: 1px solid #e5e7eb; border-radius: 0.375rem; overflow: hidden; overflow-x: auto; } .product_details_nutrition_table { width: 100%; font-size: 0.875rem; border-collapse: collapse; min-width: 600px; } .product_details_nutrition_header { background-color: #f3f4f6; } .product_details_nutrition_header_cell { padding: 0.5rem 1rem; text-align: left; font-weight: 600; } .product_details_nutrition_body { width: 100%; } .product_details_nutrition_row { border-top: 1px solid #e5e7eb; } .product_details_nutrition_row_main { border-top: 2px solid #111827; } .product_details_nutrition_cell { padding: 0.5rem 1rem; } .product_details_nutrition_cell_right { text-align: right; } .product_details_nutrition_cell_indent { padding-left: 2rem; } /* Responsive adjustments */ @media (max-width: 640px) { .product_details_actions { flex-direction: column; align-items: stretch; } .product_details_quantity_selector { width: 100%; justify-content: space-between; } .product_details_add_to_cart_btn { width: 100%; justify-content: center; } }
Looking at your code and the screenshot, I'll help you reorganize the mobile view to display the Description and Additional Information tabs after the Add to Cart button, while keeping the desktop view as is.
isMobile flag from useMediaQuery to conditionally render the tabs in different positionsThe implementation now matches the screenshot you provided, with the Description and Additional Information tabs appearing after the Add to Cart button on mobile devices only.
useMediaQuery to detect if the user is on a mobile deviceisMobile is true), the tabs and content are rendered after the Add to Cart buttonThis approach ensures a better mobile user experience while maintaining the desktop layout you already had.