diff --git a/app/Http/Controllers/HomepageController.php b/app/Http/Controllers/HomepageController.php new file mode 100644 index 0000000..c403a0c --- /dev/null +++ b/app/Http/Controllers/HomepageController.php @@ -0,0 +1,127 @@ +orderBy('name') + ->get(); + + $search = $request->input('search', ''); + $category = $request->input('category', ''); + + $productsQuery = Product::select(['id', 'name', 'slug', 'description', 'status']) + ->active() + ->with([ + 'categories:id,name,slug', + 'productVariants:id,product_id,name,stock', + 'productVariants.productPrices:id,variant_id,type,price', + ]); + + if ($search !== '') { + $productsQuery->where(function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%") + ->orWhere('description', 'like', "%{$search}%"); + }); + } + + if ($category !== '') { + $productsQuery->whereHas('categories', function ($q) use ($category) { + $q->where('slug', $category); + }); + } + + $products = Inertia::scroll( + fn () => $productsQuery->orderBy('created_at', 'desc')->paginate(12) + ); + + $galleryImages = array_map( + fn ($key) => str_starts_with($key, 'http') ? $key : $this->s3Service->getTemporaryUrl($key, 60), + $homepage->gallery_images ?? [], + ); + + return Inertia::render('welcome', [ + 'appName' => $system->app_name, + 'aboutApp' => $system->about_app, + 'contactEmail' => $system->email, + 'contactPhone' => $system->phone, + 'contactAddress' => $system->address, + 'instagramUrl' => $socialMedia->instagram_url, + '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, + ], + 'categories' => $categories, + 'products' => $products, + 'filters' => [ + 'search' => $search, + 'category' => $category, + ], + 'seo' => [ + 'title' => $system->app_name . ' - ' . $homepage->hero_badge, + 'description' => $homepage->hero_description, + 'image' => url('/assets/logo.png'), + 'url' => url('/'), + ], + ]); + } +} diff --git a/resources/js/components/home/ProductCard.tsx b/resources/js/components/home/ProductCard.tsx new file mode 100644 index 0000000..1130413 --- /dev/null +++ b/resources/js/components/home/ProductCard.tsx @@ -0,0 +1,117 @@ +import { formatRupiah } from '@/lib/rupiah'; +import type { Product } from '@/types/homepage'; + +type ProductCardProps = { + product: Product; + onQuickView: (product: Product) => void; +}; + +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`; + } + } + + const fallbacks: Record = { + '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 { + const prices = product.product_variants.flatMap((v) => + v.product_prices + .filter((p) => p.type === 'retail') + .map((p) => p.price), + ); + + if (prices.length === 0) { + const fallbackPrices = product.product_variants.flatMap((v) => + v.product_prices.map((p) => p.price), + ); + + if (fallbackPrices.length === 0) { + return 'Rp 0'; + } + + const min = Math.min(...fallbackPrices); + const max = Math.max(...fallbackPrices); + + return min === max + ? `Rp ${formatRupiah(min)}` + : `Rp ${formatRupiah(min)} - Rp ${formatRupiah(max)}`; + } + + const min = Math.min(...prices); + const max = Math.max(...prices); + + return min === max + ? `Rp ${formatRupiah(min)}` + : `Rp ${formatRupiah(min)} - Rp ${formatRupiah(max)}`; +} + +export default function ProductCard({ product, onQuickView }: ProductCardProps) { + return ( +
onQuickView(product)} + > +
+ {product.name} +
+
+ + Lihat Detail + +
+
+ +
+
+ {product.categories.slice(0, 2).map((cat) => ( + + {cat.name} + + ))} +
+

+ {product.name} +

+

+ {product.description || 'Koleksi fashion berkualitas'} +

+

+ {getProductPriceRange(product)} +

+
+
+ ); +} diff --git a/resources/js/components/home/ProductModal.tsx b/resources/js/components/home/ProductModal.tsx new file mode 100644 index 0000000..59f22bd --- /dev/null +++ b/resources/js/components/home/ProductModal.tsx @@ -0,0 +1,221 @@ +import { X, Star, Minus, Plus, Phone } from 'lucide-react'; +import { useState } from 'react'; +import { formatRupiah } from '@/lib/rupiah'; +import type { Product, ProductVariant, ProductPrice } from '@/types/homepage'; + +type ProductModalProps = { + product: Product | null; + onClose: () => void; +}; + +function getProductImage(): string { + return 'https://images.unsplash.com/photo-1490481651871-ab68de25d43d?w=1000&auto=format&fit=crop&q=80'; +} + +function getRetailPrice(variant: ProductVariant): number { + const retail = variant.product_prices.find((p) => p.type === 'retail'); + return retail?.price ?? 0; +} + +function getWholesalePrice(variant: ProductVariant): number { + const wholesale = variant.product_prices.find((p) => p.type === 'wholesale'); + return wholesale?.price ?? 0; +} + +export default function ProductModal({ product, onClose }: ProductModalProps) { + const [selectedVariant, setSelectedVariant] = useState(null); + const [selectedPriceType, setSelectedPriceType] = useState<'retail' | 'wholesale'>('retail'); + const [quantity, setQuantity] = useState(1); + + if (!product) return null; + + const variants = product.product_variants; + const activeVariant = selectedVariant ?? variants[0]; + const retailPrice = activeVariant ? getRetailPrice(activeVariant) : 0; + const wholesalePrice = activeVariant ? getWholesalePrice(activeVariant) : 0; + const currentPrice = selectedPriceType === 'retail' ? retailPrice : wholesalePrice; + const maxStock = activeVariant?.stock ?? 0; + + const handleWhatsAppOrder = () => { + if (!activeVariant) return; + + const priceLabel = selectedPriceType === 'retail' ? 'Retail' : 'Grosir'; + const message = `Halo, saya tertarik dengan produk:\n\n*${product.name}*\n- Varian: ${activeVariant.name}\n- Harga ${priceLabel}: Rp ${formatRupiah(currentPrice)}\n- Jumlah: ${quantity} pcs\n- Subtotal: Rp ${formatRupiah(currentPrice * quantity)}\n\nMohon info detail pesanan. Terima kasih!`; + + const phone = '6281234567890'; + const waUrl = `https://wa.me/${phone}?text=${encodeURIComponent(message)}`; + window.open(waUrl, '_blank'); + }; + + return ( +
+
e.stopPropagation()} + > + {/* Header */} +
+

Detail Produk

+ +
+ +
+ {/* Image */} +
+ {product.name} +
+ + {/* Details */} +
+
+
+ {product.categories.slice(0, 2).map((cat) => ( + + {cat.name} + + ))} +
+

{product.name}

+
+ +

+ {product.description || 'Produk fashion berkualitas dengan desain elegan dan bahan nyaman.'} +

+ + {/* Rating */} +
+
+ + + + + +
+ 5.0 (120+) +
+ + {/* Price */} +
+

Harga

+

+ Rp {formatRupiah(currentPrice)} +

+ {selectedPriceType === 'wholesale' && wholesalePrice > 0 && ( +

+ Harga retail: Rp {formatRupiah(retailPrice)} +

+ )} +
+ + {/* Variant Selection */} + {variants.length > 0 && ( +
+

Varian

+
+ {variants.map((variant) => ( + + ))} +
+
+ )} + + {/* Price Type */} +
+

Jenis Harga

+
+ + +
+
+ + {/* Quantity */} +
+

Jumlah

+
+ + {quantity} + +
+

Stok tersedia: {maxStock}

+
+ + {/* Total */} +
+
+ Subtotal + + Rp {formatRupiah(currentPrice * quantity)} + +
+
+ + {/* WhatsApp Order */} + +
+
+
+
+ ); +} diff --git a/resources/js/pages/welcome.tsx b/resources/js/pages/welcome.tsx index fd91ee3..226fc69 100644 --- a/resources/js/pages/welcome.tsx +++ b/resources/js/pages/welcome.tsx @@ -1,388 +1,621 @@ -import { Head, Link, usePage } from '@inertiajs/react'; -import { dashboard, login } from '@/routes'; -import { register } from '@/routes'; +import { Head, Link, router, InfiniteScroll } from '@inertiajs/react'; +import { useState, useCallback, useRef } from 'react'; +import { + 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'; -export default function Welcome() { - const { auth } = usePage().props; +type Props = { + appName: string; + aboutApp: string | null; + contactEmail: string | null; + contactPhone: string | null; + contactAddress: string | null; + instagramUrl: string | null; + facebookUrl: string | null; + tiktokUrl: string | null; + homepage: HomepageData; + categories: Category[]; + products: Paginated; + filters: { search: string; category: string }; + seo: { title: string; description: string; image: string; url: string }; +}; + +export default function Welcome({ + appName, + aboutApp, + contactEmail, + contactPhone, + contactAddress, + instagramUrl, + facebookUrl, + tiktokUrl, + homepage, + categories, + products, + filters, +}: Props) { + const [searchInput, setSearchInput] = useState(filters.search); + const [selectedCategory, setSelectedCategory] = useState(filters.category); + const [activeProduct, setActiveProduct] = useState(null); + const searchTimeoutRef = useRef>(undefined); + + const getProductImage = (product: Product): string => { + const fallbacks: Record = { + 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]; + }; + + const getProductPriceRange = (product: Product): string => { + const prices = product.product_variants.flatMap((v) => + v.product_prices + .filter((p: ProductPrice) => p.type === 'retail') + .map((p: ProductPrice) => p.price), + ); + + if (prices.length === 0) { + const fallbackPrices = product.product_variants.flatMap((v) => + v.product_prices.map((p: ProductPrice) => p.price), + ); + if (fallbackPrices.length === 0) return 'Rp 0'; + const min = Math.min(...fallbackPrices); + const max = Math.max(...fallbackPrices); + return min === max ? `Rp ${formatRupiah(min)}` : `Rp ${formatRupiah(min)} - Rp ${formatRupiah(max)}`; + } + + const min = Math.min(...prices); + const max = Math.max(...prices); + return min === max ? `Rp ${formatRupiah(min)}` : `Rp ${formatRupiah(min)} - Rp ${formatRupiah(max)}`; + }; + + const fetchProducts = useCallback( + (search: string, category: string) => { + const params = new URLSearchParams(); + if (search) params.set('search', search); + if (category) params.set('category', category); + const url = `/${params.toString() ? `?${params.toString()}` : ''}`; + + router.visit(url, { + only: ['products', 'filters'], + reset: ['products'], + preserveState: true, + preserveScroll: true, + }); + }, + [], + ); + + const handleSearchChange = useCallback( + (value: string) => { + setSearchInput(value); + if (searchTimeoutRef.current) { + clearTimeout(searchTimeoutRef.current); + } + searchTimeoutRef.current = setTimeout(() => { + fetchProducts(value, selectedCategory); + }, 400); + }, + [selectedCategory, fetchProducts], + ); + + const handleCategoryChange = useCallback( + (slug: string | null) => { + const newCategory = slug ?? ''; + setSelectedCategory(newCategory); + fetchProducts(searchInput, newCategory); + }, + [searchInput, fetchProducts], + ); + + const handleResetFilters = useCallback(() => { + setSearchInput(''); + setSelectedCategory(''); + fetchProducts('', ''); + }, [fetchProducts]); + + // Use first 3 products from current page as featured for hero + const featuredProducts = products.data.slice(0, 3); return ( <> - -
-
- +
+ + + +
+ + + +
+
-
-
-
-

- Let's get started + + {/* Hero Section */} +
+
+
+
+
+ +
+ {/* Left Column */} +
+
+ {homepage.hero_badge} +
+ +

+ {homepage.hero_title_line1}
+ {homepage.hero_title_line2}{' '} + + {homepage.hero_title_highlight} +

-

- Laravel has an incredibly rich ecosystem. -
- We suggest starting with the following. + +

+ {homepage.hero_description}

- -
+
+ + {/* Center Column: Visual */} +
+
+
+ {homepage.hero_bg_text_left} + {homepage.hero_bg_text_right} +
+
+ +
+
+
+
+ +
+ {appName} +
+ + {/* Floating Tag 1 */} + {featuredProducts[0] && ( +
setActiveProduct(featuredProducts[0])} + className="absolute top-10 left-4 md:-left-6 z-20 flex items-center space-x-3 bg-white/90 backdrop-blur-md p-3 rounded-2xl shadow-lg border border-amber-100/40 cursor-pointer transform hover:-translate-y-1 transition-all" + > + Preview Produk +
+

Terpopuler

+

{featuredProducts[0].name}

+ {getProductPriceRange(featuredProducts[0])} +
+
+ +
+
+ )} + + {/* Floating Tag 2 */} + {featuredProducts[1] && ( +
setActiveProduct(featuredProducts[1])} + className="absolute top-28 right-4 md:-right-6 z-20 flex items-center space-x-3 bg-white/90 backdrop-blur-md p-3 rounded-2xl shadow-lg border border-amber-100/40 cursor-pointer transform hover:-translate-y-1 transition-all" + > + Preview Produk +
+
+ {[1, 2, 3, 4, 5].map((i) => ( + + ))} +
+

{featuredProducts[1].name}

+ Stok Tersedia +
+
+ )} +
+
+ + {/* Scroll Indicator */} +
+
+ #{homepage.scroll_hashtag} + + {homepage.scroll_tagline} +
+ + Lihat katalog lengkap + + +
+
+ + {/* Catalog Section */} +
+
+
+

{homepage.catalog_badge}

+

{homepage.catalog_title}

+

{homepage.catalog_description}

+
+ + {/* Filters */} +
+
+ 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" + /> + +
+ +
+ + {categories.map((category) => ( + + ))} +
+
+
+ + {/* Product Grid with Infinite Scroll */} + ( +
+
+
+ Memuat produk lainnya... +
+
+ )} + > + {products.data.length > 0 ? ( +
+ {products.data.map((product) => ( + + ))} +
+ ) : ( +
+ +

Produk Tidak Ditemukan

+

+ Mohon maaf, kami tidak menemukan pakaian yang cocok dengan kata kunci pencarian Anda. +

+ +
+ )} + +
+ + {/* Gallery Section */} + + + {/* Order Guide Section */} +
+
+

{homepage.order_guide_badge}

+

{homepage.order_guide_title}

+

{homepage.order_guide_description}

+
+ +
+ {homepage.order_steps.map((step, index) => ( +
+
+ {index + 1} +
+
+ {index === 0 && } + {index === 1 && } + {index === 2 && } + {index === 3 && } +
+
{step.title}
+

{step.description}

+
+ ))} +
+
+ + {/* About Section */} +
+
+
+ Tentang Kami +
+
+ {homepage.about_badge} +

{homepage.about_title}

+

+ {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.'} +

+ {homepage.about_features.map((feature, index) => ( +
+ + {feature} +
+ ))} +
+
+
+ + {/* Contact Section */} +
+
+
+ {homepage.contact_badge} +

{homepage.contact_title}

+

{homepage.contact_description}

+ +
+ {contactPhone && ( +
+
+ +
+ {contactPhone} +
+ )} + {contactEmail && ( +
+
+ +
+ {contactEmail} +
+ )} + {contactAddress && ( +
+
+ +
+ {contactAddress} +
+ )} +
+
+ +
+

{homepage.contact_form_title}

+
+ +