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',
|
'categories:id,name,slug',
|
||||||
'productVariants:id,product_id,name,stock',
|
'productVariants:id,product_id,name,stock',
|
||||||
'productVariants.productPrices:id,variant_id,type,price',
|
'productVariants.productPrices:id,variant_id,type,price',
|
||||||
|
'productVariants.media',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($search !== '') {
|
if ($search !== '') {
|
||||||
@ -53,9 +54,23 @@ public function __invoke(Request $request): Response
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$products = Inertia::scroll(
|
$s3Service = $this->s3Service;
|
||||||
fn () => $productsQuery->orderBy('created_at', 'desc')->paginate(12)
|
$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(
|
$galleryImages = array_map(
|
||||||
fn ($key) => str_starts_with($key, 'http') ? $key : $this->s3Service->getTemporaryUrl($key, 60),
|
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,
|
'facebookUrl' => $socialMedia->facebook_url,
|
||||||
'tiktokUrl' => $socialMedia->tiktok_url,
|
'tiktokUrl' => $socialMedia->tiktok_url,
|
||||||
'homepage' => [
|
'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
|
'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))
|
? (str_starts_with($homepage->hero_image_url, 'http') ? $homepage->hero_image_url : $this->s3Service->getTemporaryUrl($homepage->hero_image_url, 60))
|
||||||
: null,
|
: 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
|
'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))
|
? (str_starts_with($homepage->about_image_url, 'http') ? $homepage->about_image_url : $this->s3Service->getTemporaryUrl($homepage->about_image_url, 60))
|
||||||
: null,
|
: null,
|
||||||
'about_features' => $homepage->about_features,
|
'gallery_images' => $galleryImages,
|
||||||
'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,
|
|
||||||
],
|
],
|
||||||
'categories' => $categories,
|
'categories' => $categories,
|
||||||
'products' => $products,
|
'products' => $products,
|
||||||
@ -118,8 +102,8 @@ public function __invoke(Request $request): Response
|
|||||||
'category' => $category,
|
'category' => $category,
|
||||||
],
|
],
|
||||||
'seo' => [
|
'seo' => [
|
||||||
'title' => $system->app_name.' - '.$homepage->hero_badge,
|
'title' => $system->app_name.' - Koleksi Segar 2026',
|
||||||
'description' => $homepage->hero_description,
|
'description' => 'Selamat datang di '.$system->app_name.'. Temukan keanggunan motif batik modern dan setelan pakaian santai berkualitas premium.',
|
||||||
'image' => url('/assets/logo.png'),
|
'image' => url('/assets/logo.png'),
|
||||||
'url' => url('/'),
|
'url' => url('/'),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -6,73 +6,11 @@
|
|||||||
|
|
||||||
class HomepageSettings extends Settings
|
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_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 ?string $about_image_url;
|
||||||
|
|
||||||
public array $about_features;
|
public array $gallery_images;
|
||||||
|
|
||||||
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 static function group(): string
|
public static function group(): string
|
||||||
{
|
{
|
||||||
|
|||||||
@ -8,82 +8,16 @@
|
|||||||
public function up(): void
|
public function up(): void
|
||||||
{
|
{
|
||||||
$this->migrator->inGroup('homepage', function (SettingsBlueprint $blueprint): 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_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('about_image_url', 'https://images.unsplash.com/photo-1595777457583-95e059d581b8?w=800&auto=format&fit=crop&q=80');
|
||||||
$blueprint->add('hero_bg_text_right', 'Collection');
|
$blueprint->add('gallery_images', [
|
||||||
|
|
||||||
// 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', [
|
|
||||||
'https://images.unsplash.com/photo-1595777457583-95e059d581b8?w=800&auto=format&fit=crop&q=80',
|
'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-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-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-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
|
public function up(): void
|
||||||
{
|
{
|
||||||
$this->migrator->inGroup('homepage', function (SettingsBlueprint $blueprint): void {
|
$this->migrator->inGroup('homepage', function (SettingsBlueprint $blueprint): void {
|
||||||
// Delete old deal fields
|
|
||||||
$blueprint->delete('deal_badge');
|
$blueprint->delete('deal_badge');
|
||||||
$blueprint->delete('deal_title');
|
$blueprint->delete('deal_title');
|
||||||
$blueprint->delete('deal_description');
|
$blueprint->delete('deal_description');
|
||||||
$blueprint->delete('deal_cta_text');
|
$blueprint->delete('deal_cta_text');
|
||||||
$blueprint->delete('deal_images');
|
$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 { FlashToast, PWAUpdateToast } from '@/components/notifications';
|
||||||
import { Toaster } from '@/components/ui/sonner';
|
import { Toaster } from '@/components/ui/sonner';
|
||||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||||
|
import { CartProvider } from '@/contexts/cart-context';
|
||||||
import { initializeTheme } from '@/hooks/use-appearance';
|
import { initializeTheme } from '@/hooks/use-appearance';
|
||||||
import AppLayout from '@/layouts/app-layout';
|
import AppLayout from '@/layouts/app-layout';
|
||||||
import AuthLayout from '@/layouts/auth-layout';
|
import AuthLayout from '@/layouts/auth-layout';
|
||||||
@ -38,12 +39,14 @@ createInertiaApp({
|
|||||||
strictMode: true,
|
strictMode: true,
|
||||||
withApp(app) {
|
withApp(app) {
|
||||||
return (
|
return (
|
||||||
|
<CartProvider>
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<FlashToast />
|
<FlashToast />
|
||||||
<PWAUpdateToast />
|
<PWAUpdateToast />
|
||||||
{app}
|
{app}
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
</CartProvider>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
progress: {
|
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 { formatRupiah } from '@/lib/rupiah';
|
||||||
|
import { formatWhatsAppPhone } from '@/lib/format';
|
||||||
import type { Product } from '@/types/homepage';
|
import type { Product } from '@/types/homepage';
|
||||||
|
import { ShoppingBag, ShoppingCart } from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
type ProductCardProps = {
|
type ProductCardProps = {
|
||||||
product: Product;
|
product: Product;
|
||||||
onQuickView: (product: Product) => void;
|
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 {
|
function getProductImage(product: Product): string {
|
||||||
for (const variant of product.product_variants) {
|
for (const variant of product.product_variants) {
|
||||||
const prices = variant.product_prices;
|
if (variant.photo_urls && variant.photo_urls.length > 0) {
|
||||||
if (prices.length > 0) {
|
return variant.photo_urls[0];
|
||||||
return `https://images.unsplash.com/photo-1490481651871-ab68de25d43d?w=800&auto=format&fit=crop&q=80`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return FALLBACK_IMAGE;
|
||||||
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];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProductPriceRange(product: Product): string {
|
function getProductPriceRange(product: Product): string {
|
||||||
@ -71,46 +56,87 @@ function getProductPriceRange(product: Product): string {
|
|||||||
: `Rp ${formatRupiah(min)} - Rp ${formatRupiah(max)}`;
|
: `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 (
|
return (
|
||||||
<div
|
<div className="cursor-pointer" onClick={() => onQuickView(product)}>
|
||||||
className="group 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">
|
<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
|
<img
|
||||||
src={getProductImage(product)}
|
src={getProductImage(product)}
|
||||||
alt={product.name}
|
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>
|
||||||
|
|
||||||
<div className="mt-4 space-y-1">
|
<div className="mt-4 space-y-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{product.categories.slice(0, 2).map((cat) => (
|
{product.categories.slice(0, 2).map((cat) => (
|
||||||
<span
|
<Badge key={cat.id} variant="secondary" className="bg-amber-50 text-amber-600 uppercase tracking-wider font-semibold px-2 py-0.5">
|
||||||
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}
|
{cat.name}
|
||||||
</span>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<h4 className="text-sm font-semibold text-slate-800 line-clamp-1">
|
<h4 className="text-sm font-semibold text-slate-800 line-clamp-1">
|
||||||
{product.name}
|
{product.name}
|
||||||
</h4>
|
</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">
|
<p className="text-sm font-bold text-amber-600">
|
||||||
{getProductPriceRange(product)}
|
{getProductPriceRange(product)}
|
||||||
</p>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,15 +1,26 @@
|
|||||||
import { X, Star, Minus, Plus, Phone } from 'lucide-react';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { useState } from 'react';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { useCart } from '@/contexts/cart-context';
|
||||||
import { formatRupiah } from '@/lib/rupiah';
|
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 = {
|
type ProductModalProps = {
|
||||||
product: Product | null;
|
product: Product | null;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
contactPhone?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getProductImage(): string {
|
const FALLBACK_IMAGE = 'https://images.unsplash.com/photo-1490481651871-ab68de25d43d?w=1000&auto=format&fit=crop&q=80';
|
||||||
return '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 {
|
function getRetailPrice(variant: ProductVariant): number {
|
||||||
@ -22,29 +33,46 @@ function getWholesalePrice(variant: ProductVariant): number {
|
|||||||
return wholesale?.price ?? 0;
|
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 [selectedVariant, setSelectedVariant] = useState<ProductVariant | null>(null);
|
||||||
const [selectedPriceType, setSelectedPriceType] = useState<'retail' | 'wholesale'>('retail');
|
const [selectedPriceType, setSelectedPriceType] = useState<'retail' | 'wholesale'>('retail');
|
||||||
|
const [selectedImage, setSelectedImage] = useState<string | null>(null);
|
||||||
const [quantity, setQuantity] = useState(1);
|
const [quantity, setQuantity] = useState(1);
|
||||||
|
const { addItem } = useCart();
|
||||||
|
|
||||||
if (!product) return null;
|
if (!product) return null;
|
||||||
|
|
||||||
const variants = product.product_variants;
|
const variants = product.product_variants;
|
||||||
const activeVariant = selectedVariant ?? variants[0];
|
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 retailPrice = activeVariant ? getRetailPrice(activeVariant) : 0;
|
||||||
const wholesalePrice = activeVariant ? getWholesalePrice(activeVariant) : 0;
|
const wholesalePrice = activeVariant ? getWholesalePrice(activeVariant) : 0;
|
||||||
const currentPrice = selectedPriceType === 'retail' ? retailPrice : wholesalePrice;
|
const currentPrice = selectedPriceType === 'retail' ? retailPrice : wholesalePrice;
|
||||||
const maxStock = activeVariant?.stock ?? 0;
|
const maxStock = activeVariant?.stock ?? 0;
|
||||||
|
|
||||||
const handleWhatsAppOrder = () => {
|
const handleVariantChange = (variant: ProductVariant) => {
|
||||||
|
setSelectedVariant(variant);
|
||||||
|
setSelectedImage(null);
|
||||||
|
setQuantity(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddToCart = () => {
|
||||||
if (!activeVariant) return;
|
if (!activeVariant) return;
|
||||||
|
|
||||||
const priceLabel = selectedPriceType === 'retail' ? 'Retail' : 'Grosir';
|
addItem({
|
||||||
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!`;
|
productId: product.id,
|
||||||
|
productName: product.name,
|
||||||
const phone = '6281234567890';
|
variantId: activeVariant.id,
|
||||||
const waUrl = `https://wa.me/${phone}?text=${encodeURIComponent(message)}`;
|
variantName: activeVariant.name,
|
||||||
window.open(waUrl, '_blank');
|
priceType: selectedPriceType,
|
||||||
|
price: currentPrice,
|
||||||
|
imageUrl: getVariantImage(activeVariant),
|
||||||
|
quantity,
|
||||||
|
});
|
||||||
|
toast.success(`${product.name} ditambahkan ke keranjang`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -59,35 +87,53 @@ export default function ProductModal({ product, onClose }: ProductModalProps) {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex justify-between items-center p-6 border-b border-amber-100/30">
|
<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>
|
<h3 className="text-lg font-serif text-slate-800">Detail Produk</h3>
|
||||||
<button
|
<Button variant="ghost" size="icon" onClick={onClose} className="rounded-full hover:bg-slate-100">
|
||||||
onClick={onClose}
|
|
||||||
className="p-2 rounded-full hover:bg-slate-100 transition-colors"
|
|
||||||
>
|
|
||||||
<X className="w-5 h-5 text-slate-500" />
|
<X className="w-5 h-5 text-slate-500" />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 p-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 p-6">
|
||||||
{/* Image */}
|
{/* Image */}
|
||||||
|
<div className="space-y-3">
|
||||||
<div className="relative aspect-3/4 rounded-2xl overflow-hidden">
|
<div className="relative aspect-3/4 rounded-2xl overflow-hidden">
|
||||||
<img
|
<img
|
||||||
src={getProductImage()}
|
key={currentImage}
|
||||||
|
src={currentImage}
|
||||||
alt={product.name}
|
alt={product.name}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 */}
|
{/* Details */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
{product.categories.slice(0, 2).map((cat) => (
|
{product.categories.slice(0, 2).map((cat) => (
|
||||||
<span
|
<Badge key={cat.id} variant="secondary" className="bg-amber-50 text-amber-600 uppercase tracking-wider font-semibold px-2 py-0.5">
|
||||||
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}
|
{cat.name}
|
||||||
</span>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-serif text-slate-800">{product.name}</h2>
|
<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.'}
|
{product.description || 'Produk fashion berkualitas dengan desain elegan dan bahan nyaman.'}
|
||||||
</p>
|
</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 */}
|
{/* Price */}
|
||||||
<div className="bg-amber-50/50 p-4 rounded-xl">
|
<div className="bg-amber-50/50 p-4 rounded-xl">
|
||||||
<p className="text-xs text-slate-500 mb-1">Harga</p>
|
<p className="text-xs text-slate-500 mb-1">Harga</p>
|
||||||
<p className="text-2xl font-bold text-amber-600">
|
<p className="text-2xl font-bold text-amber-600">
|
||||||
Rp {formatRupiah(currentPrice)}
|
Rp {formatRupiah(currentPrice)}
|
||||||
</p>
|
</p>
|
||||||
{selectedPriceType === 'wholesale' && wholesalePrice > 0 && (
|
|
||||||
<p className="text-xs text-slate-400 mt-1">
|
|
||||||
Harga retail: Rp {formatRupiah(retailPrice)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Variant Selection */}
|
{/* 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>
|
<p className="text-xs font-semibold text-slate-700 mb-2">Varian</p>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{variants.map((variant) => (
|
{variants.map((variant) => (
|
||||||
<button
|
<Button
|
||||||
key={variant.id}
|
key={variant.id}
|
||||||
onClick={() => setSelectedVariant(variant)}
|
variant={activeVariant?.id === variant.id ? 'default' : 'outline'}
|
||||||
className={`px-4 py-2 text-xs font-semibold rounded-xl border transition-all ${
|
size="sm"
|
||||||
activeVariant?.id === variant.id
|
onClick={() => handleVariantChange(variant)}
|
||||||
|
className={activeVariant?.id === variant.id
|
||||||
? 'bg-amber-600 text-white border-transparent'
|
? 'bg-amber-600 text-white border-transparent'
|
||||||
: 'bg-white text-slate-600 border-slate-200 hover:border-amber-300'
|
: 'bg-white text-slate-600 border-slate-200 hover:border-amber-300'
|
||||||
}`}
|
}
|
||||||
>
|
>
|
||||||
{variant.name}
|
{variant.name}
|
||||||
<span className="ml-1 text-[10px] opacity-70">
|
<span className="ml-1 text-[10px] opacity-70">
|
||||||
({variant.stock})
|
({variant.stock})
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -151,26 +181,28 @@ export default function ProductModal({ product, onClose }: ProductModalProps) {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold text-slate-700 mb-2">Jenis Harga</p>
|
<p className="text-xs font-semibold text-slate-700 mb-2">Jenis Harga</p>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<Button
|
||||||
|
variant={selectedPriceType === 'retail' ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
onClick={() => setSelectedPriceType('retail')}
|
onClick={() => setSelectedPriceType('retail')}
|
||||||
className={`px-4 py-2 text-xs font-semibold rounded-xl border transition-all ${
|
className={selectedPriceType === 'retail'
|
||||||
selectedPriceType === 'retail'
|
|
||||||
? 'bg-amber-600 text-white border-transparent'
|
? 'bg-amber-600 text-white border-transparent'
|
||||||
: 'bg-white text-slate-600 border-slate-200 hover:border-amber-300'
|
: 'bg-white text-slate-600 border-slate-200 hover:border-amber-300'
|
||||||
}`}
|
}
|
||||||
>
|
>
|
||||||
Retail
|
Retail
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
|
variant={selectedPriceType === 'wholesale' ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
onClick={() => setSelectedPriceType('wholesale')}
|
onClick={() => setSelectedPriceType('wholesale')}
|
||||||
className={`px-4 py-2 text-xs font-semibold rounded-xl border transition-all ${
|
className={selectedPriceType === 'wholesale'
|
||||||
selectedPriceType === 'wholesale'
|
|
||||||
? 'bg-amber-600 text-white border-transparent'
|
? 'bg-amber-600 text-white border-transparent'
|
||||||
: 'bg-white text-slate-600 border-slate-200 hover:border-amber-300'
|
: 'bg-white text-slate-600 border-slate-200 hover:border-amber-300'
|
||||||
}`}
|
}
|
||||||
>
|
>
|
||||||
Grosir
|
Grosir
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -178,19 +210,21 @@ export default function ProductModal({ product, onClose }: ProductModalProps) {
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold text-slate-700 mb-2">Jumlah</p>
|
<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">
|
<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))}
|
onClick={() => setQuantity(Math.max(1, quantity - 1))}
|
||||||
className="p-1 text-slate-500 hover:text-slate-800"
|
|
||||||
>
|
>
|
||||||
<Minus className="w-4 h-4" />
|
<Minus className="w-4 h-4" />
|
||||||
</button>
|
</Button>
|
||||||
<span className="text-sm font-semibold w-8 text-center">{quantity}</span>
|
<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))}
|
onClick={() => setQuantity(Math.min(maxStock, quantity + 1))}
|
||||||
className="p-1 text-slate-500 hover:text-slate-800"
|
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4" />
|
<Plus className="w-4 h-4" />
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[10px] text-slate-400 mt-1">Stok tersedia: {maxStock}</p>
|
<p className="text-[10px] text-slate-400 mt-1">Stok tersedia: {maxStock}</p>
|
||||||
</div>
|
</div>
|
||||||
@ -205,14 +239,19 @@ export default function ProductModal({ product, onClose }: ProductModalProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* WhatsApp Order */}
|
{/* Actions */}
|
||||||
<button
|
<div className="flex gap-3">
|
||||||
onClick={handleWhatsAppOrder}
|
<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">
|
||||||
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"
|
<ShoppingBag className="w-4 h-4 mr-2" />
|
||||||
>
|
Keranjang
|
||||||
<Phone className="w-4 h-4 mr-2" />
|
</Button>
|
||||||
Pesan Via WhatsApp
|
<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">
|
||||||
</button>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -141,7 +141,7 @@ export function AppHeader({ breadcrumbs = [] }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="ml-auto flex items-center space-x-2">
|
<div className="ml-auto flex items-center space-x-2">
|
||||||
<DropdownMenu>
|
<DropdownMenu>k
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
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);
|
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 type DateFormatStyle = 'full' | 'short' | 'datetime';
|
||||||
|
|
||||||
export function formatDate(dateString: string, style: DateFormatStyle = 'full'): string {
|
export function formatDate(dateString: string, style: DateFormatStyle = 'full'): string {
|
||||||
|
|||||||
@ -1,23 +1,28 @@
|
|||||||
import { Head, Link, router, InfiniteScroll } from '@inertiajs/react';
|
import ProductCard from '@/components/home/ProductCard';
|
||||||
import { useState, useCallback, useRef } from 'react';
|
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 {
|
import {
|
||||||
|
ArrowRight,
|
||||||
|
Check,
|
||||||
|
ChevronRight,
|
||||||
|
Info,
|
||||||
|
LogIn,
|
||||||
|
Mail,
|
||||||
|
MapPin,
|
||||||
|
Phone,
|
||||||
|
Plus,
|
||||||
Search,
|
Search,
|
||||||
ShoppingBag,
|
ShoppingBag,
|
||||||
Plus,
|
|
||||||
ArrowRight,
|
|
||||||
Star,
|
Star,
|
||||||
Info,
|
|
||||||
ChevronRight,
|
|
||||||
Phone,
|
|
||||||
MapPin,
|
|
||||||
Mail,
|
|
||||||
LogIn,
|
|
||||||
Check,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { formatRupiah } from '@/lib/rupiah';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import ProductCard from '@/components/home/ProductCard';
|
|
||||||
import ProductModal from '@/components/home/ProductModal';
|
|
||||||
import type { Product, Category, HomepageData, ProductPrice, Paginated } from '@/types/homepage';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
appName: string;
|
appName: string;
|
||||||
@ -52,30 +57,43 @@ export default function Welcome({
|
|||||||
const [searchInput, setSearchInput] = useState(filters.search);
|
const [searchInput, setSearchInput] = useState(filters.search);
|
||||||
const [selectedCategory, setSelectedCategory] = useState(filters.category);
|
const [selectedCategory, setSelectedCategory] = useState(filters.category);
|
||||||
const [activeProduct, setActiveProduct] = useState<Product | null>(null);
|
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 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 getProductImage = (product: Product): string => {
|
||||||
const fallbacks: Record<string, string[]> = {
|
for (const variant of product.product_variants) {
|
||||||
daster: [
|
if (variant.photo_urls && variant.photo_urls.length > 0) {
|
||||||
'https://images.unsplash.com/photo-1595777457583-95e059d581b8?w=800&auto=format&fit=crop&q=80',
|
return variant.photo_urls[0];
|
||||||
],
|
}
|
||||||
'setelan-celana': [
|
}
|
||||||
'https://images.unsplash.com/photo-1539109136881-3be0616acf4b?w=800&auto=format&fit=crop&q=80',
|
return FALLBACK_IMAGE;
|
||||||
],
|
|
||||||
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];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProductPriceRange = (product: Product): string => {
|
const getProductPriceRange = (product: Product): string => {
|
||||||
@ -145,7 +163,6 @@ export default function Welcome({
|
|||||||
fetchProducts('', '');
|
fetchProducts('', '');
|
||||||
}, [fetchProducts]);
|
}, [fetchProducts]);
|
||||||
|
|
||||||
// Use first 3 products from current page as featured for hero
|
|
||||||
const featuredProducts = products.data.slice(0, 3);
|
const featuredProducts = products.data.slice(0, 3);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -159,30 +176,42 @@ export default function Welcome({
|
|||||||
{/* Sticky Header */}
|
{/* Sticky Header */}
|
||||||
<header className="sticky top-0 z-40 bg-white/70 backdrop-blur-lg border-b border-amber-100/50 transition-all">
|
<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="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">
|
||||||
<Link href="/" className="flex items-center space-x-2">
|
<img src="/assets/logo.png" alt={appName} className="h-12 w-auto" />
|
||||||
<span className="font-serif text-2xl font-bold tracking-widest text-amber-600">
|
|
||||||
{appName}
|
|
||||||
</span>
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
|
||||||
|
|
||||||
<nav className="hidden lg:flex items-center space-x-8 text-xs font-semibold uppercase tracking-widest text-slate-600">
|
<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>
|
{ id: 'hero', label: 'Beranda' },
|
||||||
<a href="#about" className="hover:text-amber-600 transition-colors">Tentang Kami</a>
|
{ id: 'catalog', label: 'Produk' },
|
||||||
<a href="#order-guide" className="hover:text-amber-600 transition-colors">Cara Pesan</a>
|
{ id: 'about', label: 'Tentang Kami' },
|
||||||
<a href="#contact" className="hover:text-amber-600 transition-colors">Hubungi Kami</a>
|
{ 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>
|
</nav>
|
||||||
|
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center gap-2">
|
||||||
<a
|
<Button variant="ghost" size="icon" onClick={() => setCartOpen(true)} className="relative">
|
||||||
href="/admin/dashboard"
|
<ShoppingBag className="w-4.5 h-4.5 text-amber-600" />
|
||||||
className="p-2 rounded-full hover:bg-amber-50 text-slate-600 transition-colors"
|
{totalItems > 0 && (
|
||||||
aria-label="Login"
|
<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" />
|
<LogIn className="w-4.5 h-4.5 text-amber-600" />
|
||||||
</a>
|
</a>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</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">
|
<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 */}
|
{/* Left Column */}
|
||||||
<div className="lg:col-span-5 flex flex-col justify-center space-y-6 lg:pr-6">
|
<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">
|
<Badge variant="secondary" className="w-fit bg-amber-500/10 text-amber-700 uppercase tracking-widest font-bold px-3 py-1 shadow-sm">
|
||||||
<span>{homepage.hero_badge}</span>
|
Hadir dengan Gaya Terbaru
|
||||||
</div>
|
</Badge>
|
||||||
|
|
||||||
<h1 className="text-5xl md:text-6xl font-serif leading-[1.1] tracking-tight font-light select-none text-slate-800">
|
<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 />
|
Nyaman<br />
|
||||||
{homepage.hero_title_line2}{' '}
|
dalam setiap{' '}
|
||||||
<span className="font-normal italic text-amber-600 bg-amber-100/20 px-2 rounded-lg">
|
<span className="font-normal italic text-amber-600 bg-amber-100/20 px-2 rounded-lg">
|
||||||
{homepage.hero_title_highlight}
|
Penampilan
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="text-sm text-slate-500 max-w-md leading-relaxed font-light">
|
<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>
|
</p>
|
||||||
|
|
||||||
<div className="flex items-center space-x-4 pt-4">
|
<div className="flex items-center space-x-4 pt-4">
|
||||||
<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">
|
||||||
href="#catalog"
|
<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"
|
Beli Sekarang
|
||||||
>
|
|
||||||
{homepage.hero_cta_primary_text}
|
|
||||||
<ArrowRight className="w-3.5 h-3.5 ml-2 transform group-hover:translate-x-1 transition-transform" />
|
<ArrowRight className="w-3.5 h-3.5 ml-2 transform group-hover:translate-x-1 transition-transform" />
|
||||||
</a>
|
</a>
|
||||||
<a
|
</Button>
|
||||||
href="#about"
|
<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">
|
||||||
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"
|
<a href="#about">Tentang Kami</a>
|
||||||
>
|
</Button>
|
||||||
{homepage.hero_cta_secondary_text}
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Center Column: Visual */}
|
{/* Center Column: Visual */}
|
||||||
<div className="lg:col-span-7 relative flex justify-center items-center h-[500px] lg:h-[650px] w-full">
|
<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 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-[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 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>
|
||||||
|
|
||||||
|
<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]">
|
<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
|
<img
|
||||||
src={homepage.hero_image_url || 'https://images.unsplash.com/photo-1490481651871-ab68de25d43d?w=1000&auto=format&fit=crop&q=80'}
|
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 */}
|
{/* 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="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">
|
<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 className="w-1 h-1 rounded-full bg-amber-300" />
|
||||||
<span>{homepage.scroll_tagline}</span>
|
<span>Bahan Adem & Lembut</span>
|
||||||
</div>
|
</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>
|
<span>Lihat katalog lengkap</span>
|
||||||
<ChevronRight className="w-3.5 h-3.5 rotate-90 text-amber-600" />
|
<ChevronRight className="w-3.5 h-3.5 rotate-90 text-amber-600" />
|
||||||
</a>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<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="space-y-6 mb-12">
|
||||||
<div className="text-center space-y-2">
|
<div className="text-center space-y-2">
|
||||||
<h2 className="text-xs uppercase tracking-[0.3em] font-bold text-amber-600">{homepage.catalog_badge}</h2>
|
<Badge variant="secondary" className="bg-amber-500/10 text-amber-700 uppercase tracking-[0.3em] font-bold px-3 py-1">
|
||||||
<h3 className="text-3xl font-serif text-slate-800">{homepage.catalog_title}</h3>
|
Katalog Eksklusif
|
||||||
<p className="text-sm text-slate-500 max-w-lg mx-auto font-light">{homepage.catalog_description}</p>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Filters */}
|
{/* 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="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">
|
<div className="relative w-full md:w-80">
|
||||||
<input
|
<Input
|
||||||
value={searchInput}
|
value={searchInput}
|
||||||
onChange={(e) => handleSearchChange(e.target.value)}
|
onChange={(e) => handleSearchChange(e.target.value)}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={homepage.catalog_search_placeholder}
|
placeholder="Cari produk..."
|
||||||
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"
|
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" />
|
<Search className="absolute right-3 top-3.5 w-4 h-4 text-slate-400" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-2 w-full md:w-auto">
|
<div className="flex flex-wrap gap-2 w-full md:w-auto">
|
||||||
<button
|
<Button
|
||||||
|
variant={!selectedCategory ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
onClick={() => handleCategoryChange(null)}
|
onClick={() => handleCategoryChange(null)}
|
||||||
className={`shrink-0 px-4 py-2 text-[11px] font-semibold uppercase tracking-wider rounded-xl border transition-all whitespace-nowrap ${
|
className={`shrink-0 text-[11px] font-semibold uppercase tracking-wider rounded-xl transition-all whitespace-nowrap ${!selectedCategory
|
||||||
!selectedCategory
|
|
||||||
? 'bg-amber-600 text-white border-transparent shadow-md shadow-amber-200/50'
|
? 'bg-amber-600 text-white border-transparent shadow-md shadow-amber-200/50'
|
||||||
: 'bg-transparent text-slate-600 border-slate-200 hover'
|
: 'bg-transparent text-slate-600 border-slate-200 hover'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Semua
|
Semua
|
||||||
</button>
|
</Button>
|
||||||
{categories.map((category) => (
|
{categories.map((category) => (
|
||||||
<button
|
<Button
|
||||||
key={category.id}
|
key={category.id}
|
||||||
|
variant={selectedCategory === category.slug ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
onClick={() => handleCategoryChange(selectedCategory === category.slug ? null : category.slug)}
|
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 ${
|
className={`shrink-0 text-[11px] font-semibold uppercase tracking-wider rounded-xl transition-all whitespace-nowrap ${selectedCategory === category.slug
|
||||||
selectedCategory === category.slug
|
|
||||||
? 'bg-amber-600 text-white border-transparent shadow-md shadow-amber-200/50'
|
? 'bg-amber-600 text-white border-transparent shadow-md shadow-amber-200/50'
|
||||||
: 'bg-transparent text-slate-600 border-slate-200 hover'
|
: 'bg-transparent text-slate-600 border-slate-200 hover'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{category.name}
|
{category.name}
|
||||||
</button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -384,6 +419,7 @@ export default function Welcome({
|
|||||||
key={product.id}
|
key={product.id}
|
||||||
product={product}
|
product={product}
|
||||||
onQuickView={setActiveProduct}
|
onQuickView={setActiveProduct}
|
||||||
|
contactPhone={contactPhone}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -394,12 +430,9 @@ export default function Welcome({
|
|||||||
<p className="text-sm text-slate-400">
|
<p className="text-sm text-slate-400">
|
||||||
Mohon maaf, kami tidak menemukan pakaian yang cocok dengan kata kunci pencarian Anda.
|
Mohon maaf, kami tidak menemukan pakaian yang cocok dengan kata kunci pencarian Anda.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<Button variant="ghost" onClick={handleResetFilters} className="mt-2 text-xs font-bold uppercase tracking-widest text-amber-600 underline underline-offset-4">
|
||||||
onClick={handleResetFilters}
|
|
||||||
className="mt-2 text-xs font-bold uppercase tracking-widest text-amber-600 underline underline-offset-4"
|
|
||||||
>
|
|
||||||
Reset Pencarian
|
Reset Pencarian
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</InfiniteScroll>
|
</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">
|
<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="max-w-7xl mx-auto px-6 space-y-12">
|
||||||
<div className="text-center space-y-3 max-w-lg mx-auto">
|
<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>
|
<Badge variant="secondary" className="bg-amber-500/10 text-amber-700 uppercase tracking-[0.3em] font-bold px-3 py-1">
|
||||||
<h3 className="text-4xl font-serif leading-tight">{homepage.gallery_title}</h3>
|
Galeri Kami
|
||||||
<p className="text-sm text-slate-500 leading-relaxed font-light">{homepage.gallery_description}</p>
|
</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>
|
</div>
|
||||||
|
|
||||||
{homepage.gallery_images.length > 0 ? (
|
{homepage.gallery_images.length > 0 ? (
|
||||||
@ -441,13 +476,20 @@ export default function Welcome({
|
|||||||
{/* Order Guide Section */}
|
{/* Order Guide Section */}
|
||||||
<section id="order-guide" className="max-w-7xl mx-auto px-6 py-20 border-b border-amber-100/30">
|
<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">
|
<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>
|
<Badge variant="secondary" className="bg-amber-500/10 text-amber-700 uppercase tracking-[0.3em] font-bold px-3 py-1">
|
||||||
<h4 className="text-3xl font-serif">{homepage.order_guide_title}</h4>
|
Langkah Pemesanan
|
||||||
<p className="text-sm text-slate-500 max-w-md mx-auto font-light">{homepage.order_guide_description}</p>
|
</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>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
|
<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
|
<div
|
||||||
key={index}
|
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"
|
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>
|
||||||
<div className="lg:col-span-6 space-y-6">
|
<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>
|
<Badge variant="secondary" className="bg-amber-500/10 text-amber-700 uppercase tracking-[0.3em] font-bold px-3 py-1">
|
||||||
<h3 className="text-3xl font-serif">{homepage.about_title}</h3>
|
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">
|
<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>
|
</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">
|
<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" />
|
<Check className="w-4 h-4 text-emerald-500" />
|
||||||
<span>{feature}</span>
|
<span>{feature}</span>
|
||||||
@ -496,13 +540,15 @@ export default function Welcome({
|
|||||||
|
|
||||||
{/* Contact Section */}
|
{/* Contact Section */}
|
||||||
<section id="contact" className="max-w-7xl mx-auto px-6 py-20">
|
<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="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="space-y-6">
|
<div className="p-8 space-y-6 flex flex-col justify-center">
|
||||||
<span className="text-xs uppercase tracking-[0.3em] font-bold text-amber-600">{homepage.contact_badge}</span>
|
<Badge variant="secondary" className="bg-amber-500/10 text-amber-700 uppercase tracking-[0.3em] font-bold px-3 py-1 w-fit">
|
||||||
<h3 className="text-3xl font-serif">{homepage.contact_title}</h3>
|
Kontak Kami
|
||||||
<p className="text-sm text-slate-500 leading-relaxed font-light">{homepage.contact_description}</p>
|
</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 && (
|
{contactPhone && (
|
||||||
<div className="flex items-center space-x-3 text-sm">
|
<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">
|
<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>
|
</div>
|
||||||
|
<div className="h-80 lg:h-auto">
|
||||||
<div className="bg-slate-50 p-6 rounded-2xl border border-slate-200 space-y-4">
|
<iframe
|
||||||
<h4 className="font-bold text-base">{homepage.contact_form_title}</h4>
|
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"
|
||||||
<div className="space-y-3">
|
width="100%"
|
||||||
<input
|
height="100%"
|
||||||
type="text"
|
style={{ border: 0 }}
|
||||||
placeholder="Nama Anda"
|
allowFullScreen
|
||||||
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"
|
loading="lazy"
|
||||||
|
referrerPolicy="no-referrer-when-downgrade"
|
||||||
|
className="w-full h-full min-h-[320px]"
|
||||||
/>
|
/>
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -561,13 +594,9 @@ export default function Welcome({
|
|||||||
<footer className="bg-white text-slate-600 py-16 border-t border-amber-100/30">
|
<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="max-w-7xl mx-auto px-6 grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center space-x-2">
|
<img src="/assets/logo.png" alt={appName} className="h-10 w-auto" />
|
||||||
<span className="font-serif text-xl font-bold tracking-widest text-amber-600">
|
|
||||||
{appName}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-slate-400 leading-relaxed font-light">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -610,12 +639,13 @@ export default function Welcome({
|
|||||||
</div>
|
</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">
|
<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>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
{/* Product Modal */}
|
{/* 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>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -14,6 +14,7 @@ export type ProductVariant = {
|
|||||||
stock: number;
|
stock: number;
|
||||||
formatted_stock: string;
|
formatted_stock: string;
|
||||||
product_prices: ProductPrice[];
|
product_prices: ProductPrice[];
|
||||||
|
photo_urls: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProductCategory = {
|
export type ProductCategory = {
|
||||||
@ -42,40 +43,9 @@ export type Category = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type HomepageData = {
|
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_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_image_url: string | null;
|
||||||
about_features: string[];
|
gallery_images: string[];
|
||||||
contact_badge: string;
|
|
||||||
contact_title: string;
|
|
||||||
contact_description: string;
|
|
||||||
contact_form_title: string;
|
|
||||||
footer_description: string;
|
|
||||||
footer_copyright: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Paginated<T> = {
|
export type Paginated<T> = {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user