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.
This commit is contained in:
parent
f7c9891787
commit
7d21a1e6db
@ -38,6 +38,7 @@ public function __invoke(Request $request): Response
|
||||
'categories:id,name,slug',
|
||||
'productVariants:id,product_id,name,stock',
|
||||
'productVariants.productPrices:id,variant_id,type,price',
|
||||
'productVariants.media',
|
||||
]);
|
||||
|
||||
if ($search !== '') {
|
||||
@ -53,9 +54,23 @@ public function __invoke(Request $request): Response
|
||||
});
|
||||
}
|
||||
|
||||
$products = Inertia::scroll(
|
||||
fn () => $productsQuery->orderBy('created_at', 'desc')->paginate(12)
|
||||
);
|
||||
$s3Service = $this->s3Service;
|
||||
$products = Inertia::scroll(function () use ($productsQuery, $s3Service) {
|
||||
$paginator = $productsQuery->orderBy('created_at', 'desc')->paginate(12);
|
||||
|
||||
$paginator->getCollection()->each(function ($product) use ($s3Service) {
|
||||
$product->productVariants->each(function ($variant) use ($s3Service) {
|
||||
$media = $variant->getMedia('images');
|
||||
$variant->photo_urls = $media->map(
|
||||
fn ($m) => str_starts_with($m->getPath(), 'http')
|
||||
? $m->getPath()
|
||||
: $s3Service->getTemporaryUrl($m->getPath(), 60)
|
||||
)->toArray();
|
||||
});
|
||||
});
|
||||
|
||||
return $paginator;
|
||||
});
|
||||
|
||||
$galleryImages = array_map(
|
||||
fn ($key) => str_starts_with($key, 'http') ? $key : $this->s3Service->getTemporaryUrl($key, 60),
|
||||
@ -72,44 +87,13 @@ public function __invoke(Request $request): Response
|
||||
'facebookUrl' => $socialMedia->facebook_url,
|
||||
'tiktokUrl' => $socialMedia->tiktok_url,
|
||||
'homepage' => [
|
||||
'hero_badge' => $homepage->hero_badge,
|
||||
'hero_title_line1' => $homepage->hero_title_line1,
|
||||
'hero_title_line2' => $homepage->hero_title_line2,
|
||||
'hero_title_highlight' => $homepage->hero_title_highlight,
|
||||
'hero_description' => $homepage->hero_description,
|
||||
'hero_cta_primary_text' => $homepage->hero_cta_primary_text,
|
||||
'hero_cta_secondary_text' => $homepage->hero_cta_secondary_text,
|
||||
'hero_image_url' => $homepage->hero_image_url
|
||||
? (str_starts_with($homepage->hero_image_url, 'http') ? $homepage->hero_image_url : $this->s3Service->getTemporaryUrl($homepage->hero_image_url, 60))
|
||||
: null,
|
||||
'hero_bg_text_left' => $homepage->hero_bg_text_left,
|
||||
'hero_bg_text_right' => $homepage->hero_bg_text_right,
|
||||
'scroll_hashtag' => $homepage->scroll_hashtag,
|
||||
'scroll_tagline' => $homepage->scroll_tagline,
|
||||
'catalog_badge' => $homepage->catalog_badge,
|
||||
'catalog_title' => $homepage->catalog_title,
|
||||
'catalog_description' => $homepage->catalog_description,
|
||||
'catalog_search_placeholder' => $homepage->catalog_search_placeholder,
|
||||
'gallery_badge' => $homepage->gallery_badge,
|
||||
'gallery_title' => $homepage->gallery_title,
|
||||
'gallery_description' => $homepage->gallery_description,
|
||||
'gallery_images' => $galleryImages,
|
||||
'order_guide_badge' => $homepage->order_guide_badge,
|
||||
'order_guide_title' => $homepage->order_guide_title,
|
||||
'order_guide_description' => $homepage->order_guide_description,
|
||||
'order_steps' => $homepage->order_steps,
|
||||
'about_badge' => $homepage->about_badge,
|
||||
'about_title' => $homepage->about_title,
|
||||
'about_image_url' => $homepage->about_image_url
|
||||
? (str_starts_with($homepage->about_image_url, 'http') ? $homepage->about_image_url : $this->s3Service->getTemporaryUrl($homepage->about_image_url, 60))
|
||||
: null,
|
||||
'about_features' => $homepage->about_features,
|
||||
'contact_badge' => $homepage->contact_badge,
|
||||
'contact_title' => $homepage->contact_title,
|
||||
'contact_description' => $homepage->contact_description,
|
||||
'contact_form_title' => $homepage->contact_form_title,
|
||||
'footer_description' => $homepage->footer_description,
|
||||
'footer_copyright' => $homepage->footer_copyright,
|
||||
'gallery_images' => $galleryImages,
|
||||
],
|
||||
'categories' => $categories,
|
||||
'products' => $products,
|
||||
@ -118,8 +102,8 @@ public function __invoke(Request $request): Response
|
||||
'category' => $category,
|
||||
],
|
||||
'seo' => [
|
||||
'title' => $system->app_name.' - '.$homepage->hero_badge,
|
||||
'description' => $homepage->hero_description,
|
||||
'title' => $system->app_name.' - Koleksi Segar 2026',
|
||||
'description' => 'Selamat datang di '.$system->app_name.'. Temukan keanggunan motif batik modern dan setelan pakaian santai berkualitas premium.',
|
||||
'image' => url('/assets/logo.png'),
|
||||
'url' => url('/'),
|
||||
],
|
||||
|
||||
@ -6,73 +6,11 @@
|
||||
|
||||
class HomepageSettings extends Settings
|
||||
{
|
||||
public string $hero_badge;
|
||||
|
||||
public string $hero_title_line1;
|
||||
|
||||
public string $hero_title_line2;
|
||||
|
||||
public string $hero_title_highlight;
|
||||
|
||||
public string $hero_description;
|
||||
|
||||
public string $hero_cta_primary_text;
|
||||
|
||||
public string $hero_cta_secondary_text;
|
||||
|
||||
public ?string $hero_image_url;
|
||||
|
||||
public string $hero_bg_text_left;
|
||||
|
||||
public string $hero_bg_text_right;
|
||||
|
||||
public string $scroll_hashtag;
|
||||
|
||||
public string $scroll_tagline;
|
||||
|
||||
public string $catalog_badge;
|
||||
|
||||
public string $catalog_title;
|
||||
|
||||
public string $catalog_description;
|
||||
|
||||
public string $catalog_search_placeholder;
|
||||
|
||||
public string $gallery_badge;
|
||||
|
||||
public string $gallery_title;
|
||||
|
||||
public string $gallery_description;
|
||||
|
||||
public array $gallery_images;
|
||||
|
||||
public string $order_guide_badge;
|
||||
|
||||
public string $order_guide_title;
|
||||
|
||||
public string $order_guide_description;
|
||||
|
||||
public array $order_steps;
|
||||
|
||||
public string $about_badge;
|
||||
|
||||
public string $about_title;
|
||||
|
||||
public ?string $about_image_url;
|
||||
|
||||
public array $about_features;
|
||||
|
||||
public string $contact_badge;
|
||||
|
||||
public string $contact_title;
|
||||
|
||||
public string $contact_description;
|
||||
|
||||
public string $contact_form_title;
|
||||
|
||||
public string $footer_description;
|
||||
|
||||
public string $footer_copyright;
|
||||
public array $gallery_images;
|
||||
|
||||
public static function group(): string
|
||||
{
|
||||
|
||||
@ -8,82 +8,16 @@
|
||||
public function up(): void
|
||||
{
|
||||
$this->migrator->inGroup('homepage', function (SettingsBlueprint $blueprint): void {
|
||||
// Hero Section
|
||||
$blueprint->add('hero_badge', 'Koleksi Segar 2026');
|
||||
$blueprint->add('hero_title_line1', 'Ekspresikan');
|
||||
$blueprint->add('hero_title_line2', 'Gaya');
|
||||
$blueprint->add('hero_title_highlight', 'Segar Anda');
|
||||
$blueprint->add('hero_description', 'Selamat datang di {app_name}. Temukan keanggunan motif batik modern dan setelan pakaian santai berkualitas premium. Dirancang khusus dengan bahan adem yang menyejukkan aktivitas harian Anda.');
|
||||
$blueprint->add('hero_cta_primary_text', 'Beli Sekarang');
|
||||
$blueprint->add('hero_cta_secondary_text', 'Tentang Kami');
|
||||
$blueprint->add('hero_image_url', 'https://images.unsplash.com/photo-1490481651871-ab68de25d43d?w=1000&auto=format&fit=crop&q=80');
|
||||
$blueprint->add('hero_bg_text_left', 'DST');
|
||||
$blueprint->add('hero_bg_text_right', 'Collection');
|
||||
|
||||
// Scroll Indicator
|
||||
$blueprint->add('scroll_hashtag', '#DSTCollection');
|
||||
$blueprint->add('scroll_tagline', 'Bahan Adem & Lembut');
|
||||
|
||||
// Catalog Section
|
||||
$blueprint->add('catalog_badge', 'Katalog Eksklusif');
|
||||
$blueprint->add('catalog_title', 'Koleksi Busana Pilihan');
|
||||
$blueprint->add('catalog_description', 'Gunakan kategori dan filter di bawah untuk menyesuaikan pencarian busana idaman Anda dengan mudah.');
|
||||
$blueprint->add('catalog_search_placeholder', 'Cari nama pakaian...');
|
||||
|
||||
// Deal Section
|
||||
$blueprint->add('deal_badge', 'Promo Terbatas');
|
||||
$blueprint->add('deal_title', 'Penawaran Bulan Ini');
|
||||
$blueprint->add('deal_description', 'Jangan lewatkan promosi batik dan daster eksklusif kami! Dapatkan potongan harga spesial up to 25% untuk varian daster pilihan dan setelan pakaian modern. Dapatkan kenyamanan ekstra dengan bahan menyejukkan sebelum masa promo habis!');
|
||||
$blueprint->add('deal_cta_text', 'Jelajahi Produk Diskon');
|
||||
$blueprint->add('deal_images', [
|
||||
$blueprint->add('about_image_url', 'https://images.unsplash.com/photo-1595777457583-95e059d581b8?w=800&auto=format&fit=crop&q=80');
|
||||
$blueprint->add('gallery_images', [
|
||||
'https://images.unsplash.com/photo-1595777457583-95e059d581b8?w=800&auto=format&fit=crop&q=80',
|
||||
'https://images.unsplash.com/photo-1572804013309-59a88b7e92f1?w=800&auto=format&fit=crop&q=80',
|
||||
'https://images.unsplash.com/photo-1515886657613-9f3515b0c78f?w=800&auto=format&fit=crop&q=80',
|
||||
'https://images.unsplash.com/photo-1539109136881-3be0616acf4b?w=800&auto=format&fit=crop&q=80',
|
||||
'https://images.unsplash.com/photo-1496747611176-843222e1e57c?w=800&auto=format&fit=crop&q=80',
|
||||
'https://images.unsplash.com/photo-1509631179647-0177331693ae?w=800&auto=format&fit=crop&q=80',
|
||||
]);
|
||||
|
||||
// Order Guide Section
|
||||
$blueprint->add('order_guide_badge', 'Langkah Pemesanan');
|
||||
$blueprint->add('order_guide_title', 'Cara Melakukan Pemesanan');
|
||||
$blueprint->add('order_guide_description', 'Sistem pemesanan kami sangat mudah dan terhubung langsung via WhatsApp untuk pelayanan cepat dan personal.');
|
||||
$blueprint->add('order_steps', [
|
||||
[
|
||||
'title' => 'Pilih Produk',
|
||||
'description' => 'Jelajahi pakaian batik dan daster favorit Anda, lalu ketuk "Lihat Detail" untuk memeriksa ukuran.',
|
||||
],
|
||||
[
|
||||
'title' => 'Pilih Varian & Harga',
|
||||
'description' => 'Tentukan varian ukuran yang diinginkan dan pilih jenis harga (Eceran, Grosir, Agen, dll.).',
|
||||
],
|
||||
[
|
||||
'title' => 'Masukkan Keranjang',
|
||||
'description' => 'Masukkan ke Keranjang Belanja untuk menampung seluruh daftar pakaian yang ingin Anda beli.',
|
||||
],
|
||||
[
|
||||
'title' => 'Kirim ke WhatsApp',
|
||||
'description' => 'Klik tombol kirim pesanan, admin kami akan merespons rincian transfer bank dan pengiriman kurir.',
|
||||
],
|
||||
]);
|
||||
|
||||
// About Section
|
||||
$blueprint->add('about_badge', 'Tentang Kami');
|
||||
$blueprint->add('about_title', 'DST Collection');
|
||||
$blueprint->add('about_image_url', 'https://images.unsplash.com/photo-1595777457583-95e059d581b8?w=800&auto=format&fit=crop&q=80');
|
||||
$blueprint->add('about_features', [
|
||||
'Kain Rayon Super Tebal & Menyerap Keringat',
|
||||
'Motif Eksklusif & Tidak Pasaran',
|
||||
'Dukungan Penuh Layanan Admin Via WhatsApp',
|
||||
]);
|
||||
|
||||
// Contact Section
|
||||
$blueprint->add('contact_badge', 'Kontak Kami');
|
||||
$blueprint->add('contact_title', 'Ada Pertanyaan? Hubungi Kami');
|
||||
$blueprint->add('contact_description', 'Kami sangat senang mendengarkan pertanyaan Anda terkait spesifikasi produk, ketersediaan grosir, atau kemitraan. Hubungi tim admin kami melalui media di bawah.');
|
||||
$blueprint->add('contact_form_title', 'Kirim Pesan Cepat');
|
||||
|
||||
// Footer
|
||||
$blueprint->add('footer_description', 'Galeri resmi {app_name}. Pilihan busana lokal premium berpotongan modern dengan kenyamanan menyejukkan.');
|
||||
$blueprint->add('footer_copyright', '© 2026 {app_name} DST Collection. Hak Cipta Dilindungi.');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -8,25 +8,11 @@
|
||||
public function up(): void
|
||||
{
|
||||
$this->migrator->inGroup('homepage', function (SettingsBlueprint $blueprint): void {
|
||||
// Delete old deal fields
|
||||
$blueprint->delete('deal_badge');
|
||||
$blueprint->delete('deal_title');
|
||||
$blueprint->delete('deal_description');
|
||||
$blueprint->delete('deal_cta_text');
|
||||
$blueprint->delete('deal_images');
|
||||
|
||||
// Add new gallery fields
|
||||
$blueprint->add('gallery_badge', 'Galeri Kami');
|
||||
$blueprint->add('gallery_title', 'Koleksi Lookbook');
|
||||
$blueprint->add('gallery_description', 'Intip koleksi lookbook kami untuk inspirasi gaya sehari-hari. Padu padan batik modern dan daster yang nyaman untuk berbagai suasana.');
|
||||
$blueprint->add('gallery_images', [
|
||||
'https://images.unsplash.com/photo-1595777457583-95e059d581b8?w=800&auto=format&fit=crop&q=80',
|
||||
'https://images.unsplash.com/photo-1572804013309-59a88b7e92f1?w=800&auto=format&fit=crop&q=80',
|
||||
'https://images.unsplash.com/photo-1515886657613-9f3515b0c78f?w=800&auto=format&fit=crop&q=80',
|
||||
'https://images.unsplash.com/photo-1539109136881-3be0616acf4b?w=800&auto=format&fit=crop&q=80',
|
||||
'https://images.unsplash.com/photo-1496747611176-843222e1e57c?w=800&auto=format&fit=crop&q=80',
|
||||
'https://images.unsplash.com/photo-1509631179647-0177331693ae?w=800&auto=format&fit=crop&q=80',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { FlashToast, PWAUpdateToast } from '@/components/notifications';
|
||||
import { Toaster } from '@/components/ui/sonner';
|
||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||
import { CartProvider } from '@/contexts/cart-context';
|
||||
import { initializeTheme } from '@/hooks/use-appearance';
|
||||
import AppLayout from '@/layouts/app-layout';
|
||||
import AuthLayout from '@/layouts/auth-layout';
|
||||
@ -38,12 +39,14 @@ createInertiaApp({
|
||||
strictMode: true,
|
||||
withApp(app) {
|
||||
return (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<FlashToast />
|
||||
<PWAUpdateToast />
|
||||
{app}
|
||||
<Toaster />
|
||||
</TooltipProvider>
|
||||
<CartProvider>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<FlashToast />
|
||||
<PWAUpdateToast />
|
||||
{app}
|
||||
<Toaster />
|
||||
</TooltipProvider>
|
||||
</CartProvider>
|
||||
);
|
||||
},
|
||||
progress: {
|
||||
|
||||
113
resources/js/components/home/CartDrawer.tsx
Normal file
113
resources/js/components/home/CartDrawer.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useCart } from '@/contexts/cart-context';
|
||||
import { formatRupiah } from '@/lib/rupiah';
|
||||
import { formatWhatsAppPhone } from '@/lib/format';
|
||||
import { Minus, Plus, ShoppingBag, ShoppingCart, Trash2, X } from 'lucide-react';
|
||||
|
||||
type CartDrawerProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
contactPhone?: string | null;
|
||||
};
|
||||
|
||||
export default function CartDrawer({ open, onClose, contactPhone }: CartDrawerProps) {
|
||||
const { items, removeItem, updateQuantity, clearCart, totalItems, totalPrice } = useCart();
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const handleCheckout = () => {
|
||||
if (items.length === 0 || !contactPhone) return;
|
||||
|
||||
const lines = items.map((item) => {
|
||||
const subtotal = item.price * item.quantity;
|
||||
return `*${item.productName}*\n* Varian: ${item.variantName}\n* Harga: Rp ${formatRupiah(item.price)}\n* QTY : ${item.quantity}\n* Subtotal : Rp ${formatRupiah(subtotal)}`;
|
||||
});
|
||||
const message = `Halo min, saya mau order, berikut pesanannya.\n\n${lines.join('\n\n')}\n\nTotal: Rp ${formatRupiah(totalPrice)}\n\nMohon diinformasikan detail pembayaran dan proses selanjutnya ya, Kak.\n\nTerimakasih`;
|
||||
const waUrl = `https://wa.me/${formatWhatsAppPhone(contactPhone || '')}?text=${encodeURIComponent(message)}`;
|
||||
window.open(waUrl, '_blank');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex justify-end" onClick={onClose}>
|
||||
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" />
|
||||
<div
|
||||
className="relative w-full max-w-md bg-[#FDFDFC] shadow-2xl flex flex-col animate-in slide-in-from-right duration-300"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-5 border-b border-amber-100/30">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShoppingBag className="w-5 h-5 text-amber-600" />
|
||||
<h3 className="text-lg font-serif text-slate-800">Keranjang</h3>
|
||||
<span className="text-xs text-slate-400">({totalItems} item)</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{items.length > 0 && (
|
||||
<Button variant="ghost" size="sm" onClick={clearCart} className="text-xs text-red-500 hover:text-red-600 hover:bg-red-50">
|
||||
<Trash2 className="w-3.5 h-3.5 mr-1" />
|
||||
Hapus Semua
|
||||
</Button>
|
||||
)}
|
||||
<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>
|
||||
|
||||
{/* Items */}
|
||||
<div className="flex-1 overflow-y-auto p-5 space-y-4">
|
||||
{items.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center space-y-3">
|
||||
<ShoppingBag className="w-12 h-12 text-slate-200" />
|
||||
<p className="text-sm text-slate-400">Keranjang kosong</p>
|
||||
<p className="text-xs text-slate-300">Tambahkan produk untuk mulai berbelanja</p>
|
||||
</div>
|
||||
) : (
|
||||
items.map((item) => (
|
||||
<div key={item.variantId} className="flex gap-3 p-3 bg-white rounded-xl border border-slate-100 shadow-sm">
|
||||
<div className="w-16 h-20 rounded-lg overflow-hidden shrink-0">
|
||||
<img src={item.imageUrl} alt={item.productName} className="w-full h-full object-cover" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0 space-y-1">
|
||||
<p className="text-sm font-semibold text-slate-800 truncate">{item.productName}</p>
|
||||
<p className="text-[10px] text-slate-400">{item.variantName} · {item.priceType === 'retail' ? 'Retail' : 'Grosir'}</p>
|
||||
<p className="text-sm font-bold text-amber-600">Rp {formatRupiah(item.price)}</p>
|
||||
<div className="flex items-center justify-between pt-1">
|
||||
<div className="flex items-center gap-2 border border-slate-200 rounded-lg px-2 py-0.5">
|
||||
<Button variant="ghost" size="icon-xs" onClick={() => updateQuantity(item.variantId, item.quantity - 1)}>
|
||||
<Minus className="w-3 h-3" />
|
||||
</Button>
|
||||
<span className="text-xs font-semibold w-5 text-center">{item.quantity}</span>
|
||||
<Button variant="ghost" size="icon-xs" onClick={() => updateQuantity(item.variantId, item.quantity + 1)}>
|
||||
<Plus className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon-xs" onClick={() => removeItem(item.variantId)} className="text-red-400 hover:text-red-500 hover:bg-red-50">
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
{items.length > 0 && (
|
||||
<div className="border-t border-amber-100/30 p-5 space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-slate-500">Total</span>
|
||||
<span className="text-xl font-bold text-amber-600">Rp {formatRupiah(totalPrice)}</span>
|
||||
</div>
|
||||
<Button asChild className="w-full 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="#" onClick={(e) => { e.preventDefault(); handleCheckout(); }}>
|
||||
<ShoppingCart className="w-4 h-4 mr-2" />
|
||||
Beli Sekarang
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,42 +1,27 @@
|
||||
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) {
|
||||
const prices = variant.product_prices;
|
||||
if (prices.length > 0) {
|
||||
return `https://images.unsplash.com/photo-1490481651871-ab68de25d43d?w=800&auto=format&fit=crop&q=80`;
|
||||
if (variant.photo_urls && variant.photo_urls.length > 0) {
|
||||
return variant.photo_urls[0];
|
||||
}
|
||||
}
|
||||
|
||||
const fallbacks: Record<string, string[]> = {
|
||||
'daster': [
|
||||
'https://images.unsplash.com/photo-1595777457583-95e059d581b8?w=800&auto=format&fit=crop&q=80',
|
||||
'https://images.unsplash.com/photo-1572804013309-59a88b7e92f1?w=800&auto=format&fit=crop&q=80',
|
||||
],
|
||||
'setelan-celana': [
|
||||
'https://images.unsplash.com/photo-1539109136881-3be0616acf4b?w=800&auto=format&fit=crop&q=80',
|
||||
],
|
||||
'atasan': [
|
||||
'https://images.unsplash.com/photo-1515886657613-9f3515b0c78f?w=800&auto=format&fit=crop&q=80',
|
||||
],
|
||||
'bawahan': [
|
||||
'https://images.unsplash.com/photo-1583496661160-fb4886b36ca7?w=800&auto=format&fit=crop&q=80',
|
||||
],
|
||||
};
|
||||
|
||||
const firstCat = product.categories?.[0]?.slug || '';
|
||||
const categoryUrls = fallbacks[firstCat] || [
|
||||
'https://images.unsplash.com/photo-1490481651871-ab68de25d43d?w=800&auto=format&fit=crop&q=80',
|
||||
'https://images.unsplash.com/photo-1434389677669-e08b4cac3105?w=800&auto=format&fit=crop&q=80',
|
||||
];
|
||||
|
||||
return categoryUrls[0];
|
||||
return FALLBACK_IMAGE;
|
||||
}
|
||||
|
||||
function getProductPriceRange(product: Product): string {
|
||||
@ -71,46 +56,87 @@ function getProductPriceRange(product: Product): string {
|
||||
: `Rp ${formatRupiah(min)} - Rp ${formatRupiah(max)}`;
|
||||
}
|
||||
|
||||
export default function ProductCard({ product, onQuickView }: ProductCardProps) {
|
||||
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="group cursor-pointer"
|
||||
onClick={() => onQuickView(product)}
|
||||
>
|
||||
<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 group-hover:scale-110"
|
||||
className="w-full h-full object-cover transition-transform duration-700 hover:scale-110"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/40 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
<div className="absolute bottom-0 left-0 right-0 p-4 translate-y-full group-hover:translate-y-0 transition-transform duration-300">
|
||||
<span className="text-white text-xs font-semibold uppercase tracking-widest">
|
||||
Lihat Detail
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 space-y-1">
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="flex items-center gap-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"
|
||||
>
|
||||
<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}
|
||||
</span>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<h4 className="text-sm font-semibold text-slate-800 line-clamp-1">
|
||||
{product.name}
|
||||
</h4>
|
||||
<p className="text-xs text-slate-500 line-clamp-2 font-light">
|
||||
{product.description || 'Koleksi fashion berkualitas'}
|
||||
</p>
|
||||
<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>
|
||||
);
|
||||
|
||||
@ -1,15 +1,26 @@
|
||||
import { X, Star, Minus, Plus, Phone } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useCart } from '@/contexts/cart-context';
|
||||
import { formatRupiah } from '@/lib/rupiah';
|
||||
import type { Product, ProductVariant, ProductPrice } from '@/types/homepage';
|
||||
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;
|
||||
};
|
||||
|
||||
function getProductImage(): string {
|
||||
return 'https://images.unsplash.com/photo-1490481651871-ab68de25d43d?w=1000&auto=format&fit=crop&q=80';
|
||||
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 {
|
||||
@ -22,29 +33,46 @@ function getWholesalePrice(variant: ProductVariant): number {
|
||||
return wholesale?.price ?? 0;
|
||||
}
|
||||
|
||||
export default function ProductModal({ product, onClose }: ProductModalProps) {
|
||||
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 handleWhatsAppOrder = () => {
|
||||
const handleVariantChange = (variant: ProductVariant) => {
|
||||
setSelectedVariant(variant);
|
||||
setSelectedImage(null);
|
||||
setQuantity(1);
|
||||
};
|
||||
|
||||
const handleAddToCart = () => {
|
||||
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');
|
||||
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 (
|
||||
@ -59,22 +87,43 @@ export default function ProductModal({ product, onClose }: ProductModalProps) {
|
||||
{/* 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"
|
||||
>
|
||||
<Button variant="ghost" size="icon" onClick={onClose} className="rounded-full hover:bg-slate-100">
|
||||
<X className="w-5 h-5 text-slate-500" />
|
||||
</button>
|
||||
</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 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 */}
|
||||
@ -82,12 +131,9 @@ export default function ProductModal({ product, onClose }: ProductModalProps) {
|
||||
<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"
|
||||
>
|
||||
<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}
|
||||
</span>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<h2 className="text-2xl font-serif text-slate-800">{product.name}</h2>
|
||||
@ -97,29 +143,12 @@ export default function ProductModal({ product, onClose }: ProductModalProps) {
|
||||
{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 */}
|
||||
@ -128,20 +157,21 @@ export default function ProductModal({ product, onClose }: ProductModalProps) {
|
||||
<p className="text-xs font-semibold text-slate-700 mb-2">Varian</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{variants.map((variant) => (
|
||||
<button
|
||||
<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={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>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@ -151,26 +181,28 @@ export default function ProductModal({ product, onClose }: ProductModalProps) {
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-slate-700 mb-2">Jenis Harga</p>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
<Button
|
||||
variant={selectedPriceType === 'retail' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
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'
|
||||
}`}
|
||||
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
|
||||
</Button>
|
||||
<Button
|
||||
variant={selectedPriceType === 'wholesale' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
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'
|
||||
}`}
|
||||
className={selectedPriceType === 'wholesale'
|
||||
? 'bg-amber-600 text-white border-transparent'
|
||||
: 'bg-white text-slate-600 border-slate-200 hover:border-amber-300'
|
||||
}
|
||||
>
|
||||
Grosir
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -178,19 +210,21 @@ export default function ProductModal({ product, onClose }: ProductModalProps) {
|
||||
<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
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={() => setQuantity(Math.max(1, quantity - 1))}
|
||||
className="p-1 text-slate-500 hover:text-slate-800"
|
||||
>
|
||||
<Minus className="w-4 h-4" />
|
||||
</button>
|
||||
</Button>
|
||||
<span className="text-sm font-semibold w-8 text-center">{quantity}</span>
|
||||
<button
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={() => setQuantity(Math.min(maxStock, quantity + 1))}
|
||||
className="p-1 text-slate-500 hover:text-slate-800"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-400 mt-1">Stok tersedia: {maxStock}</p>
|
||||
</div>
|
||||
@ -205,14 +239,19 @@ export default function ProductModal({ product, onClose }: ProductModalProps) {
|
||||
</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>
|
||||
{/* 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>
|
||||
|
||||
@ -141,7 +141,7 @@ export function AppHeader({ breadcrumbs = [] }: Props) {
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex items-center space-x-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenu>k
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
102
resources/js/contexts/cart-context.tsx
Normal file
102
resources/js/contexts/cart-context.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
export type CartItem = {
|
||||
productId: number;
|
||||
productName: string;
|
||||
variantId: number;
|
||||
variantName: string;
|
||||
priceType: 'retail' | 'wholesale';
|
||||
price: number;
|
||||
quantity: number;
|
||||
imageUrl: string;
|
||||
};
|
||||
|
||||
type CartContextType = {
|
||||
items: CartItem[];
|
||||
addItem: (item: Omit<CartItem, 'quantity'> & { quantity?: number }) => void;
|
||||
removeItem: (variantId: number) => void;
|
||||
updateQuantity: (variantId: number, quantity: number) => void;
|
||||
clearCart: () => void;
|
||||
totalItems: number;
|
||||
totalPrice: number;
|
||||
};
|
||||
|
||||
const CartContext = createContext<CartContextType | null>(null);
|
||||
|
||||
const STORAGE_KEY = 'dst_cart';
|
||||
const VISITOR_KEY = 'dst_visitor_id';
|
||||
|
||||
function getVisitorId(): string {
|
||||
let id = localStorage.getItem(VISITOR_KEY);
|
||||
if (!id) {
|
||||
id = crypto.randomUUID();
|
||||
localStorage.setItem(VISITOR_KEY, id);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
function loadCart(): CartItem[] {
|
||||
try {
|
||||
const raw = localStorage.getItem(`${STORAGE_KEY}_${getVisitorId()}`);
|
||||
return raw ? JSON.parse(raw) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function saveCart(items: CartItem[]): void {
|
||||
localStorage.setItem(`${STORAGE_KEY}_${getVisitorId()}`, JSON.stringify(items));
|
||||
}
|
||||
|
||||
export function CartProvider({ children }: { children: React.ReactNode }) {
|
||||
const [items, setItems] = useState<CartItem[]>(loadCart);
|
||||
|
||||
useEffect(() => {
|
||||
saveCart(items);
|
||||
}, [items]);
|
||||
|
||||
const addItem = useCallback((item: Omit<CartItem, 'quantity'> & { quantity?: number }) => {
|
||||
setItems((prev) => {
|
||||
const existing = prev.find((i) => i.variantId === item.variantId);
|
||||
if (existing) {
|
||||
return prev.map((i) =>
|
||||
i.variantId === item.variantId
|
||||
? { ...i, quantity: i.quantity + (item.quantity ?? 1) }
|
||||
: i,
|
||||
);
|
||||
}
|
||||
return [...prev, { ...item, quantity: item.quantity ?? 1 }];
|
||||
});
|
||||
}, []);
|
||||
|
||||
const removeItem = useCallback((variantId: number) => {
|
||||
setItems((prev) => prev.filter((i) => i.variantId !== variantId));
|
||||
}, []);
|
||||
|
||||
const updateQuantity = useCallback((variantId: number, quantity: number) => {
|
||||
if (quantity <= 0) {
|
||||
setItems((prev) => prev.filter((i) => i.variantId !== variantId));
|
||||
return;
|
||||
}
|
||||
setItems((prev) => prev.map((i) => (i.variantId === variantId ? { ...i, quantity } : i)));
|
||||
}, []);
|
||||
|
||||
const clearCart = useCallback(() => {
|
||||
setItems([]);
|
||||
}, []);
|
||||
|
||||
const totalItems = useMemo(() => items.reduce((sum, i) => sum + i.quantity, 0), [items]);
|
||||
const totalPrice = useMemo(() => items.reduce((sum, i) => sum + i.price * i.quantity, 0), [items]);
|
||||
|
||||
return (
|
||||
<CartContext.Provider value={{ items, addItem, removeItem, updateQuantity, clearCart, totalItems, totalPrice }}>
|
||||
{children}
|
||||
</CartContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useCart(): CartContextType {
|
||||
const ctx = useContext(CartContext);
|
||||
if (!ctx) throw new Error('useCart must be used within CartProvider');
|
||||
return ctx;
|
||||
}
|
||||
@ -5,6 +5,20 @@ export function formatNumber(
|
||||
return new Intl.NumberFormat('id-ID', options).format(num);
|
||||
}
|
||||
|
||||
export function formatWhatsAppPhone(phone: string): string {
|
||||
const digits = phone.replace(/\D/g, '');
|
||||
|
||||
if (digits.startsWith('62')) {
|
||||
return digits;
|
||||
}
|
||||
|
||||
if (digits.startsWith('0')) {
|
||||
return '62' + digits.slice(1);
|
||||
}
|
||||
|
||||
return '62' + digits;
|
||||
}
|
||||
|
||||
export type DateFormatStyle = 'full' | 'short' | 'datetime';
|
||||
|
||||
export function formatDate(dateString: string, style: DateFormatStyle = 'full'): string {
|
||||
|
||||
@ -1,23 +1,28 @@
|
||||
import { Head, Link, router, InfiniteScroll } from '@inertiajs/react';
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import ProductCard from '@/components/home/ProductCard';
|
||||
import CartDrawer from '@/components/home/CartDrawer';
|
||||
import ProductModal from '@/components/home/ProductModal';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useCart } from '@/contexts/cart-context';
|
||||
import { formatRupiah } from '@/lib/rupiah';
|
||||
import type { Category, HomepageData, Paginated, Product, ProductPrice } from '@/types/homepage';
|
||||
import { Head, InfiniteScroll, Link, router } from '@inertiajs/react';
|
||||
import {
|
||||
ArrowRight,
|
||||
Check,
|
||||
ChevronRight,
|
||||
Info,
|
||||
LogIn,
|
||||
Mail,
|
||||
MapPin,
|
||||
Phone,
|
||||
Plus,
|
||||
Search,
|
||||
ShoppingBag,
|
||||
Plus,
|
||||
ArrowRight,
|
||||
Star,
|
||||
Info,
|
||||
ChevronRight,
|
||||
Phone,
|
||||
MapPin,
|
||||
Mail,
|
||||
LogIn,
|
||||
Check,
|
||||
} from 'lucide-react';
|
||||
import { formatRupiah } from '@/lib/rupiah';
|
||||
import ProductCard from '@/components/home/ProductCard';
|
||||
import ProductModal from '@/components/home/ProductModal';
|
||||
import type { Product, Category, HomepageData, ProductPrice, Paginated } from '@/types/homepage';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
type Props = {
|
||||
appName: string;
|
||||
@ -52,30 +57,43 @@ export default function Welcome({
|
||||
const [searchInput, setSearchInput] = useState(filters.search);
|
||||
const [selectedCategory, setSelectedCategory] = useState(filters.category);
|
||||
const [activeProduct, setActiveProduct] = useState<Product | null>(null);
|
||||
const [activeSection, setActiveSection] = useState('hero');
|
||||
const [cartOpen, setCartOpen] = useState(false);
|
||||
const searchTimeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||
const { totalItems } = useCart();
|
||||
|
||||
useEffect(() => {
|
||||
const sections = ['hero', 'catalog', 'gallery', 'order-guide', 'about', 'contact'];
|
||||
const observers: IntersectionObserver[] = [];
|
||||
|
||||
sections.forEach((id) => {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setActiveSection(id);
|
||||
}
|
||||
},
|
||||
{ rootMargin: '-40% 0px -55% 0px' },
|
||||
);
|
||||
observer.observe(el);
|
||||
observers.push(observer);
|
||||
});
|
||||
|
||||
return () => observers.forEach((o) => o.disconnect());
|
||||
}, []);
|
||||
|
||||
const FALLBACK_IMAGE = 'https://images.unsplash.com/photo-1490481651871-ab68de25d43d?w=800&auto=format&fit=crop&q=80';
|
||||
|
||||
const getProductImage = (product: Product): string => {
|
||||
const fallbacks: Record<string, string[]> = {
|
||||
daster: [
|
||||
'https://images.unsplash.com/photo-1595777457583-95e059d581b8?w=800&auto=format&fit=crop&q=80',
|
||||
],
|
||||
'setelan-celana': [
|
||||
'https://images.unsplash.com/photo-1539109136881-3be0616acf4b?w=800&auto=format&fit=crop&q=80',
|
||||
],
|
||||
atasan: [
|
||||
'https://images.unsplash.com/photo-1515886657613-9f3515b0c78f?w=800&auto=format&fit=crop&q=80',
|
||||
],
|
||||
bawahan: [
|
||||
'https://images.unsplash.com/photo-1583496661160-fb4886b36ca7?w=800&auto=format&fit=crop&q=80',
|
||||
],
|
||||
};
|
||||
|
||||
const firstCat = product.categories?.[0]?.slug || '';
|
||||
const urls = fallbacks[firstCat] || [
|
||||
'https://images.unsplash.com/photo-1490481651871-ab68de25d43d?w=800&auto=format&fit=crop&q=80',
|
||||
];
|
||||
|
||||
return urls[0];
|
||||
for (const variant of product.product_variants) {
|
||||
if (variant.photo_urls && variant.photo_urls.length > 0) {
|
||||
return variant.photo_urls[0];
|
||||
}
|
||||
}
|
||||
return FALLBACK_IMAGE;
|
||||
};
|
||||
|
||||
const getProductPriceRange = (product: Product): string => {
|
||||
@ -145,7 +163,6 @@ export default function Welcome({
|
||||
fetchProducts('', '');
|
||||
}, [fetchProducts]);
|
||||
|
||||
// Use first 3 products from current page as featured for hero
|
||||
const featuredProducts = products.data.slice(0, 3);
|
||||
|
||||
return (
|
||||
@ -159,30 +176,42 @@ export default function Welcome({
|
||||
{/* Sticky Header */}
|
||||
<header className="sticky top-0 z-40 bg-white/70 backdrop-blur-lg border-b border-amber-100/50 transition-all">
|
||||
<div className="max-w-7xl mx-auto px-6 h-20 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Link href="/" className="flex items-center space-x-2">
|
||||
<span className="font-serif text-2xl font-bold tracking-widest text-amber-600">
|
||||
{appName}
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
<Link href="/" className="flex items-center">
|
||||
<img src="/assets/logo.png" alt={appName} className="h-12 w-auto" />
|
||||
</Link>
|
||||
|
||||
<nav className="hidden lg:flex items-center space-x-8 text-xs font-semibold uppercase tracking-widest text-slate-600">
|
||||
<a href="#" className="hover:text-amber-600 transition-colors">Beranda</a>
|
||||
<a href="#catalog" className="hover:text-amber-600 transition-colors">Produk</a>
|
||||
<a href="#about" className="hover:text-amber-600 transition-colors">Tentang Kami</a>
|
||||
<a href="#order-guide" className="hover:text-amber-600 transition-colors">Cara Pesan</a>
|
||||
<a href="#contact" className="hover:text-amber-600 transition-colors">Hubungi Kami</a>
|
||||
{[
|
||||
{ id: 'hero', label: 'Beranda' },
|
||||
{ id: 'catalog', label: 'Produk' },
|
||||
{ id: 'about', label: 'Tentang Kami' },
|
||||
{ id: 'order-guide', label: 'Cara Pesan' },
|
||||
{ id: 'contact', label: 'Hubungi Kami' },
|
||||
].map((item) => (
|
||||
<a
|
||||
key={item.id}
|
||||
href={`#${item.id}`}
|
||||
className={`transition-colors ${activeSection === item.id ? 'text-amber-600' : 'hover:text-amber-600'}`}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<a
|
||||
href="/admin/dashboard"
|
||||
className="p-2 rounded-full hover:bg-amber-50 text-slate-600 transition-colors"
|
||||
aria-label="Login"
|
||||
>
|
||||
<LogIn className="w-4.5 h-4.5 text-amber-600" />
|
||||
</a>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="icon" onClick={() => setCartOpen(true)} className="relative">
|
||||
<ShoppingBag className="w-4.5 h-4.5 text-amber-600" />
|
||||
{totalItems > 0 && (
|
||||
<span className="absolute -top-1 -right-1 w-4.5 h-4.5 bg-amber-600 text-white text-[9px] font-bold rounded-full flex items-center justify-center">
|
||||
{totalItems}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" asChild>
|
||||
<a href="/admin/dashboard" aria-label="Login">
|
||||
<LogIn className="w-4.5 h-4.5 text-amber-600" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@ -197,53 +226,49 @@ export default function Welcome({
|
||||
<div className="max-w-7xl mx-auto px-6 w-full lg:grow grid grid-cols-1 lg:grid-cols-12 gap-8 items-center py-12 z-10">
|
||||
{/* Left Column */}
|
||||
<div className="lg:col-span-5 flex flex-col justify-center space-y-6 lg:pr-6">
|
||||
<div className="inline-flex items-center space-x-2 bg-amber-500/10/10 text-[10px] text-amber-700 uppercase tracking-widest px-3 py-1 rounded-full w-fit font-bold shadow-sm">
|
||||
<span>{homepage.hero_badge}</span>
|
||||
</div>
|
||||
<Badge variant="secondary" className="w-fit bg-amber-500/10 text-amber-700 uppercase tracking-widest font-bold px-3 py-1 shadow-sm">
|
||||
Hadir dengan Gaya Terbaru
|
||||
</Badge>
|
||||
|
||||
<h1 className="text-5xl md:text-6xl font-serif leading-[1.1] tracking-tight font-light select-none text-slate-800">
|
||||
{homepage.hero_title_line1} <br />
|
||||
{homepage.hero_title_line2}{' '}
|
||||
Nyaman<br />
|
||||
dalam setiap{' '}
|
||||
<span className="font-normal italic text-amber-600 bg-amber-100/20 px-2 rounded-lg">
|
||||
{homepage.hero_title_highlight}
|
||||
Penampilan
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-sm text-slate-500 max-w-md leading-relaxed font-light">
|
||||
{homepage.hero_description}
|
||||
Koleksi fashion wanita dengan sentuhan elegan, material berkualitas, dan kenyamanan yang dapat Anda rasakan di setiap pemakaian.
|
||||
</p>
|
||||
|
||||
<div className="flex items-center space-x-4 pt-4">
|
||||
<a
|
||||
href="#catalog"
|
||||
className="inline-flex items-center justify-center bg-amber-600 text-white px-8 py-3.5 text-xs font-semibold uppercase tracking-widest rounded-xl shadow-lg shadow-amber-200/50 hover:bg-amber-500 hover:-translate-y-0.5 transition-all duration-300 group"
|
||||
>
|
||||
{homepage.hero_cta_primary_text}
|
||||
<ArrowRight className="w-3.5 h-3.5 ml-2 transform group-hover:translate-x-1 transition-transform" />
|
||||
</a>
|
||||
<a
|
||||
href="#about"
|
||||
className="inline-flex items-center justify-center border border-amber-200 text-xs font-semibold uppercase tracking-widest px-8 py-3.5 rounded-xl hover:bg-amber-500/5 hover:border-amber-400 transition-all"
|
||||
>
|
||||
{homepage.hero_cta_secondary_text}
|
||||
</a>
|
||||
<Button asChild className="bg-amber-600 text-white px-8 py-3.5 text-xs font-semibold uppercase tracking-widest rounded-xl shadow-lg shadow-amber-200/50 hover:bg-amber-500 hover:-translate-y-0.5 transition-all duration-300 group">
|
||||
<a href="#catalog">
|
||||
Beli Sekarang
|
||||
<ArrowRight className="w-3.5 h-3.5 ml-2 transform group-hover:translate-x-1 transition-transform" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button variant="outline" asChild className="border-amber-200 text-xs font-semibold uppercase tracking-widest px-8 py-3.5 rounded-xl hover:bg-amber-500/5 hover:border-amber-400 transition-all">
|
||||
<a href="#about">Tentang Kami</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Center Column: Visual */}
|
||||
<div className="lg:col-span-7 relative flex justify-center items-center h-[500px] lg:h-[650px] w-full">
|
||||
<div className="absolute inset-0 flex justify-center items-center pointer-events-none select-none z-0">
|
||||
<div className="text-[14vw] lg:text-[120px] font-serif font-bold leading-none tracking-tight flex opacity-[0.03]">
|
||||
<span className="text-slate-900">{homepage.hero_bg_text_left}</span>
|
||||
<span className="text-amber-600">{homepage.hero_bg_text_right}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-0 -z-10 flex justify-center items-center pointer-events-none select-none">
|
||||
<div className="absolute w-[350px] h-[350px] rounded-full bg-gradient-to-tr from-amber-400/25 via-yellow-400/20 to-orange-300/20 blur-3xl" />
|
||||
<div className="absolute w-[400px] h-[400px] rounded-full bg-gradient-to-br from-rose-300/20 via-amber-200/20 to-orange-300/20 blur-3xl" />
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-0 flex justify-center items-center pointer-events-none select-none z-0">
|
||||
<div className="text-[14vw] lg:text-[120px] font-serif font-bold leading-none tracking-tight flex text-amber-200">
|
||||
<span className="text-amber-600">DST</span>
|
||||
<span className="text-slate-900">Collection</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative w-[310px] h-[440px] lg:w-[390px] lg:h-[540px] rounded-[2rem] overflow-hidden z-10 transition-transform duration-500 hover:scale-[1.02]">
|
||||
<img
|
||||
src={homepage.hero_image_url || 'https://images.unsplash.com/photo-1490481651871-ab68de25d43d?w=1000&auto=format&fit=crop&q=80'}
|
||||
@ -302,14 +327,20 @@ export default function Welcome({
|
||||
{/* Scroll Indicator */}
|
||||
<div className="max-w-7xl mx-auto px-6 w-full flex items-center justify-between pb-8 z-10 text-xs font-semibold text-slate-500">
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="text-amber-600">#{homepage.scroll_hashtag}</span>
|
||||
<span className="text-amber-600">#DSTCollection</span>
|
||||
<span className="w-1 h-1 rounded-full bg-amber-300" />
|
||||
<span>{homepage.scroll_tagline}</span>
|
||||
<span>Bahan Adem & Lembut</span>
|
||||
</div>
|
||||
<a href="#catalog" className="flex items-center space-x-2 hover:text-amber-600 transition-colors">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
document.getElementById('catalog')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}}
|
||||
className="flex items-center space-x-2 hover:text-amber-600 transition-colors cursor-pointer text-xs font-semibold uppercase tracking-widest text-slate-500"
|
||||
>
|
||||
<span>Lihat katalog lengkap</span>
|
||||
<ChevronRight className="w-3.5 h-3.5 rotate-90 text-amber-600" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -317,47 +348,51 @@ export default function Welcome({
|
||||
<section id="catalog" className="max-w-7xl mx-auto px-6 py-20 border-t border-amber-100/30">
|
||||
<div className="space-y-6 mb-12">
|
||||
<div className="text-center space-y-2">
|
||||
<h2 className="text-xs uppercase tracking-[0.3em] font-bold text-amber-600">{homepage.catalog_badge}</h2>
|
||||
<h3 className="text-3xl font-serif text-slate-800">{homepage.catalog_title}</h3>
|
||||
<p className="text-sm text-slate-500 max-w-lg mx-auto font-light">{homepage.catalog_description}</p>
|
||||
<Badge variant="secondary" className="bg-amber-500/10 text-amber-700 uppercase tracking-[0.3em] font-bold px-3 py-1">
|
||||
Katalog Eksklusif
|
||||
</Badge>
|
||||
<h3 className="text-3xl font-serif text-slate-800">Koleksi Busana Pilihan</h3>
|
||||
<p className="text-sm text-slate-500 max-w-lg mx-auto font-light">Gunakan kategori dan filter di bawah untuk menyesuaikan pencarian busana idaman Anda dengan mudah.</p>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="bg-white/80 backdrop-blur-md p-6 rounded-3xl shadow-sm border border-amber-100/30 flex flex-col md:flex-row items-center gap-4 justify-between">
|
||||
<div className="relative w-full md:w-80">
|
||||
<input
|
||||
<Input
|
||||
value={searchInput}
|
||||
onChange={(e) => handleSearchChange(e.target.value)}
|
||||
type="text"
|
||||
placeholder={homepage.catalog_search_placeholder}
|
||||
className="w-full bg-slate-50 border border-slate-200 text-xs px-4 py-3 pr-10 rounded-xl outline-none focus:border-amber-500 text-slate-700 transition-all"
|
||||
placeholder="Cari produk..."
|
||||
className="w-full bg-slate-50 border-slate-200 text-xs px-4 py-3 pr-10 rounded-xl focus:border-amber-500 text-slate-700 transition-all"
|
||||
/>
|
||||
<Search className="absolute right-3 top-3.5 w-4 h-4 text-slate-400" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 w-full md:w-auto">
|
||||
<button
|
||||
<Button
|
||||
variant={!selectedCategory ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => handleCategoryChange(null)}
|
||||
className={`shrink-0 px-4 py-2 text-[11px] font-semibold uppercase tracking-wider rounded-xl border transition-all whitespace-nowrap ${
|
||||
!selectedCategory
|
||||
? 'bg-amber-600 text-white border-transparent shadow-md shadow-amber-200/50'
|
||||
: 'bg-transparent text-slate-600 border-slate-200 hover'
|
||||
}`}
|
||||
className={`shrink-0 text-[11px] font-semibold uppercase tracking-wider rounded-xl transition-all whitespace-nowrap ${!selectedCategory
|
||||
? 'bg-amber-600 text-white border-transparent shadow-md shadow-amber-200/50'
|
||||
: 'bg-transparent text-slate-600 border-slate-200 hover'
|
||||
}`}
|
||||
>
|
||||
Semua
|
||||
</button>
|
||||
</Button>
|
||||
{categories.map((category) => (
|
||||
<button
|
||||
<Button
|
||||
key={category.id}
|
||||
variant={selectedCategory === category.slug ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => handleCategoryChange(selectedCategory === category.slug ? null : category.slug)}
|
||||
className={`shrink-0 px-4 py-2 text-[11px] font-semibold uppercase tracking-wider rounded-xl border transition-all whitespace-nowrap ${
|
||||
selectedCategory === category.slug
|
||||
? 'bg-amber-600 text-white border-transparent shadow-md shadow-amber-200/50'
|
||||
: 'bg-transparent text-slate-600 border-slate-200 hover'
|
||||
}`}
|
||||
className={`shrink-0 text-[11px] font-semibold uppercase tracking-wider rounded-xl transition-all whitespace-nowrap ${selectedCategory === category.slug
|
||||
? 'bg-amber-600 text-white border-transparent shadow-md shadow-amber-200/50'
|
||||
: 'bg-transparent text-slate-600 border-slate-200 hover'
|
||||
}`}
|
||||
>
|
||||
{category.name}
|
||||
</button>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@ -384,6 +419,7 @@ export default function Welcome({
|
||||
key={product.id}
|
||||
product={product}
|
||||
onQuickView={setActiveProduct}
|
||||
contactPhone={contactPhone}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@ -394,12 +430,9 @@ export default function Welcome({
|
||||
<p className="text-sm text-slate-400">
|
||||
Mohon maaf, kami tidak menemukan pakaian yang cocok dengan kata kunci pencarian Anda.
|
||||
</p>
|
||||
<button
|
||||
onClick={handleResetFilters}
|
||||
className="mt-2 text-xs font-bold uppercase tracking-widest text-amber-600 underline underline-offset-4"
|
||||
>
|
||||
<Button variant="ghost" onClick={handleResetFilters} className="mt-2 text-xs font-bold uppercase tracking-widest text-amber-600 underline underline-offset-4">
|
||||
Reset Pencarian
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</InfiniteScroll>
|
||||
@ -409,9 +442,11 @@ export default function Welcome({
|
||||
<section id="gallery" className="w-full bg-gradient-to-br from-amber-50/40 via-yellow-50/20 to-orange-50/30 py-24 border-t border-b border-amber-100/30">
|
||||
<div className="max-w-7xl mx-auto px-6 space-y-12">
|
||||
<div className="text-center space-y-3 max-w-lg mx-auto">
|
||||
<span className="text-xs uppercase tracking-[0.3em] font-bold text-amber-600">{homepage.gallery_badge}</span>
|
||||
<h3 className="text-4xl font-serif leading-tight">{homepage.gallery_title}</h3>
|
||||
<p className="text-sm text-slate-500 leading-relaxed font-light">{homepage.gallery_description}</p>
|
||||
<Badge variant="secondary" className="bg-amber-500/10 text-amber-700 uppercase tracking-[0.3em] font-bold px-3 py-1">
|
||||
Galeri Kami
|
||||
</Badge>
|
||||
<h3 className="text-4xl font-serif leading-tight">Koleksi Lookbook</h3>
|
||||
<p className="text-sm text-slate-500 leading-relaxed font-light">Intip koleksi lookbook kami untuk inspirasi gaya sehari-hari. Padu padan pakaian modern yang nyaman untuk berbagai suasana.</p>
|
||||
</div>
|
||||
|
||||
{homepage.gallery_images.length > 0 ? (
|
||||
@ -441,13 +476,20 @@ export default function Welcome({
|
||||
{/* Order Guide Section */}
|
||||
<section id="order-guide" className="max-w-7xl mx-auto px-6 py-20 border-b border-amber-100/30">
|
||||
<div className="text-center space-y-3 mb-16">
|
||||
<h3 className="text-xs uppercase tracking-[0.3em] font-bold text-amber-600">{homepage.order_guide_badge}</h3>
|
||||
<h4 className="text-3xl font-serif">{homepage.order_guide_title}</h4>
|
||||
<p className="text-sm text-slate-500 max-w-md mx-auto font-light">{homepage.order_guide_description}</p>
|
||||
<Badge variant="secondary" className="bg-amber-500/10 text-amber-700 uppercase tracking-[0.3em] font-bold px-3 py-1">
|
||||
Langkah Pemesanan
|
||||
</Badge>
|
||||
<h4 className="text-3xl font-serif">Cara Melakukan Pemesanan</h4>
|
||||
<p className="text-sm text-slate-500 max-w-md mx-auto font-light">Sistem pemesanan kami sangat mudah dan terhubung langsung via WhatsApp untuk pelayanan cepat dan personal.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
{homepage.order_steps.map((step, index) => (
|
||||
{[
|
||||
{ title: 'Pilih Produk', description: 'Jelajahi pakaian favorit Anda, lalu ketuk "Lihat Detail" untuk memeriksa varian.' },
|
||||
{ title: 'Pilih Varian & Harga', description: 'Tentukan varian yang diinginkan dan pilih jenis harga (Eceran, Grosir, Agen, dll.).' },
|
||||
{ title: 'Masukkan Keranjang', description: 'Masukkan ke Keranjang Belanja untuk menampung seluruh daftar pakaian yang ingin Anda beli.' },
|
||||
{ title: 'Kirim ke WhatsApp', description: 'Klik tombol kirim pesanan, admin kami akan merespons rincian transfer bank dan pengiriman kurir.' },
|
||||
].map((step, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-white/50 p-6 rounded-2xl border border-amber-100/30 shadow-sm relative overflow-hidden text-center group hover:-translate-y-1 transition-transform"
|
||||
@ -479,12 +521,14 @@ export default function Welcome({
|
||||
/>
|
||||
</div>
|
||||
<div className="lg:col-span-6 space-y-6">
|
||||
<span className="text-xs uppercase tracking-[0.3em] font-bold text-amber-600">{homepage.about_badge}</span>
|
||||
<h3 className="text-3xl font-serif">{homepage.about_title}</h3>
|
||||
<Badge variant="secondary" className="bg-amber-500/10 text-amber-700 uppercase tracking-[0.3em] font-bold px-3 py-1">
|
||||
Tentang Kami
|
||||
</Badge>
|
||||
<h3 className="text-3xl font-serif">DST Collection</h3>
|
||||
<p className="text-sm text-slate-500 leading-relaxed font-light whitespace-pre-line">
|
||||
{aboutApp || 'Kami adalah rumah produksi busana dan batik lokal berkualitas tinggi. Berfokus pada keindahan motif, ketepatan detail jahitan, dan pemilihan kain adem yang mengedepankan aspek fungsionalitas dan estetika.'}
|
||||
{aboutApp || 'Kami adalah rumah produksi busana lokal berkualitas tinggi. Berfokus pada keindahan motif, ketepatan detail jahitan, dan pemilihan kain adem yang mengedepankan aspek fungsionalitas dan estetika.'}
|
||||
</p>
|
||||
{homepage.about_features.map((feature, index) => (
|
||||
{['Kain Rayon Super Tebal & Menyerap Keringat', 'Motif Eksklusif & Tidak Pasaran', 'Dukungan Penuh Layanan Admin Via WhatsApp'].map((feature, index) => (
|
||||
<div key={index} className="flex items-center space-x-3 text-xs text-slate-500 font-semibold">
|
||||
<Check className="w-4 h-4 text-emerald-500" />
|
||||
<span>{feature}</span>
|
||||
@ -496,13 +540,15 @@ export default function Welcome({
|
||||
|
||||
{/* Contact Section */}
|
||||
<section id="contact" className="max-w-7xl mx-auto px-6 py-20">
|
||||
<div className="bg-white/60 backdrop-blur-md border border-amber-100/30 p-8 rounded-3xl grid grid-cols-1 lg:grid-cols-2 gap-12 items-center shadow-sm">
|
||||
<div className="space-y-6">
|
||||
<span className="text-xs uppercase tracking-[0.3em] font-bold text-amber-600">{homepage.contact_badge}</span>
|
||||
<h3 className="text-3xl font-serif">{homepage.contact_title}</h3>
|
||||
<p className="text-sm text-slate-500 leading-relaxed font-light">{homepage.contact_description}</p>
|
||||
<div className="bg-white/60 backdrop-blur-md border border-amber-100/30 rounded-3xl overflow-hidden shadow-sm grid grid-cols-1 lg:grid-cols-2">
|
||||
<div className="p-8 space-y-6 flex flex-col justify-center">
|
||||
<Badge variant="secondary" className="bg-amber-500/10 text-amber-700 uppercase tracking-[0.3em] font-bold px-3 py-1 w-fit">
|
||||
Kontak Kami
|
||||
</Badge>
|
||||
<h3 className="text-3xl font-serif">Ada Pertanyaan? Hubungi Kami</h3>
|
||||
<p className="text-sm text-slate-500 leading-relaxed font-light">Kami sangat senang mendengarkan pertanyaan Anda terkait spesifikasi produk, ketersediaan grosir, atau kemitraan. Hubungi tim admin kami melalui media di bawah.</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
{contactPhone && (
|
||||
<div className="flex items-center space-x-3 text-sm">
|
||||
<div className="w-8 h-8 rounded-lg bg-amber-100/40 text-amber-600 flex items-center justify-center">
|
||||
@ -529,30 +575,17 @@ export default function Welcome({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-50 p-6 rounded-2xl border border-slate-200 space-y-4">
|
||||
<h4 className="font-bold text-base">{homepage.contact_form_title}</h4>
|
||||
<div className="space-y-3">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Nama Anda"
|
||||
className="w-full text-xs px-4 py-3 bg-white border border-slate-200 rounded-lg outline-none focus:border-amber-500 text-slate-700"
|
||||
/>
|
||||
<textarea
|
||||
placeholder="Tulis pesan Anda di sini..."
|
||||
rows={4}
|
||||
className="w-full text-xs px-4 py-3 bg-white border border-slate-200 rounded-lg outline-none focus:border-amber-500 text-slate-700"
|
||||
/>
|
||||
<a
|
||||
href={`https://wa.me/${(contactPhone || '6281234567890').replace(/[^0-9]/g, '')}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="w-full inline-flex items-center justify-center bg-emerald-600 hover:bg-emerald-500 text-white py-3 rounded-lg text-xs font-bold uppercase tracking-wider transition-colors"
|
||||
>
|
||||
<Phone className="w-4 h-4 mr-2" />
|
||||
Kirim Melalui WhatsApp
|
||||
</a>
|
||||
</div>
|
||||
<div className="h-80 lg:h-auto">
|
||||
<iframe
|
||||
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d304.913429094586!2d107.58352379265978!3d-6.414270414938328!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x2e696bd874fdab6d%3A0x12de5546c3d8eac4!2sAGEN%20JNE%20DEESTEPABUARAN!5e1!3m2!1sen!2sid!4v1786611903455!5m2!1sen!2sid"
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{ border: 0 }}
|
||||
allowFullScreen
|
||||
loading="lazy"
|
||||
referrerPolicy="no-referrer-when-downgrade"
|
||||
className="w-full h-full min-h-[320px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -561,13 +594,9 @@ export default function Welcome({
|
||||
<footer className="bg-white text-slate-600 py-16 border-t border-amber-100/30">
|
||||
<div className="max-w-7xl mx-auto px-6 grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="font-serif text-xl font-bold tracking-widest text-amber-600">
|
||||
{appName}
|
||||
</span>
|
||||
</div>
|
||||
<img src="/assets/logo.png" alt={appName} className="h-10 w-auto" />
|
||||
<p className="text-xs text-slate-400 leading-relaxed font-light">
|
||||
{homepage.footer_description}
|
||||
Galeri resmi {appName}. Pilihan busana lokal premium berpotongan modern dengan kenyamanan menyejukkan.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -610,12 +639,13 @@ export default function Welcome({
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-6 mt-12 pt-8 border-t border-amber-100/30 flex flex-col sm:flex-row justify-between items-center text-[10px] text-slate-400 font-light space-y-4 sm:space-y-0">
|
||||
<p>© {new Date().getFullYear()} {appName}. {homepage.footer_copyright}</p>
|
||||
<p>© {new Date().getFullYear()} {appName}. Hak Cipta Dilindungi.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{/* Product Modal */}
|
||||
<ProductModal product={activeProduct} onClose={() => setActiveProduct(null)} />
|
||||
<ProductModal product={activeProduct} onClose={() => setActiveProduct(null)} contactPhone={contactPhone} />
|
||||
<CartDrawer open={cartOpen} onClose={() => setCartOpen(false)} contactPhone={contactPhone} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -14,6 +14,7 @@ export type ProductVariant = {
|
||||
stock: number;
|
||||
formatted_stock: string;
|
||||
product_prices: ProductPrice[];
|
||||
photo_urls: string[];
|
||||
};
|
||||
|
||||
export type ProductCategory = {
|
||||
@ -42,40 +43,9 @@ export type Category = {
|
||||
};
|
||||
|
||||
export type HomepageData = {
|
||||
hero_badge: string;
|
||||
hero_title_line1: string;
|
||||
hero_title_line2: string;
|
||||
hero_title_highlight: string;
|
||||
hero_description: string;
|
||||
hero_cta_primary_text: string;
|
||||
hero_cta_secondary_text: string;
|
||||
hero_image_url: string | null;
|
||||
hero_bg_text_left: string;
|
||||
hero_bg_text_right: string;
|
||||
scroll_hashtag: string;
|
||||
scroll_tagline: string;
|
||||
catalog_badge: string;
|
||||
catalog_title: string;
|
||||
catalog_description: string;
|
||||
catalog_search_placeholder: string;
|
||||
gallery_badge: string;
|
||||
gallery_title: string;
|
||||
gallery_description: string;
|
||||
gallery_images: string[];
|
||||
order_guide_badge: string;
|
||||
order_guide_title: string;
|
||||
order_guide_description: string;
|
||||
order_steps: Array<{ title: string; description: string }>;
|
||||
about_badge: string;
|
||||
about_title: string;
|
||||
about_image_url: string | null;
|
||||
about_features: string[];
|
||||
contact_badge: string;
|
||||
contact_title: string;
|
||||
contact_description: string;
|
||||
contact_form_title: string;
|
||||
footer_description: string;
|
||||
footer_copyright: string;
|
||||
gallery_images: string[];
|
||||
};
|
||||
|
||||
export type Paginated<T> = {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user