Skip to content

Instantly share code, notes, and snippets.

@parvezpreo
Created April 23, 2026 13:23
Show Gist options
  • Select an option

  • Save parvezpreo/5d1ddf3a61ffcde3ffa26eeba4c9f9b1 to your computer and use it in GitHub Desktop.

Select an option

Save parvezpreo/5d1ddf3a61ffcde3ffa26eeba4c9f9b1 to your computer and use it in GitHub Desktop.
React js ProductDetails.jsx codes file
import React, { useState, useEffect, useMemo } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import useSWR from 'swr';
import ReactMarkdown from 'react-markdown';
import {
ShoppingCart, Zap, AlertCircle,
Minus, Plus, Phone, MessageCircle, MessageSquare // 🌟 নতুন আইকন ইম্পোর্ট করা হয়েছে
} from 'lucide-react';
import { useCart } from './CartContext';
import api, { STORAGE_URL } from './api/api';
// 🌟 SWR Fetcher
const fetcher = (url) => api.get(url).then((res) => res.data?.data || res.data);
// 🌟 Helper Function (বাইরে রাখা হলো যাতে বারবার রেন্ডার না হয়)
const parseImages = (imgData) => {
try {
if (!imgData) return [];
const parsed = typeof imgData === 'string' ? JSON.parse(imgData) : imgData;
return Array.isArray(parsed) ? parsed : [parsed];
} catch (e) {
return [imgData];
}
};
const getFullImageUrl = (path) => {
if (!path) return 'https://via.placeholder.com/600';
return path.startsWith('http') ? path : `${STORAGE_URL}${path}`;
};
const ProductDetails = () => {
const { id } = useParams();
const navigate = useNavigate();
const { addToCart } = useCart();
// 🌟 SWR দিয়ে প্রোডাক্টের ডেটা ফেচ করা হচ্ছে
const { data: rawData, isLoading } = useSWR(`/products/${id}`, fetcher);
// 🌟 SWR দিয়ে গ্লোবাল সেটিংস ফেচ করা হচ্ছে (Contact Button এর জন্য)
const { data: settings } = useSWR('/settings', fetcher);
// Local States
const [selectedColor, setSelectedColor] = useState(null);
const [selectedSize, setSelectedSize] = useState(null);
const [quantity, setQuantity] = useState(1);
const [activeImage, setActiveImage] = useState(0);
// অন্যান্য State এর সাথে এটি যুক্ত করুন
const [isDescOpen, setIsDescOpen] = useState(false);
const [error, setError] = useState("");
// 🌟 ডেটা ফরম্যাটিং (useMemo দিয়ে ক্যাশ করা হয়েছে পারফরম্যান্সের জন্য)
const product = useMemo(() => {
if (!rawData) return null;
const inventory = rawData.productSizes || rawData.product_sizes || [];
return {
...rawData,
availableColors: rawData.colors || [],
inventorySizes: inventory,
images: parseImages(rawData.images)
};
}, [rawData]);
// 🌟 ডাইনামিক টাইটেল এবং পেজ টপে স্ক্রল করা
useEffect(() => {
// Get the dynamic site name from settings, or use a default fallback
const dynamicAppName = settings?.site_name || 'Webit Shop';
if (product?.name) {
document.title = `${product.name} - ${dynamicAppName}`;
}
window.scrollTo(0, 0);
// Reset states when a new product is loaded
setSelectedColor(null);
setSelectedSize(null);
setQuantity(1);
setActiveImage(0);
setError("");
}, [product?.name, id, settings?.site_name]); // Added settings?.site_name to dependencies
const getPricingDetails = () => {
if (!product) return { displayPrice: 0, hasOffer: false };
const regPrice = Number(product.regular_price) || 0;
const globalSalePrice = Number(product.sale_price) || 0;
let activePrice = regPrice;
if (selectedSize && Number(selectedSize.price) > 0) {
activePrice = Number(selectedSize.price);
}
else if (!selectedSize && product.inventorySizes?.some(s => Number(s.price) > 0)) {
const defaultSizePrice = product.inventorySizes.find(s => Number(s.price) > 0);
activePrice = Number(defaultSizePrice.price);
}
else if (globalSalePrice > 0) {
activePrice = globalSalePrice;
}
const hasOffer = activePrice > 0 && activePrice < regPrice;
return {
displayPrice: activePrice > 0 ? activePrice : regPrice,
hasOffer
};
};
const handleAction = (isBuyNow = false) => {
const needsColor = product.availableColors?.length > 0;
const needsSize = product.inventorySizes?.length > 0;
if ((needsColor && !selectedColor) || (needsSize && !selectedSize)) {
setError("দয়া করে কালার এবং সাইজ সিলেক্ট করুন।");
return;
}
if (selectedSize && selectedSize.quantity !== null && selectedSize.quantity < quantity) {
setError(`দুঃখিত! এই সাইজটি মাত্র ${selectedSize.quantity} টি স্টকে আছে।`);
return;
}
setError("");
const { displayPrice } = getPricingDetails();
const cartData = {
id: product.id,
name: product.name,
price: displayPrice,
color: selectedColor,
size: selectedSize?.size?.name || selectedSize?.size_id,
quantity: quantity,
image: product.images[0]
};
addToCart(cartData);
if (isBuyNow) navigate('/checkout');
};
if (isLoading) return <div className="min-h-screen flex items-center justify-center">Loading...</div>;
if (!product) return <div className="text-center py-20 font-black text-slate-400">Product Not Found</div>;
const { displayPrice, hasOffer } = getPricingDetails();
return (
<div className="bg-white min-h-screen pb-20">
<div className="max-w-7xl mx-auto px-4 grid grid-cols-1 lg:grid-cols-2 gap-12 pt-10">
{/* Gallery */}
<div className="space-y-4">
<div className="aspect-[4/5] rounded-3xl overflow-hidden bg-slate-100 border border-slate-100">
{/* 🌟 প্রধান ইমেজে Lazy Loading */}
<img src={getFullImageUrl(product.images[activeImage])} loading="lazy" className="w-full h-full object-cover transition-opacity duration-500" alt={product.name} />
</div>
<div className="flex gap-4 overflow-x-auto pb-2">
{product.images.map((img, i) => (
<img key={i} src={getFullImageUrl(img)} loading="lazy" onClick={() => setActiveImage(i)} className={`w-20 h-20 rounded-xl cursor-pointer object-cover border-2 transition-all ${activeImage === i ? 'border-indigo-600' : 'border-transparent opacity-60 hover:opacity-100'}`} alt="" />
))}
</div>
</div>
{/* Info */}
<div className="space-y-6">
<h1 className="text-4xl font-black uppercase italic tracking-tighter">{product.name}</h1>
<div className="flex items-baseline gap-4">
<span className="text-4xl font-black text-indigo-600">
৳{displayPrice}
</span>
{hasOffer && (
<del className="text-lg text-slate-400 font-bold">
৳{product.regular_price}
</del>
)}
</div>
{error && <div className="bg-red-50 text-red-600 p-4 rounded-xl flex items-center gap-2 font-bold text-sm border border-red-100"><AlertCircle size={16}/> {error}</div>}
{/* Color Selection */}
{product.availableColors.length > 0 && (
<div className="space-y-3">
<p className="text-[10px] font-black uppercase text-slate-400 tracking-widest">Select Color: <span className="text-slate-900">{selectedColor || "Required"}</span></p>
<div className="flex gap-3">
{product.availableColors.map((color, i) => (
<button key={i} onClick={() => setSelectedColor(color.name)} className={`w-10 h-10 rounded-full border-2 transition-all ${selectedColor === color.name ? 'border-indigo-600 p-1 scale-110' : 'border-slate-100'}`}>
<div className="w-full h-full rounded-full shadow-inner" style={{ backgroundColor: color.name.toLowerCase() }} />
</button>
))}
</div>
</div>
)}
{/* Size Selection */}
{product.inventorySizes.length > 0 && (
<div className="space-y-3">
<p className="text-[10px] font-black uppercase text-slate-400 tracking-widest">Select Size: <span className="text-slate-900">{selectedSize?.size?.name || "Required"}</span></p>
<div className="flex flex-wrap gap-3">
{product.inventorySizes.map((item, i) => {
const sizeName = item.size?.name || `Size-${item.size_id}`;
const isSoldOut = item.quantity !== null && Number(item.quantity) <= 0;
return (
<button
key={i}
disabled={isSoldOut}
onClick={() => { setSelectedSize(item); setError(""); }}
className={`px-6 py-3 rounded-2xl font-black uppercase text-xs border-2 transition-all
${isSoldOut ? 'bg-slate-50 text-slate-300 cursor-not-allowed border-slate-50' :
selectedSize?.id === item.id ? 'bg-slate-900 text-white border-slate-900 shadow-xl' : 'bg-white text-slate-600 border-slate-100 hover:border-slate-300'}`}
>
{sizeName}
{item.quantity !== null && item.quantity <= 5 && item.quantity > 0 && (
<span className="text-[8px] text-orange-500 ml-1">({item.quantity} left)</span>
)}
{isSoldOut && <span className="text-[8px] block opacity-50">Sold Out</span>}
</button>
);
})}
</div>
</div>
)}
{/* Quantity */}
<div className="space-y-3">
<p className="text-[10px] font-black uppercase text-slate-400 tracking-widest">Quantity</p>
<div className="flex items-center gap-4 bg-slate-50 w-fit p-1 rounded-2xl border border-slate-100">
<button onClick={() => setQuantity(Math.max(1, quantity - 1))} className="w-10 h-10 flex items-center justify-center bg-white rounded-xl shadow-sm hover:bg-indigo-600 hover:text-white transition-all"><Minus size={16}/></button>
<span className="w-8 text-center font-black">{quantity}</span>
<button onClick={() => setQuantity(quantity + 1)} className="w-10 h-10 flex items-center justify-center bg-white rounded-xl shadow-sm hover:bg-indigo-600 hover:text-white transition-all"><Plus size={16}/></button>
</div>
</div>
{/* Action Buttons (Add to Cart / Buy Now) */}
<div className="flex gap-4 pt-6">
<button onClick={() => handleAction(false)} className="flex-1 bg-slate-50 text-slate-900 py-6 rounded-[2rem] font-black uppercase tracking-widest text-xs border border-slate-200 hover:bg-slate-100 transition-all flex items-center justify-center gap-2">
<ShoppingCart size={18} /> Add to Bag
</button>
<button onClick={() => handleAction(true)} className="flex-[1.5] bg-indigo-600 text-white py-6 rounded-[2rem] font-black uppercase tracking-widest text-xs hover:bg-slate-900 transition-all shadow-xl shadow-indigo-200 flex items-center justify-center gap-2">
Buy Now <Zap size={18} />
</button>
</div>
{/* 🌟 NEW: Contact Buttons (WhatsApp, Call, Messenger) */}
<div className="grid grid-cols-3 gap-3 pt-2">
<a
href={settings?.whatsapp ? `https://wa.me/${settings.whatsapp.replace(/[^0-9]/g, '')}` : '#'}
target="_blank"
rel="noreferrer"
className="flex flex-col items-center justify-center gap-1.5 py-4 bg-[#25D366]/10 text-[#25D366] hover:bg-[#25D366] hover:text-white rounded-2xl transition-all font-black text-[9px] uppercase tracking-widest border border-[#25D366]/20"
>
<MessageCircle size={20} />
WhatsApp
</a>
<a
href={settings?.phone ? `tel:${settings.phone}` : '#'}
className="flex flex-col items-center justify-center gap-1.5 py-4 bg-slate-100 text-slate-700 hover:bg-slate-800 hover:text-white rounded-2xl transition-all font-black text-[9px] uppercase tracking-widest border border-slate-200"
>
<Phone size={20} />
Call Us
</a>
<a
href={settings?.facebook_messager ? settings.facebook_messager : '#'}
target="_blank"
rel="noreferrer"
className="flex flex-col items-center justify-center gap-1.5 py-4 bg-[#0084FF]/10 text-[#0084FF] hover:bg-[#0084FF] hover:text-white rounded-2xl transition-all font-black text-[9px] uppercase tracking-widest border border-[#0084FF]/20"
>
<MessageSquare size={20} />
Messenger
</a>
</div>
</div>
</div>
{/* --- Product Details Section --- */}
<div className="max-w-4xl mx-auto px-4 mt-20 mb-20">
<div className="border border-slate-100 rounded-[2rem] overflow-hidden bg-white shadow-xl shadow-slate-100/50 transition-all duration-500">
{/* Header Button */}
<button
onClick={() => setIsDescOpen(!isDescOpen)}
className="w-full flex items-center justify-between p-6 md:p-8 bg-slate-50/50 hover:bg-slate-50 transition-colors group outline-none"
>
<div className="flex items-center gap-4">
{/* Decorative Line */}
<div className="w-1.5 h-8 rounded-full transition-colors duration-500" style={{ backgroundColor: isDescOpen ? 'var(--accent-color)' : '#cbd5e1' }}></div>
<h2 className="text-2xl md:text-3xl font-black uppercase tracking-tighter text-slate-900 italic">
Product Details
</h2>
</div>
{/* Plus/Minus Icon */}
<div className="w-12 h-12 rounded-2xl bg-white border border-slate-100 flex items-center justify-center shadow-sm transition-transform duration-500 group-hover:scale-105">
{isDescOpen ? (
<Minus size={20} style={{ color: 'var(--accent-color)' }} />
) : (
<Plus size={20} className="text-slate-400" />
)}
</div>
</button>
{/* Animated Content Body */}
<div
className={`grid transition-all duration-500 ease-in-out ${
isDescOpen ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'
}`}
>
<div className="overflow-hidden">
<div className="p-6 md:p-8 border-t border-slate-100 prose prose-slate max-w-none prose-headings:font-black prose-headings:uppercase prose-headings:tracking-tight prose-a:text-indigo-600">
<ReactMarkdown>{product.description}</ReactMarkdown>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default ProductDetails;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment