dstpabuaran.com/resources/js/components/home/ProductModal.tsx
Yoga Pangestu 7d21a1e6db feat: add cart context and functionality, update welcome page with new components and styles
- Implemented CartContext for managing cart state, including adding, removing, and updating items.
- Added CartDrawer component to display cart items.
- Updated AppHeader to include cart item count.
- Enhanced welcome page with new layout, improved product image handling, and updated call-to-action buttons.
- Refactored homepage data types to accommodate new features and removed unused properties.
2026-08-13 17:19:56 +07:00

261 lines
13 KiB
TypeScript

import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { useCart } from '@/contexts/cart-context';
import { formatRupiah } from '@/lib/rupiah';
import { formatWhatsAppPhone } from '@/lib/format';
import type { Product, ProductVariant } from '@/types/homepage';
import { Minus, Plus, ShoppingBag, ShoppingCart, X } from 'lucide-react';
import { useState } from 'react';
import { toast } from 'sonner';
type ProductModalProps = {
product: Product | null;
onClose: () => void;
contactPhone?: string | null;
};
const FALLBACK_IMAGE = 'https://images.unsplash.com/photo-1490481651871-ab68de25d43d?w=1000&auto=format&fit=crop&q=80';
function getVariantImage(variant: ProductVariant): string {
if (variant.photo_urls && variant.photo_urls.length > 0) {
return variant.photo_urls[0];
}
return FALLBACK_IMAGE;
}
function getRetailPrice(variant: ProductVariant): number {
const retail = variant.product_prices.find((p) => p.type === 'retail');
return retail?.price ?? 0;
}
function getWholesalePrice(variant: ProductVariant): number {
const wholesale = variant.product_prices.find((p) => p.type === 'wholesale');
return wholesale?.price ?? 0;
}
export default function ProductModal({ product, onClose, contactPhone }: ProductModalProps) {
const [selectedVariant, setSelectedVariant] = useState<ProductVariant | null>(null);
const [selectedPriceType, setSelectedPriceType] = useState<'retail' | 'wholesale'>('retail');
const [selectedImage, setSelectedImage] = useState<string | null>(null);
const [quantity, setQuantity] = useState(1);
const { addItem } = useCart();
if (!product) return null;
const variants = product.product_variants;
const activeVariant = selectedVariant ?? variants[0];
const variantImages: string[] = activeVariant?.photo_urls?.length
? activeVariant.photo_urls
: [FALLBACK_IMAGE];
const currentImage = selectedImage ?? variantImages[0];
const retailPrice = activeVariant ? getRetailPrice(activeVariant) : 0;
const wholesalePrice = activeVariant ? getWholesalePrice(activeVariant) : 0;
const currentPrice = selectedPriceType === 'retail' ? retailPrice : wholesalePrice;
const maxStock = activeVariant?.stock ?? 0;
const handleVariantChange = (variant: ProductVariant) => {
setSelectedVariant(variant);
setSelectedImage(null);
setQuantity(1);
};
const handleAddToCart = () => {
if (!activeVariant) return;
addItem({
productId: product.id,
productName: product.name,
variantId: activeVariant.id,
variantName: activeVariant.name,
priceType: selectedPriceType,
price: currentPrice,
imageUrl: getVariantImage(activeVariant),
quantity,
});
toast.success(`${product.name} ditambahkan ke keranjang`);
};
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4"
onClick={onClose}
>
<div
className="bg-[#FDFDFC] rounded-3xl max-w-4xl w-full max-h-[90vh] overflow-y-auto shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="flex justify-between items-center p-6 border-b border-amber-100/30">
<h3 className="text-lg font-serif text-slate-800">Detail Produk</h3>
<Button variant="ghost" size="icon" onClick={onClose} className="rounded-full hover:bg-slate-100">
<X className="w-5 h-5 text-slate-500" />
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 p-6">
{/* Image */}
<div className="space-y-3">
<div className="relative aspect-3/4 rounded-2xl overflow-hidden">
<img
key={currentImage}
src={currentImage}
alt={product.name}
className="w-full h-full object-cover"
/>
</div>
{variantImages.length > 1 && (
<div className="flex gap-2 overflow-x-auto pb-1">
{variantImages.map((url, i) => (
<button
key={i}
onClick={() => setSelectedImage(url)}
className={`relative shrink-0 w-16 h-20 rounded-lg overflow-hidden border-2 transition-all ${currentImage === url
? 'border-amber-500 ring-2 ring-amber-200'
: 'border-slate-200 hover:border-amber-300 opacity-70 hover:opacity-100'
}`}
>
<img
src={url}
alt={`${product.name} ${i + 1}`}
className="w-full h-full object-cover"
/>
</button>
))}
</div>
)}
</div>
{/* Details */}
<div className="space-y-4">
<div>
<div className="flex items-center gap-2 mb-2">
{product.categories.slice(0, 2).map((cat) => (
<Badge key={cat.id} variant="secondary" className="bg-amber-50 text-amber-600 uppercase tracking-wider font-semibold px-2 py-0.5">
{cat.name}
</Badge>
))}
</div>
<h2 className="text-2xl font-serif text-slate-800">{product.name}</h2>
</div>
<p className="text-sm text-slate-500 leading-relaxed font-light">
{product.description || 'Produk fashion berkualitas dengan desain elegan dan bahan nyaman.'}
</p>
{/* Price */}
<div className="bg-amber-50/50 p-4 rounded-xl">
<p className="text-xs text-slate-500 mb-1">Harga</p>
<p className="text-2xl font-bold text-amber-600">
Rp {formatRupiah(currentPrice)}
</p>
</div>
{/* Variant Selection */}
{variants.length > 0 && (
<div>
<p className="text-xs font-semibold text-slate-700 mb-2">Varian</p>
<div className="flex flex-wrap gap-2">
{variants.map((variant) => (
<Button
key={variant.id}
variant={activeVariant?.id === variant.id ? 'default' : 'outline'}
size="sm"
onClick={() => handleVariantChange(variant)}
className={activeVariant?.id === variant.id
? 'bg-amber-600 text-white border-transparent'
: 'bg-white text-slate-600 border-slate-200 hover:border-amber-300'
}
>
{variant.name}
<span className="ml-1 text-[10px] opacity-70">
({variant.stock})
</span>
</Button>
))}
</div>
</div>
)}
{/* Price Type */}
<div>
<p className="text-xs font-semibold text-slate-700 mb-2">Jenis Harga</p>
<div className="flex gap-2">
<Button
variant={selectedPriceType === 'retail' ? 'default' : 'outline'}
size="sm"
onClick={() => setSelectedPriceType('retail')}
className={selectedPriceType === 'retail'
? 'bg-amber-600 text-white border-transparent'
: 'bg-white text-slate-600 border-slate-200 hover:border-amber-300'
}
>
Retail
</Button>
<Button
variant={selectedPriceType === 'wholesale' ? 'default' : 'outline'}
size="sm"
onClick={() => setSelectedPriceType('wholesale')}
className={selectedPriceType === 'wholesale'
? 'bg-amber-600 text-white border-transparent'
: 'bg-white text-slate-600 border-slate-200 hover:border-amber-300'
}
>
Grosir
</Button>
</div>
</div>
{/* Quantity */}
<div>
<p className="text-xs font-semibold text-slate-700 mb-2">Jumlah</p>
<div className="flex items-center gap-3 border border-slate-200 rounded-xl px-3 py-2 w-fit">
<Button
variant="ghost"
size="icon-xs"
onClick={() => setQuantity(Math.max(1, quantity - 1))}
>
<Minus className="w-4 h-4" />
</Button>
<span className="text-sm font-semibold w-8 text-center">{quantity}</span>
<Button
variant="ghost"
size="icon-xs"
onClick={() => setQuantity(Math.min(maxStock, quantity + 1))}
>
<Plus className="w-4 h-4" />
</Button>
</div>
<p className="text-[10px] text-slate-400 mt-1">Stok tersedia: {maxStock}</p>
</div>
{/* Total */}
<div className="bg-slate-50 p-4 rounded-xl">
<div className="flex justify-between text-sm">
<span className="text-slate-500">Subtotal</span>
<span className="font-bold text-amber-600">
Rp {formatRupiah(currentPrice * quantity)}
</span>
</div>
</div>
{/* Actions */}
<div className="flex gap-3">
<Button onClick={handleAddToCart} variant="outline" className="flex-1 border-amber-200 text-amber-700 hover:bg-amber-50 py-3.5 text-xs font-bold uppercase tracking-widest rounded-xl transition-all">
<ShoppingBag className="w-4 h-4 mr-2" />
Keranjang
</Button>
<Button asChild className="flex-1 bg-amber-600 text-white hover:bg-amber-500 py-3.5 text-xs font-bold uppercase tracking-widest rounded-xl transition-all shadow-md">
<a href={contactPhone ? `https://wa.me/${formatWhatsAppPhone(contactPhone)}?text=${encodeURIComponent(`Halo min, saya mau order, berikut pesanannya.\n\n*${product.name}*\n* Varian: ${activeVariant?.name ?? '-'}\n* Harga: Rp ${formatRupiah(currentPrice)}\n* QTY : ${quantity}\n* Subtotal : Rp ${formatRupiah(currentPrice * quantity)}\n\nTotal: Rp ${formatRupiah(currentPrice * quantity)}\n\nMohon diinformasikan detail pembayaran dan proses selanjutnya ya, Kak.\n\nTerimakasih`)}` : '#'} target="_blank" rel="noreferrer">
<ShoppingCart className="w-4 h-4 mr-2" />
Beli Sekarang
</a>
</Button>
</div>
</div>
</div>
</div>
</div>
);
}