dstpabuaran.com/resources/js/components/home/ProductCard.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

144 lines
5.8 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 } from '@/types/homepage';
import { ShoppingBag, ShoppingCart } from 'lucide-react';
import { toast } from 'sonner';
type ProductCardProps = {
product: Product;
onQuickView: (product: Product) => void;
contactPhone?: string | null;
};
const FALLBACK_IMAGE = 'https://images.unsplash.com/photo-1490481651871-ab68de25d43d?w=800&auto=format&fit=crop&q=80';
function getProductImage(product: Product): string {
for (const variant of product.product_variants) {
if (variant.photo_urls && variant.photo_urls.length > 0) {
return variant.photo_urls[0];
}
}
return FALLBACK_IMAGE;
}
function getProductPriceRange(product: Product): string {
const prices = product.product_variants.flatMap((v) =>
v.product_prices
.filter((p) => p.type === 'retail')
.map((p) => p.price),
);
if (prices.length === 0) {
const fallbackPrices = product.product_variants.flatMap((v) =>
v.product_prices.map((p) => p.price),
);
if (fallbackPrices.length === 0) {
return 'Rp 0';
}
const min = Math.min(...fallbackPrices);
const max = Math.max(...fallbackPrices);
return min === max
? `Rp ${formatRupiah(min)}`
: `Rp ${formatRupiah(min)} - Rp ${formatRupiah(max)}`;
}
const min = Math.min(...prices);
const max = Math.max(...prices);
return min === max
? `Rp ${formatRupiah(min)}`
: `Rp ${formatRupiah(min)} - Rp ${formatRupiah(max)}`;
}
export default function ProductCard({ product, onQuickView, contactPhone }: ProductCardProps) {
const { addItem } = useCart();
console.log(contactPhone)
const handleAddToCart = () => {
const variant = product.product_variants[0];
if (!variant) return;
const retail = variant.product_prices.find((p) => p.type === 'retail');
const wholesale = variant.product_prices.find((p) => p.type === 'wholesale');
const price = retail?.price ?? wholesale?.price ?? 0;
addItem({
productId: product.id,
productName: product.name,
variantId: variant.id,
variantName: variant.name,
priceType: 'retail',
price,
imageUrl: getProductImage(product),
});
toast.success(`${product.name} ditambahkan ke keranjang`);
};
const variant = product.product_variants[0];
const retail = variant?.product_prices.find((p) => p.type === 'retail');
const price = retail?.price ?? 0;
const waMessage = encodeURIComponent(
`Halo min, saya mau order, berikut pesanannya.\n\n*${product.name}*\n* Varian: ${variant?.name ?? '-'}\n* Harga: Rp ${formatRupiah(price)}\n* QTY : 1\n* Subtotal : Rp ${formatRupiah(price)}\n\nTotal: Rp ${formatRupiah(price)}\n\nMohon diinformasikan detail pembayaran dan proses selanjutnya ya, Kak.\n\nTerimakasih`
);
const waUrl = `https://wa.me/${formatWhatsAppPhone(contactPhone || '')}?text=${waMessage}`;
return (
<div className="cursor-pointer" onClick={() => onQuickView(product)}>
<div className="relative aspect-3/4 rounded-2xl overflow-hidden shadow-md border-2 border-white transition-all duration-300 hover:shadow-xl hover:-translate-y-1">
<img
src={getProductImage(product)}
alt={product.name}
className="w-full h-full object-cover transition-transform duration-700 hover:scale-110"
/>
</div>
<div className="mt-4 space-y-2">
<div className="flex items-center gap-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>
<h4 className="text-sm font-semibold text-slate-800 line-clamp-1">
{product.name}
</h4>
<p className="text-sm font-bold text-amber-600">
{getProductPriceRange(product)}
</p>
<div className="flex items-center gap-2 pt-1">
<Button
variant="outline"
size="icon"
onClick={(e) => {
e.stopPropagation();
if (product.product_variants.length === 1) {
handleAddToCart();
} else {
onQuickView(product);
}
}}
className="w-9 h-9 rounded-full bg-amber-50 text-amber-600 hover:bg-amber-100 border-amber-200"
aria-label="Tambah ke Keranjang"
>
<ShoppingBag className="w-4 h-4" />
</Button>
<Button asChild className="flex-1 bg-amber-500 hover:bg-amber-600 text-white text-[11px] font-semibold uppercase tracking-wider py-2 rounded-lg transition-colors">
<a href={contactPhone ? waUrl : '#'} target="_blank" rel="noreferrer">
<ShoppingCart className="w-4 h-4" />
Beli Sekarang
</a>
</Button>
</div>
</div>
</div>
);
}