feat: integrate dynamic categories and product data into homepage with retail price attribute support
This commit is contained in:
parent
37f11e63f9
commit
470fbd4b40
@ -2,10 +2,17 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\Product;
|
||||
|
||||
class HomepageController extends Controller
|
||||
{
|
||||
public function __invoke()
|
||||
{
|
||||
return inertia('homepage');
|
||||
return inertia('homepage', [
|
||||
'categories' => Category::all(),
|
||||
'bestSellers' => Product::with(['categories', 'prices'])->active()->latest()->take(4)->get(),
|
||||
'topSellingProducts' => Product::with(['categories', 'prices'])->withCount('orderItems')->active()->orderBy('order_items_count', 'desc')->take(3)->get(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\PriceType;
|
||||
use Illuminate\Database\Eloquent\Attributes\Appends;
|
||||
use Illuminate\Database\Eloquent\Attributes\Guarded;
|
||||
use Illuminate\Database\Eloquent\Attributes\Scope;
|
||||
@ -21,11 +22,18 @@
|
||||
use Spatie\Sluggable\SlugOptions;
|
||||
|
||||
#[Guarded(['id'])]
|
||||
#[Appends(['thumbnail_url', 'images_data'])]
|
||||
#[Appends(['thumbnail_url', 'images_data', 'retail_price'])]
|
||||
class Product extends Model implements HasMedia
|
||||
{
|
||||
use HasFactory, HasSlug, InteractsWithMedia, LogsActivity, SoftDeletes;
|
||||
|
||||
protected function retailPrice(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->prices->where('price_type', PriceType::RETAIL)->first()?->price ?: 0,
|
||||
);
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import { Link } from '@inertiajs/react';
|
||||
import { ArrowRight, Facebook, Instagram, Twitter, X, Youtube } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { formatCurrency } from '@/lib/formatters';
|
||||
|
||||
export default function Homepage() {
|
||||
export default function Homepage({ categories = [], bestSellers = [], topSellingProducts = [] }: { categories: any[], bestSellers: any[], topSellingProducts: any[] }) {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const [selectedProduct, setSelectedProduct] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
@ -53,6 +55,16 @@ export default function Homepage() {
|
||||
scroll-behavior: smooth;
|
||||
scroll-padding-top: 54px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 2px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: #1e4a6d;
|
||||
border-radius: 10px;
|
||||
}
|
||||
`}</style>
|
||||
<div className="min-h-screen scroll-smooth bg-[#f7f3eb] font-serif text-[#1e4a6d] selection:bg-[#1e4a6d] selection:text-white">
|
||||
{/* Navbar */}
|
||||
@ -90,13 +102,16 @@ export default function Homepage() {
|
||||
{/* Categories List */}
|
||||
<div className="order-2 flex flex-col justify-center lg:order-1 lg:col-span-3">
|
||||
<div className="relative mb-8">
|
||||
<div className="flex flex-col border-y border-[#1e4a6d]/20">
|
||||
{['Kenyamanan Terpilih', 'Kemewahan Harian', 'Gaya Berkelanjutan', 'Seri Oxford', 'Seri Flanel'].map((cat) => (
|
||||
<button key={cat} className="group flex items-center justify-between border-b border-[#1e4a6d]/10 px-4 py-4 text-left text-xs font-bold uppercase tracking-wider transition-all hover:bg-[#1e4a6d] hover:text-white last:border-0">
|
||||
{cat}
|
||||
<div className="custom-scrollbar flex max-h-[300px] flex-col overflow-y-auto border-y border-[#1e4a6d]/20">
|
||||
{categories.map((cat) => (
|
||||
<button key={cat.id} className="group flex items-center justify-between border-b border-[#1e4a6d]/10 px-4 py-4 text-left text-xs font-bold uppercase tracking-wider transition-all hover:bg-[#1e4a6d] hover:text-white last:border-0">
|
||||
{cat.name}
|
||||
<ArrowRight size={14} className="opacity-0 transition-all group-hover:translate-x-1 group-hover:opacity-100" />
|
||||
</button>
|
||||
))}
|
||||
{categories.length === 0 && (
|
||||
<p className="py-8 text-center text-[10px] uppercase tracking-widest opacity-30 italic">Belum ada kategori</p>
|
||||
)}
|
||||
</div>
|
||||
{/* Diagonal decorative lines like in the image */}
|
||||
<div className="absolute -right-4 top-0 hidden h-full w-20 lg:block">
|
||||
@ -137,15 +152,26 @@ export default function Homepage() {
|
||||
</button>
|
||||
|
||||
<div className="mt-16 flex gap-4">
|
||||
{[
|
||||
'https://images.unsplash.com/photo-1718871716580-117417d490f6?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MzJ8fGRyZXNzfGVufDB8fDB8fHww',
|
||||
'https://plus.unsplash.com/premium_photo-1676234842565-bc1df0bfd45a?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1yZWxhdGVkfDh8fHxlbnwwfHx8fHw%3D',
|
||||
'https://images.unsplash.com/photo-1718871729636-887b3d2359ae?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1yZWxhdGVkfDE0fHx8ZW58MHx8fHx8'
|
||||
].map((url, i) => (
|
||||
<div key={i} className="h-24 w-20 overflow-hidden rounded-t-full border border-[#1e4a6d]/20 bg-white">
|
||||
<img src={url} alt="Shirt Preview" className="h-full w-full object-cover" />
|
||||
{topSellingProducts.map((product, i) => (
|
||||
<div
|
||||
key={product.id}
|
||||
className="group h-24 w-20 cursor-pointer overflow-hidden rounded-t-full border border-[#1e4a6d]/20 bg-white transition-all hover:scale-110"
|
||||
onClick={() => setSelectedProduct(product)}
|
||||
>
|
||||
<img
|
||||
src={product.thumbnail_url || 'https://images.unsplash.com/photo-1594179056536-dc8714fc8fdf?w=200&auto=format&fit=crop&q=60'}
|
||||
alt={product.name}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{topSellingProducts.length === 0 && (
|
||||
<div className="flex gap-4">
|
||||
{[1,2,3].map(i => (
|
||||
<div key={i} className="h-24 w-20 rounded-t-full border border-dashed border-[#1e4a6d]/20 bg-transparent"></div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -168,28 +194,38 @@ export default function Homepage() {
|
||||
<div className="border-y border-[#1e4a6d]/20">
|
||||
<div className="mx-auto max-w-7xl">
|
||||
<div className="grid grid-cols-1 border-x border-[#1e4a6d]/20 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{[
|
||||
{ name: 'Kenyamanan Terpilih', price: 'Rp 450.000', img: 'https://plus.unsplash.com/premium_photo-1661432689064-89feeaa63025?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1yZWxhdGVkfDI0fHx8ZW58MHx8fHx8' },
|
||||
{ name: 'Kemewahan Harian', price: 'Rp 520.000', img: 'https://images.unsplash.com/photo-1718871828963-450573c7b76f?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1yZWxhdGVkfDI3fHx8ZW58MHx8fHx8' },
|
||||
{ name: 'Seri Oxford', price: 'Rp 425.000', img: 'https://images.unsplash.com/photo-1583333001978-8c57d752ce5b?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1yZWxhdGVkfDM0fHx8ZW58MHx8fHx8' },
|
||||
{ name: 'Gaya Berkelanjutan', price: 'Rp 380.000', img: 'https://images.unsplash.com/photo-1594179056536-dc8714fc8fdf?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1yZWxhdGVkfDU3fHx8ZW58MHx8fHx8' }
|
||||
].map((item, idx) => (
|
||||
<div key={idx} className="group border-b border-[#1e4a6d]/20 p-8 last:border-b-0 sm:border-r sm:even:border-r-0 lg:border-b-0 lg:border-r lg:last:border-r-0">
|
||||
<div className="mb-8 aspect-square overflow-hidden bg-white">
|
||||
{bestSellers.map((item, idx) => (
|
||||
<div key={item.id} className="group border-b border-[#1e4a6d]/20 p-8 last:border-b-0 sm:border-r sm:even:border-r-0 lg:border-b-0 lg:border-r lg:last:border-r-0">
|
||||
<div
|
||||
className="mb-8 aspect-square overflow-hidden bg-white cursor-pointer"
|
||||
onClick={() => setSelectedProduct(item)}
|
||||
>
|
||||
<img
|
||||
src={item.img}
|
||||
src={item.thumbnail_url || 'https://images.unsplash.com/photo-1594179056536-dc8714fc8fdf?w=500&auto=format&fit=crop&q=60'}
|
||||
alt={item.name}
|
||||
className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-110"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="mb-2 flex flex-wrap justify-center gap-1">
|
||||
{item.categories?.map((c: any) => (
|
||||
<span key={c.id} className="rounded-full border border-[#1e4a6d]/10 bg-[#1e4a6d]/5 px-2 py-0.5 text-[7px] font-bold uppercase tracking-widest text-[#1e4a6d] opacity-60">
|
||||
{c.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="mb-2 text-[10px] font-bold uppercase tracking-widest group-hover:text-indigo-600">
|
||||
{item.name} <ArrowRight size={12} className="inline ml-1" />
|
||||
{item.name}
|
||||
</p>
|
||||
<p className="text-sm font-medium opacity-60">{item.price}</p>
|
||||
<p className="text-sm font-medium opacity-60">{formatCurrency(item.retail_price)}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{bestSellers.length === 0 && (
|
||||
<div className="col-span-full py-20 text-center text-xs uppercase tracking-widest opacity-40">
|
||||
Produk belum tersedia
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -331,6 +367,69 @@ export default function Homepage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Product Detail Modal */}
|
||||
<div className={`fixed inset-0 z-[70] flex items-center justify-center p-4 transition-all duration-500 ${
|
||||
selectedProduct ? 'visible opacity-100' : 'invisible opacity-0'
|
||||
}`}>
|
||||
<div className="absolute inset-0 bg-[#1e4a6d]/40 backdrop-blur-sm" onClick={() => setSelectedProduct(null)}></div>
|
||||
<div className={`relative w-full max-w-4xl overflow-hidden bg-[#f7f3eb] shadow-2xl transition-all duration-500 ${
|
||||
selectedProduct ? 'translate-y-0 scale-100' : 'translate-y-10 scale-95'
|
||||
}`}>
|
||||
<button className="absolute right-4 top-4 z-10 p-2 hover:rotate-90 transition-transform" onClick={() => setSelectedProduct(null)}>
|
||||
<X size={24} />
|
||||
</button>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2">
|
||||
{/* Product Images */}
|
||||
<div className="border-b border-[#1e4a6d]/10 md:border-b-0 md:border-r overflow-hidden bg-white">
|
||||
<div className="aspect-[4/5] overflow-hidden">
|
||||
<img
|
||||
src={selectedProduct?.thumbnail_url || 'https://images.unsplash.com/photo-1594179056536-dc8714fc8fdf?w=800&auto=format&fit=crop&q=60'}
|
||||
alt={selectedProduct?.name}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
{/* Image Gallery if exists */}
|
||||
{selectedProduct?.images_data?.length > 0 && (
|
||||
<div className="flex border-t border-[#1e4a6d]/10">
|
||||
{selectedProduct.images_data.map((img: any, i: number) => (
|
||||
<div key={i} className="aspect-square w-20 border-r border-[#1e4a6d]/10 overflow-hidden">
|
||||
<img src={img.url} alt="detail" className="h-full w-full object-cover opacity-60 hover:opacity-100 transition-opacity cursor-pointer" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Product Info */}
|
||||
<div className="flex flex-col p-8 md:p-12">
|
||||
<div className="mb-6 flex flex-wrap gap-2">
|
||||
{selectedProduct?.categories?.map((c: any) => (
|
||||
<span key={c.id} className="rounded-full border border-[#1e4a6d]/20 bg-[#1e4a6d]/5 px-3 py-1 text-[8px] font-bold uppercase tracking-widest text-[#1e4a6d]">
|
||||
{c.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<h2 className="mb-4 text-3xl font-medium leading-tight">{selectedProduct?.name}</h2>
|
||||
<p className="mb-8 text-2xl font-light text-[#1e4a6d]">{formatCurrency(selectedProduct?.retail_price || 0)}</p>
|
||||
|
||||
<div className="mb-10 space-y-4">
|
||||
<p className="text-xs font-bold uppercase tracking-widest opacity-60">Deskripsi</p>
|
||||
<p className="text-sm leading-relaxed opacity-70">
|
||||
{selectedProduct?.description || 'Tidak ada deskripsi untuk produk ini.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto pt-8 border-t border-[#1e4a6d]/10">
|
||||
<button className="w-full bg-[#1e4a6d] py-4 text-[10px] font-bold uppercase tracking-[0.2em] text-white transition-all hover:bg-[#15344e] flex items-center justify-center gap-2">
|
||||
Hubungi via WhatsApp <ArrowRight size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user