- Created a new TypeScript file for homepage-related types including Product, ProductVariant, ProductCategory, and HomepageData. - Updated the index.ts to export the new homepage types. - Refactored the web routes to replace the inline closure for the home route with the HomepageController.
222 lines
11 KiB
TypeScript
222 lines
11 KiB
TypeScript
import { X, Star, Minus, Plus, Phone } from 'lucide-react';
|
|
import { useState } from 'react';
|
|
import { formatRupiah } from '@/lib/rupiah';
|
|
import type { Product, ProductVariant, ProductPrice } from '@/types/homepage';
|
|
|
|
type ProductModalProps = {
|
|
product: Product | null;
|
|
onClose: () => void;
|
|
};
|
|
|
|
function getProductImage(): string {
|
|
return 'https://images.unsplash.com/photo-1490481651871-ab68de25d43d?w=1000&auto=format&fit=crop&q=80';
|
|
}
|
|
|
|
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 }: ProductModalProps) {
|
|
const [selectedVariant, setSelectedVariant] = useState<ProductVariant | null>(null);
|
|
const [selectedPriceType, setSelectedPriceType] = useState<'retail' | 'wholesale'>('retail');
|
|
const [quantity, setQuantity] = useState(1);
|
|
|
|
if (!product) return null;
|
|
|
|
const variants = product.product_variants;
|
|
const activeVariant = selectedVariant ?? variants[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 handleWhatsAppOrder = () => {
|
|
if (!activeVariant) return;
|
|
|
|
const priceLabel = selectedPriceType === 'retail' ? 'Retail' : 'Grosir';
|
|
const message = `Halo, saya tertarik dengan produk:\n\n*${product.name}*\n- Varian: ${activeVariant.name}\n- Harga ${priceLabel}: Rp ${formatRupiah(currentPrice)}\n- Jumlah: ${quantity} pcs\n- Subtotal: Rp ${formatRupiah(currentPrice * quantity)}\n\nMohon info detail pesanan. Terima kasih!`;
|
|
|
|
const phone = '6281234567890';
|
|
const waUrl = `https://wa.me/${phone}?text=${encodeURIComponent(message)}`;
|
|
window.open(waUrl, '_blank');
|
|
};
|
|
|
|
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
|
|
onClick={onClose}
|
|
className="p-2 rounded-full hover:bg-slate-100 transition-colors"
|
|
>
|
|
<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="relative aspect-3/4 rounded-2xl overflow-hidden">
|
|
<img
|
|
src={getProductImage()}
|
|
alt={product.name}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
</div>
|
|
|
|
{/* Details */}
|
|
<div className="space-y-4">
|
|
<div>
|
|
<div className="flex items-center gap-2 mb-2">
|
|
{product.categories.slice(0, 2).map((cat) => (
|
|
<span
|
|
key={cat.id}
|
|
className="text-[10px] font-semibold uppercase tracking-wider text-amber-600 bg-amber-50 px-2 py-0.5 rounded-full"
|
|
>
|
|
{cat.name}
|
|
</span>
|
|
))}
|
|
</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>
|
|
|
|
{/* Rating */}
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex text-amber-400">
|
|
<Star className="w-4 h-4 fill-current" />
|
|
<Star className="w-4 h-4 fill-current" />
|
|
<Star className="w-4 h-4 fill-current" />
|
|
<Star className="w-4 h-4 fill-current" />
|
|
<Star className="w-4 h-4 fill-current" />
|
|
</div>
|
|
<span className="text-xs text-slate-400">5.0 (120+)</span>
|
|
</div>
|
|
|
|
{/* 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>
|
|
{selectedPriceType === 'wholesale' && wholesalePrice > 0 && (
|
|
<p className="text-xs text-slate-400 mt-1">
|
|
Harga retail: Rp {formatRupiah(retailPrice)}
|
|
</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}
|
|
onClick={() => setSelectedVariant(variant)}
|
|
className={`px-4 py-2 text-xs font-semibold rounded-xl border transition-all ${
|
|
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
|
|
onClick={() => setSelectedPriceType('retail')}
|
|
className={`px-4 py-2 text-xs font-semibold rounded-xl border transition-all ${
|
|
selectedPriceType === 'retail'
|
|
? 'bg-amber-600 text-white border-transparent'
|
|
: 'bg-white text-slate-600 border-slate-200 hover:border-amber-300'
|
|
}`}
|
|
>
|
|
Retail
|
|
</button>
|
|
<button
|
|
onClick={() => setSelectedPriceType('wholesale')}
|
|
className={`px-4 py-2 text-xs font-semibold rounded-xl border transition-all ${
|
|
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
|
|
onClick={() => setQuantity(Math.max(1, quantity - 1))}
|
|
className="p-1 text-slate-500 hover:text-slate-800"
|
|
>
|
|
<Minus className="w-4 h-4" />
|
|
</button>
|
|
<span className="text-sm font-semibold w-8 text-center">{quantity}</span>
|
|
<button
|
|
onClick={() => setQuantity(Math.min(maxStock, quantity + 1))}
|
|
className="p-1 text-slate-500 hover:text-slate-800"
|
|
>
|
|
<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>
|
|
|
|
{/* WhatsApp Order */}
|
|
<button
|
|
onClick={handleWhatsAppOrder}
|
|
className="w-full flex items-center justify-center bg-emerald-600 text-white hover:bg-emerald-500 py-3.5 text-xs font-bold uppercase tracking-widest rounded-xl transition-all shadow-md"
|
|
>
|
|
<Phone className="w-4 h-4 mr-2" />
|
|
Pesan Via WhatsApp
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|