39490-vm/src/pages/ProductDetailPage.tsx
2026-04-03 19:45:50 +03:00

593 lines
25 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useMemo } from "react";
import { useParams, Link } from "react-router-dom";
import {
Heart,
ShoppingCart,
Star,
Plus,
Minus,
Check,
Truck,
Shield,
RotateCcw,
ChevronDown,
ChevronUp,
Package,
} from "lucide-react";
import { toast } from "sonner";
import { useCartStore } from "@/stores/cartStore";
import { PRODUCTS } from "@/constants/data";
import ProductCard from "@/components/features/ProductCard";
import type { ProductVariant } from "@/types";
export default function ProductDetailPage() {
const { id } = useParams<{ id: string }>();
const { addToCart, toggleWishlist, isInWishlist } = useCartStore();
const [quantity, setQuantity] = useState(1);
const [selectedImage, setSelectedImage] = useState(0);
const [selectedColor, setSelectedColor] = useState<string | null>(null);
const [selectedSize, setSelectedSize] = useState<string | null>(null);
const [showVariantsTable, setShowVariantsTable] = useState(false);
const product = PRODUCTS.find((p) => p.id === id);
if (!product) {
return (
<div className="max-w-7xl mx-auto px-4 py-20 text-center">
<div className="text-6xl mb-4">😕</div>
<h2 className="text-2xl font-black text-gray-800 mb-2">المنتج غير موجود</h2>
<Link to="/products" className="btn-primary inline-block mt-4">
العودة للمتجر
</Link>
</div>
);
}
const hasVariants = product.variants && product.variants.length > 0;
// Find matching variant based on selected color+size
const selectedVariant: ProductVariant | null = useMemo(() => {
if (!hasVariants) return null;
if (!selectedColor && !selectedSize) return null;
return (
product.variants!.find(
(v) =>
(!selectedColor || v.color === selectedColor) &&
(!selectedSize || v.size === selectedSize)
) || null
);
}, [selectedColor, selectedSize, product.variants, hasVariants]);
// Sizes available for selected color
const availableSizesForColor = useMemo(() => {
if (!hasVariants) return product.availableSizes || [];
if (!selectedColor) return product.availableSizes || [];
return [
...new Set(
product.variants!
.filter((v) => v.color === selectedColor)
.map((v) => v.size!)
.filter(Boolean)
),
];
}, [selectedColor, product.variants, hasVariants, product.availableSizes]);
// Check if a size is in stock for selected color
const isSizeInStock = (size: string) => {
if (!hasVariants) return product.inStock;
const v = product.variants!.find(
(variant) =>
variant.size === size &&
(!selectedColor || variant.color === selectedColor)
);
return v ? v.inStock : false;
};
const displayPrice = selectedVariant
? selectedVariant.price
: product.price;
const inWishlist = isInWishlist(product.id);
const images = product.images || [product.image];
const discount = product.originalPrice
? Math.round((1 - product.price / product.originalPrice) * 100)
: 0;
const related = PRODUCTS.filter(
(p) => p.categoryId === product.categoryId && p.id !== product.id
).slice(0, 4);
const handleAddToCart = () => {
if (!product.inStock) return;
if (hasVariants && !selectedVariant) {
toast.error("يرجى اختيار اللون والمقاس أولاً");
return;
}
if (selectedVariant && !selectedVariant.inStock) {
toast.error("هذا الخيار غير متوفر حالياً");
return;
}
addToCart(product, quantity, selectedVariant || undefined);
toast.success(
`تمت إضافة "${product.name}"${selectedVariant ? ` (${selectedVariant.color} - ${selectedVariant.size})` : ""} إلى السلة`
);
};
const handleToggleWishlist = () => {
toggleWishlist(product);
toast.success(inWishlist ? "تمت الإزالة من المفضلة" : "تمت الإضافة إلى المفضلة");
};
const handleColorSelect = (colorName: string) => {
setSelectedColor(colorName === selectedColor ? null : colorName);
setSelectedSize(null); // reset size when color changes
};
const handleSizeSelect = (size: string) => {
if (!isSizeInStock(size)) return;
setSelectedSize(size === selectedSize ? null : size);
};
return (
<div className="max-w-7xl mx-auto px-4 py-6">
{/* Breadcrumb */}
<div className="flex items-center gap-2 text-sm text-gray-500 mb-6 flex-wrap">
<Link to="/" className="hover:text-orange-500 transition-colors">الرئيسية</Link>
<span></span>
<Link to="/products" className="hover:text-orange-500 transition-colors">المنتجات</Link>
<span></span>
<Link
to={`/products?category=${product.categoryId}`}
className="hover:text-orange-500 transition-colors"
>
{product.category}
</Link>
<span></span>
<span className="text-gray-800 font-semibold line-clamp-1">{product.name}</span>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-10 mb-12">
{/* ─── Images ─── */}
<div>
<div className="bg-white rounded-2xl overflow-hidden shadow-sm border border-gray-100 mb-3">
<img
src={images[selectedImage]}
alt={product.name}
className="w-full h-80 md:h-[420px] object-cover"
/>
</div>
{images.length > 1 && (
<div className="flex gap-2 flex-wrap">
{images.map((img, i) => (
<button
key={i}
onClick={() => setSelectedImage(i)}
className={`w-20 h-20 rounded-xl overflow-hidden border-2 transition-colors ${
selectedImage === i ? "border-orange-500" : "border-gray-200 hover:border-orange-300"
}`}
>
<img src={img} alt="" className="w-full h-full object-cover" />
</button>
))}
</div>
)}
</div>
{/* ─── Info ─── */}
<div>
{/* Badges */}
<div className="flex flex-wrap gap-2 mb-3">
<Link
to={`/products?category=${product.categoryId}`}
className="text-xs font-bold text-orange-500 bg-orange-50 px-3 py-1 rounded-full hover:bg-orange-100 transition-colors"
>
{product.category}
</Link>
{product.isNew && (
<span className="text-xs font-bold text-green-600 bg-green-50 px-3 py-1 rounded-full">
جديد
</span>
)}
{discount > 0 && (
<span className="text-xs font-bold text-red-600 bg-red-50 px-3 py-1 rounded-full">
خصم {discount}%
</span>
)}
</div>
<h1 className="text-2xl md:text-3xl font-black text-gray-800 leading-tight mb-4">
{product.name}
</h1>
{/* Rating */}
<div className="flex items-center gap-3 mb-4">
<div className="flex">
{Array.from({ length: 5 }).map((_, i) => (
<Star
key={i}
size={18}
className={
i < Math.floor(product.rating) ? "text-amber-400" : "text-gray-200"
}
fill={i < Math.floor(product.rating) ? "currentColor" : "none"}
/>
))}
</div>
<span className="font-bold text-gray-700">{product.rating}</span>
<span className="text-gray-400 text-sm">({product.reviewCount} تقييم)</span>
</div>
{/* Price */}
<div className="bg-orange-50 rounded-2xl p-5 mb-5">
<div className="text-4xl font-black text-orange-500 mb-1">
{displayPrice.toLocaleString()} <span className="text-xl">ر.ي</span>
</div>
{product.originalPrice && (
<div className="flex items-center gap-3 flex-wrap">
<span className="text-gray-400 line-through text-lg">
{product.originalPrice.toLocaleString()} ر.ي
</span>
<span className="bg-red-500 text-white text-sm font-bold px-3 py-0.5 rounded-full">
وفرت {(product.originalPrice - product.price).toLocaleString()} ر.ي
</span>
</div>
)}
{selectedVariant && (
<p className="text-xs text-orange-600 font-semibold mt-2">
* السعر يختلف حسب اللون والمقاس المختار
</p>
)}
</div>
{/* Description */}
<p className="text-gray-600 leading-relaxed mb-6 text-sm">{product.description}</p>
{/* ─── Color Selector ─── */}
{hasVariants && product.availableColors && product.availableColors.length > 0 && (
<div className="mb-5">
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-bold text-gray-700">اختر اللون</span>
{selectedColor && (
<span className="text-xs text-orange-500 font-semibold">{selectedColor}</span>
)}
</div>
<div className="flex gap-3 flex-wrap">
{product.availableColors.map((col) => (
<button
key={col.name}
onClick={() => handleColorSelect(col.name)}
title={col.name}
className={`w-10 h-10 rounded-full border-4 transition-all duration-200 hover:scale-110 ${
selectedColor === col.name
? "border-orange-500 scale-110 shadow-lg"
: "border-white shadow-md"
}`}
style={{ backgroundColor: col.hex }}
/>
))}
</div>
</div>
)}
{/* ─── Size Selector ─── */}
{hasVariants && availableSizesForColor.length > 0 && (
<div className="mb-5">
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-bold text-gray-700">اختر المقاس</span>
{selectedSize && (
<span className="text-xs text-orange-500 font-semibold">{selectedSize}</span>
)}
</div>
<div className="flex gap-2 flex-wrap">
{availableSizesForColor.map((size) => {
const inStock = isSizeInStock(size);
const isSelected = selectedSize === size;
// Get price for this size+color combination
const variantForSize = product.variants!.find(
(v) => v.size === size && (!selectedColor || v.color === selectedColor)
);
return (
<button
key={size}
onClick={() => handleSizeSelect(size)}
disabled={!inStock}
className={`relative px-3 py-2 rounded-xl border-2 text-sm font-bold transition-all duration-200 min-w-[56px] text-center ${
isSelected
? "border-orange-500 bg-orange-500 text-white shadow-md"
: inStock
? "border-gray-200 text-gray-700 hover:border-orange-300 hover:bg-orange-50"
: "border-gray-100 text-gray-300 bg-gray-50 cursor-not-allowed line-through"
}`}
>
{size}
{variantForSize && variantForSize.price !== product.price && (
<span
className={`block text-xs font-normal leading-tight ${
isSelected ? "text-orange-100" : "text-orange-500"
}`}
>
{variantForSize.price.toLocaleString()}
</span>
)}
</button>
);
})}
</div>
{!selectedColor && hasVariants && product.availableColors && (
<p className="text-xs text-gray-400 mt-2">اختر اللون أولاً لرؤية المقاسات المتاحة</p>
)}
</div>
)}
{/* Variant selection hint */}
{hasVariants && selectedVariant && (
<div className="flex items-center gap-2 text-sm font-semibold text-green-600 mb-4 bg-green-50 px-4 py-2.5 rounded-xl">
<Check size={16} className="bg-green-500 text-white rounded-full p-0.5 shrink-0" />
{selectedVariant.color} · {selectedVariant.size} ·{" "}
<span className="text-orange-500">{selectedVariant.price.toLocaleString()} ر.ي</span>
</div>
)}
{hasVariants && !selectedVariant && (selectedColor || selectedSize) && (
<div className="text-xs text-amber-600 bg-amber-50 px-4 py-2.5 rounded-xl mb-4">
هذه التوليفة غير متوفرة، جرب مقاساً أو لوناً آخر
</div>
)}
{/* Stock (non-variant) */}
{!hasVariants && (
<div
className={`flex items-center gap-2 text-sm font-semibold mb-5 ${
product.inStock ? "text-green-600" : "text-red-500"
}`}
>
{product.inStock ? (
<>
<Check size={16} className="bg-green-500 text-white rounded-full p-0.5" />
متوفر في المخزن
</>
) : (
<>
<span className="w-4 h-4 bg-red-500 rounded-full inline-block" />
نفدت الكمية
</>
)}
</div>
)}
{/* Quantity */}
<div className="flex items-center gap-4 mb-5">
<span className="text-sm font-semibold text-gray-700">الكمية:</span>
<div className="flex items-center gap-2 bg-gray-100 rounded-xl p-1">
<button
onClick={() => setQuantity(Math.max(1, quantity - 1))}
className="w-9 h-9 bg-white rounded-lg flex items-center justify-center hover:bg-orange-50 hover:text-orange-500 transition-colors shadow-sm"
>
<Minus size={16} />
</button>
<span className="w-10 text-center font-black text-lg">{quantity}</span>
<button
onClick={() => setQuantity(quantity + 1)}
className="w-9 h-9 bg-white rounded-lg flex items-center justify-center hover:bg-orange-50 hover:text-orange-500 transition-colors shadow-sm"
>
<Plus size={16} />
</button>
</div>
</div>
{/* Actions */}
<div className="flex gap-3 mb-6">
<button
onClick={handleAddToCart}
disabled={!product.inStock || (hasVariants && !selectedVariant)}
className={`flex-1 flex items-center justify-center gap-2 py-3.5 rounded-xl font-bold text-sm transition-all duration-200 ${
product.inStock && (!hasVariants || selectedVariant)
? "bg-orange-500 hover:bg-orange-600 text-white shadow-md hover:shadow-orange-200 hover:shadow-lg active:scale-95"
: "bg-gray-100 text-gray-400 cursor-not-allowed"
}`}
>
<ShoppingCart size={18} />
{!product.inStock
? "نفدت الكمية"
: hasVariants && !selectedVariant
? "اختر اللون والمقاس"
: "أضف إلى السلة"}
</button>
<button
onClick={handleToggleWishlist}
className={`w-14 h-14 rounded-xl flex items-center justify-center border-2 transition-all duration-200 ${
inWishlist
? "border-red-500 bg-red-500 text-white"
: "border-gray-200 text-gray-400 hover:border-red-300 hover:text-red-400"
}`}
>
<Heart size={20} fill={inWishlist ? "currentColor" : "none"} />
</button>
</div>
{/* Features */}
<div className="grid grid-cols-3 gap-3">
{[
{ icon: Truck, label: "توصيل سريع", sub: "24-48 ساعة" },
{ icon: Shield, label: "ضمان الجودة", sub: "أصلي 100%" },
{ icon: RotateCcw, label: "إرجاع مجاني", sub: "خلال 7 أيام" },
].map(({ icon: Icon, label, sub }) => (
<div key={label} className="bg-gray-50 rounded-xl p-3 text-center">
<Icon size={18} className="text-orange-500 mx-auto mb-1" />
<p className="text-xs font-bold text-gray-700">{label}</p>
<p className="text-xs text-gray-400">{sub}</p>
</div>
))}
</div>
</div>
</div>
{/* ─── Variants Table ─── */}
{hasVariants && product.variants && (
<section className="mb-12">
<button
onClick={() => setShowVariantsTable(!showVariantsTable)}
className="w-full flex items-center justify-between bg-white border border-gray-200 rounded-2xl px-6 py-4 hover:bg-gray-50 transition-colors"
>
<div className="flex items-center gap-3">
<Package size={20} className="text-orange-500" />
<span className="font-bold text-gray-800 text-base">
خصائص الصنف ({product.variants.length} تنويعة)
</span>
</div>
{showVariantsTable ? (
<ChevronUp size={20} className="text-gray-500" />
) : (
<ChevronDown size={20} className="text-gray-500" />
)}
</button>
{showVariantsTable && (
<div className="mt-3 bg-white border border-gray-200 rounded-2xl overflow-hidden">
{/* Mobile: cards grid */}
<div className="grid grid-cols-2 sm:grid-cols-3 gap-px bg-gray-100 sm:hidden">
{product.variants.map((v) => (
<button
key={v.id}
onClick={() => {
if (!v.inStock) return;
setSelectedColor(v.color || null);
setSelectedSize(v.size || null);
window.scrollTo({ top: 0, behavior: "smooth" });
}}
disabled={!v.inStock}
className={`bg-white p-4 text-right transition-colors ${
selectedVariant?.id === v.id
? "bg-orange-50 border-2 border-orange-400"
: v.inStock
? "hover:bg-orange-50 cursor-pointer"
: "opacity-50 cursor-not-allowed"
}`}
>
<div className="flex items-center justify-between mb-2">
<div
className="w-5 h-5 rounded-full border border-gray-300 shrink-0"
style={{ backgroundColor: v.colorHex }}
/>
{!v.inStock && (
<span className="text-xs text-red-500 font-bold">نفد</span>
)}
{selectedVariant?.id === v.id && (
<Check size={14} className="text-orange-500" />
)}
</div>
<p className="text-xs text-gray-500 mb-0.5">
الوحدة: <span className="font-semibold text-gray-700">{product.unit || "قطعة"}</span>
</p>
{v.size && (
<p className="text-xs text-gray-500 mb-0.5">
المقاس: <span className="font-semibold text-gray-700">{v.size}</span>
</p>
)}
{v.color && (
<p className="text-xs text-gray-500 mb-1">
اللون: <span className="font-semibold text-gray-700">{v.color}</span>
</p>
)}
<p className="text-sm font-black text-orange-500">
{v.price.toLocaleString()}.00
</p>
</button>
))}
</div>
{/* Desktop: table */}
<div className="hidden sm:block overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-4 py-3 text-right font-bold text-gray-600">الوحدة</th>
{product.availableSizes && <th className="px-4 py-3 text-right font-bold text-gray-600">المقاس</th>}
{product.availableColors && <th className="px-4 py-3 text-right font-bold text-gray-600">اللون</th>}
<th className="px-4 py-3 text-right font-bold text-gray-600">السعر</th>
<th className="px-4 py-3 text-right font-bold text-gray-600">الحالة</th>
<th className="px-4 py-3"></th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{product.variants.map((v) => (
<tr
key={v.id}
className={`transition-colors ${
selectedVariant?.id === v.id
? "bg-orange-50"
: v.inStock
? "hover:bg-gray-50"
: "opacity-50"
}`}
>
<td className="px-4 py-3 font-semibold text-gray-700">
{product.unit || "قطعة"}
</td>
{product.availableSizes && (
<td className="px-4 py-3 text-gray-700">{v.size || "—"}</td>
)}
{product.availableColors && (
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<div
className="w-5 h-5 rounded-full border border-gray-300 shrink-0"
style={{ backgroundColor: v.colorHex }}
/>
<span className="text-gray-700">{v.color || "—"}</span>
</div>
</td>
)}
<td className="px-4 py-3 font-black text-gray-800">
{v.price.toLocaleString()}.00
</td>
<td className="px-4 py-3">
{v.inStock ? (
<span className="text-green-600 text-xs font-bold bg-green-50 px-2 py-1 rounded-full">متوفر</span>
) : (
<span className="text-red-500 text-xs font-bold bg-red-50 px-2 py-1 rounded-full">نفد</span>
)}
</td>
<td className="px-4 py-3">
<button
onClick={() => {
if (!v.inStock) return;
setSelectedColor(v.color || null);
setSelectedSize(v.size || null);
window.scrollTo({ top: 0, behavior: "smooth" });
}}
disabled={!v.inStock}
className={`text-xs font-bold px-3 py-1.5 rounded-lg transition-colors ${
selectedVariant?.id === v.id
? "bg-orange-500 text-white"
: v.inStock
? "bg-orange-50 text-orange-500 hover:bg-orange-100"
: "text-gray-300 cursor-not-allowed"
}`}
>
{selectedVariant?.id === v.id ? "✓ مختار" : "اختر"}
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</section>
)}
{/* Related products */}
{related.length > 0 && (
<section>
<h2 className="section-title mb-5">منتجات مشابهة</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{related.map((p) => (
<ProductCard key={p.id} product={p} />
))}
</div>
</section>
)}
</div>
);
}