refactor: Consolidate home page UI into new hero and brands components and improve stock opname quantity handling.

This commit is contained in:
Yoga Pangestu 2026-02-10 15:02:08 +07:00
parent f844bb1fb7
commit 307213c7d2
24 changed files with 389 additions and 468 deletions

View File

@ -54,7 +54,7 @@ public function update(): void
continue;
}
$item->qty_physical = $qtyPhysical;
$item->qty_physical = str_replace('.', '', (string) $qtyPhysical);
$item->save();
}
}

View File

@ -22,6 +22,8 @@ class Index extends Component
{
use WithPagination;
public array $testimonials = [];
public array $bestSellingPerfumes = [];
public array $brands = [];
@ -34,23 +36,39 @@ class Index extends Component
public int $totalMembersCount = 0;
public $latestMembers = [];
public string $formattedTotalMembersCount = '';
public array $testimonials = [];
public array $latestMembers = [];
public function mount(): void
{
$this->testimonials = Testimonial::with(['user', 'user.customer'])
->show()
->latest()
->take(5)
->get()
->map(function (Testimonial $testimonial) {
$name = $testimonial->user->customer->name ?? 'User';
$testimonial->customer_name = $name;
$testimonial->avatar_url = 'https://ui-avatars.com/api/?name='.urlencode($name).'&background=random&color=fff';
return $testimonial;
})
->toArray();
$this->bestSellingPerfumes = Perfume::withSum(['items' => function ($query) {
$query->whereHas('order', function ($q) {
$q->paid();
});
}], 'quantity')
->orderByDesc('items_sum_quantity')
->take(2)
->take(5)
->get()
->map(function (Perfume $perfume) {
$perfume->image = $perfume->getFirstMediaUrl('image') ? $perfume->getFirstMediaUrl('image') : asset('assets/images/logo.png');
$perfume->image = $perfume->getFirstMediaUrl('image') ?: asset('assets/images/logo.png');
$perfume->concentration_label = $perfume->concentration->label();
$perfume->formatted_price = formatCurrencyNumber($perfume->sale_price, 'Rp');
$perfume->formatted_sold_count = formatCurrencyNumber($perfume->items_sum_quantity);
return $perfume;
})
@ -60,7 +78,7 @@ public function mount(): void
$this->brands = Brand::orderBy('sort_order')
->get()
->map(function (Brand $brand) {
$brand->image = $brand->getFirstMediaUrl('image') ? $brand->getFirstMediaUrl('image') : asset('assets/images/dark-logo.png');
$brand->image = $brand->getFirstMediaUrl('image') ?: asset('assets/images/dark-logo.png');
return $brand;
})
@ -70,33 +88,27 @@ public function mount(): void
$this->perfumeFeatures = PerfumeFeature::orderBy('sort_order')->get()->toArray();
$this->faqs = Faq::orderBy('sort_order')
->get()
->map(fn (Faq $faq) => [
'question' => $faq->question,
'answer' => $faq->answer,
])
->toArray();
$this->faqs = Faq::orderBy('sort_order')->get()->toArray();
$this->totalMembersCount = Customer::count();
$this->formattedTotalMembersCount = number_format($this->totalMembersCount);
$this->latestMembers = Customer::whereNotNull('user_id')
->inRandomOrder()
->take(3)
->get();
$this->testimonials = Testimonial::with(['user', 'user.customer'])
->show()
->latest()
->take(5)
->get()
->map(function (Customer $member) {
$member->avatar_url = 'https://ui-avatars.com/api/?name='.urlencode($member->name).'&background=random&color=fff';
return $member;
})
->toArray();
}
public function render(): View
{
return view('livewire.home.index', [
'pageTitle' => 'Beranda',
'pageTitle' => 'Selamat Datang! ✨',
'categories' => Category::perfume()
->orderBy('sort_order')
->paginate(12)

View File

@ -12,15 +12,30 @@
])]
class Outlet extends Component
{
public array $outlets = [];
public string $search = '';
public ?OutletModel $selectedOutlet = null;
public function mount(): void
public function showDetail(OutletModel $outletModel): void
{
$this->outlets = OutletModel::with(['facilities', 'openingHours'])
$this->selectedOutlet = $outletModel;
$this->selectedOutlet->load(['openingHours' => function ($query) {
$query->orderBy('day');
}]);
$this->selectedOutlet->openingHours->each(function ($hour) {
$hour->formatted_open_time = \Carbon\Carbon::parse($hour->open_time)->format('H:i');
$hour->formatted_close_time = \Carbon\Carbon::parse($hour->close_time)->format('H:i');
});
}
public function closeModal(): void
{
$this->selectedOutlet = null;
}
public function render(): View
{
$outlets = OutletModel::with(['facilities', 'openingHours'])
->operational()
->when($this->search, function ($query) {
$query->where(function ($query) {
@ -31,29 +46,17 @@ public function mount(): void
->latest()
->get()
->map(function ($outlet) {
$outlet->image = $outlet->getFirstMediaUrl('featured_image') ? $outlet->getFirstMediaUrl('featured_image') : asset('assets/images/dark-logo.png');
$outlet->image = $outlet->getFirstMediaUrl('featured_image') ?: asset('assets/images/dark-logo.png');
$outlet->status_label = $outlet->status->label();
$outlet->status_color = $outlet->status->color();
return $outlet;
})
->toArray();
}
});
public function showDetail(int $outletId): void
{
$this->selectedOutlet = OutletModel::with(['facilities', 'openingHours'])->find($outletId);
}
public function closeModal(): void
{
$this->selectedOutlet = null;
}
public function render(): View
{
return view('livewire.home.outlets', [
'pageTitle' => 'Outlet',
'pageTitle' => 'Outlet Terdekat ✨',
'pageDesc' => 'Temukan outlet kami di sekitarmu dan rasakan langsung kemewahan aromanya! 🚗🏢',
'outlets' => $outlets,
]);
}
}

View File

@ -30,13 +30,7 @@ protected function loadItems(): void
->where('period_month', now()->format('Y-m'))
->get()
->map(function ($stockOpname) {
$outlet = $stockOpname->outlet;
$parfumCount = $outlet->perfumes()->count();
$botolCount = $outlet->bottles()->count();
$produkCount = $outlet->products()->count();
$total = $parfumCount + $botolCount + $produkCount;
$total = $stockOpname->items()->count();
$filled = $stockOpname->items()
->whereNotNull('qty_physical')

View File

@ -8,6 +8,7 @@
use App\Models\StockOpnameItem;
use App\Traits\Authorization\WithAuthorization;
use App\Traits\Components\WithConfirmation;
use App\Traits\Components\WithToast;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Collection;
use Livewire\Attributes\Title;
@ -16,7 +17,7 @@
#[Title('Kelola Stock Opname')]
class Manage extends Component
{
use WithAuthorization, WithConfirmation;
use WithAuthorization, WithConfirmation, WithToast;
public StockOpnameForm $form;
@ -46,19 +47,27 @@ public function updated(string $propertyName, $value): void
{
$this->canOrAbort('manage stock opname');
$this->form->update();
if (str_starts_with($propertyName, 'form.outlet_stock.')) {
$itemId = explode('.', $propertyName)[2];
$item = $this->items->firstWhere('id', $itemId);
if ($item) {
$item->qty_physical = $value;
$item->difference = $value - $item->qty_system;
$normalizedValue = str_replace('.', '', (string) $value);
$item->qty_physical = (int) $normalizedValue;
$item->difference = (int) $normalizedValue - $item->qty_system;
}
}
}
public function save(): void
{
$this->canOrAbort('manage stock opname');
$this->form->update();
$this->toast('Data stock opname berhasil disimpan.', 'Berhasil');
}
public function render(): View
{
return view('livewire.studio.stock.stock-opname.manage', [

View File

@ -1,8 +1,8 @@
<div class="flex justify-center flex-col">
<div class="flex justify-center flex-col mb-10">
<dotlottie-wc src="{{ asset('assets/animations/boxing-cat.json') }}" autoplay loop
style="width: 200px; height: 200px; margin: 0 auto;">
</dotlottie-wc>
<p class="block text-center italic mt-5">
{!! isset($message) ? $message : 'Yahhh, sepertinya belum ada data yang tersedia.' !!}
<p class="block text-center italic mt-5 text-home-foreground font-medium">
{!! $message ?? 'Yahhh, sepertinya belum ada data yang tersedia. 😉✨' !!}
</p>
</div>

View File

@ -13,7 +13,7 @@
</head>
<body>
@include('components.sections.ui.home._header')
<x-sections.ui.home.header :header="$header" />
@persist('toast')
<flux:toast position="bottom end" class="ps-6" />
@ -21,7 +21,7 @@
{{ $slot }}
@include('components.sections.ui.home._footer')
<x-sections.ui.home.footer :generalSettings="$generalSettings" :contactSettings="$contactSettings" />
@fluxScripts

View File

@ -12,11 +12,11 @@ class="w-full h-[400px] lg:h-[600px] object-cover rounded-2xl shadow-lg">
<header>
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-home-foreground leading-tight mb-6">
Keunggulan Produk Kami.
Kenapa Harus Kami? 🏆✨
</h2>
<p class="text-base md:text-lg text-home-foreground/70 leading-relaxed">
Setiap parfum dibuat dengan standar tinggi agar kamu mendapatkan kualitas terbaik dan pengalaman
wangi yang tak terlupakan.
Tiap tetesnya dibikin spesial buat kamu, biar wangimu tahan lama dan bikin happy sepanjang hari!
💖✨
</p>
</header>

View File

@ -0,0 +1,18 @@
<section class="bg-home-foreground py-8 overflow-hidden" id="brand-slider">
<div class="swiper brand-swiper overflow-visible">
<div class="swiper-wrapper !ease-linear">
@foreach (array_merge($brands, $brands) as $brand)
<div class="swiper-slide !w-auto">
<div class="flex flex-col items-center px-10 group">
<img src="{{ $brand['image'] }}" alt="{{ $brand['name'] }}"
class="w-10 h-10 lg:w-14 lg:h-14 object-contain brightness-0 invert opacity-50 group-hover:opacity-100 transition-all duration-500 filter grayscale hover:grayscale-0">
<h3
class="mt-3 text-[10px] lg:text-xs font-bold text-center text-white/30 uppercase tracking-[0.2em] group-hover:text-white transition-all duration-500">
{{ $brand['name'] }}
</h3>
</div>
</div>
@endforeach
</div>
</div>
</section>

View File

@ -4,11 +4,10 @@
<header class="text-center mb-16">
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-home-background leading-tight mb-4">
Temukan Aroma yang <br class="hidden sm:block">
Cocok dengan Kepribadianmu.
Temukan Aroma yang Kamu Banget! 💃✨
</h2>
<p class="text-gray-500 text-lg max-w-2xl mx-auto">
Pilih kategori aroma yang paling menggambarkan dirimu dan ciptakan kesan tak terlupakan.
Pilih kategori aroma yang paling menggambarkan dirimu dan bikin kesan tak terlupakan! 🌹
</p>
</header>

View File

@ -1,3 +1,9 @@
@props([
'totalMembersCount' => 0,
'formattedTotalMembersCount' => '',
'latestMembers' => [],
])
<section class="bg-home-primary text-home-foreground font-sans py-24 relative overflow-hidden">
<div class="container mx-auto px-4 md:px-8 lg:px-24 xl:px-32 relative z-10">
@ -5,31 +11,30 @@
<div class="space-y-8">
<h2 class="text-4xl md:text-5xl font-bold tracking-tight leading-tight text-home-background">
Bergabung dengan Member Eksklusif.
Jadi Bagian Keluarga Eksklusif Kami! 👑✨
</h2>
<p class="text-home-background/40 text-lg leading-relaxed max-w-md">
Dapatkan akses awal ke koleksi terbaru, diskon member khusus, dan tips perawatan parfum langsung
dari ahlinya.
Dapetin akses VIP ke koleksi terbaru, diskon spesial, dan tips wangi-wangian eksklusif cuma buat
kamu! 😉🎉
</p>
<form onsubmit="event.preventDefault();" class="flex flex-col sm:flex-row gap-4 w-full">
<button type="submit"
class="px-8 py-4 rounded-xl bg-home-background text-home-foreground font-bold hover:bg-home-secondary hover:text-home-foreground transition-all duration-300 shadow-lg shadow-home-background/20 whitespace-nowrap">
Gabung Sekarang
Join Sekarang Yuk! 🚀
</button>
</form>
<div class="flex items-center gap-4 text-sm text-home-background/40">
<div class="flex -space-x-2">
@foreach ($latestMembers as $member)
<img class="w-8 h-8 rounded-full border-2 border-white"
src="https://ui-avatars.com/api/?name={{ urlencode($member->name) }}&background=random&color=fff"
alt="{{ $member->name }}">
<img class="w-8 h-8 rounded-full border-2 border-white" src="{{ $member['avatar_url'] }}"
alt="{{ $member['name'] }}">
@endforeach
</div>
<p>Bergabung bersama <span
class="text-home-background font-bold">{{ number_format($totalMembersCount) }}+</span>
member lainnya.</p>
<p>Gabung sama <span
class="text-home-background font-bold">{{ $formattedTotalMembersCount }}+</span>
parfum lovers lainnya! 💕</p>
</div>
</div>

View File

@ -39,7 +39,7 @@ class="w-10 h-10 rounded-full bg-white/5 flex items-center justify-center text-g
</div>
<div class="md:col-span-3 md:pl-8">
<h3 class="text-white font-bold mb-6 text-sm uppercase tracking-wider">Tautan Cepat</h3>
<h3 class="text-white font-bold mb-6 text-sm uppercase tracking-wider">Cek Link Ini </h3>
<ul class="space-y-4 text-sm text-gray-400">
<li>
<a href="{{ route('homepage') }}" wire:navigate
@ -64,7 +64,7 @@ class="hover:text-home-secondary transition-colors duration-300 flex items-cente
<div class="md:col-span-5 flex flex-col gap-6">
<div>
<h3 class="text-white font-bold mb-6 text-sm uppercase tracking-wider">Kontak Kami</h3>
<h3 class="text-white font-bold mb-6 text-sm uppercase tracking-wider">Sapa Kami Yuk! 👋</h3>
<div class="space-y-5 text-sm text-gray-400">
<div class="flex items-start gap-4">
<div class="mt-1"><svg class="w-5 h-5 text-home-primary" fill="none"
@ -102,7 +102,7 @@ class="hover:text-home-secondary transition-colors duration-300 flex items-cente
<div class="border-t border-white/10 pt-8 flex flex-col md:flex-row justify-between items-center gap-4">
<div class="text-sm text-gray-500">
&copy; {{ date('Y') }} Yadi Parfum. Semua hak dilindungi undang-undang.
&copy; {{ date('Y') }} Yadi Parfum. Dibuat dengan cinta untuk kamu! ❤️✨
</div>
{{-- <div class="flex gap-6 text-sm text-gray-500">
<a href="#" class="hover:text-white transition-colors">Privacy Policy</a>

View File

@ -62,7 +62,7 @@ class="{{ request()->routeIs($item['match']) ? 'font-bold text-home-primary' : '
'border-home-foreground text-home-foreground': !scrolled || isOpen
}"
class="hidden lg:flex items-center border px-4 py-2 rounded-full transition-all duration-200 hover:scale-105 font-medium">
Masuk
Yuk Masuk!
</a>
<a href="{{ route('profile.redirect') }}" id="profile-mobile" wire:navigate

View File

@ -0,0 +1,193 @@
@props([
'totalMembersCount' => 0,
'formattedTotalMembersCount' => '',
'latestMembers' => [],
'testimonials' => [],
'bestSellingPerfumes' => [],
])
<main class="flex flex-col lg:flex-row justify-between lg:min-h-screen w-full relative overflow-x-hidden">
{{-- Left Section --}}
<section
class="relative w-full lg:w-[35%] pt-24 lg:pt-0 py-0 px-6 lg:px-10 lg:pb-8 flex flex-col lg:min-h-screen lg:justify-between space-y-6 lg:space-y-0 z-20 bg-white">
<div class="flex flex-col justify-center flex-1 space-y-6 lg:pt-48">
@if ($totalMembersCount > 0)
<header class="flex flex-wrap items-center gap-4">
<div class="flex items-center">
@foreach ($latestMembers as $member)
<figure
class="border-2 border-home-background w-8 h-8 lg:w-10 lg:h-10 rounded-full overflow-hidden shadow-sm {{ !$loop->first ? '-ms-2' : '' }}">
<img src="{{ $member['avatar_url'] }}" alt="{{ $member['name'] }}"
class="w-full h-full object-cover">
</figure>
@endforeach
</div>
<div
class="border border-home-primary/30 py-1 px-3 rounded-full text-sm lg:text-lg text-home-primary">
<span class="text-home-primary font-bold">{{ $formattedTotalMembersCount }}</span>+ Customer
Happy &
Percaya
Kami 😍
</div>
</header>
@endif
<h1 class="text-home-foreground text-4xl lg:text-6xl leading-tight text-start font-bold">
Temukan<br>
<span class="italic font-serif font-light text-home-primary">
signature scent-mu
</span> hari ini
</h1>
<p class="text-lg w-[90%] lg:w-full lg:text-lg text-home-foreground/80">
Tiap parfum punya cerita lho. Pilih aroma yang pas sama gayamu dan biarkan wangimu bercerita! 😉🌸
</p>
<div class="flex items-center">
<a href="{{ route('product.index') }}" wire:navigate
class="bg-home-primary text-home-background py-3 px-6 rounded-full hover:shadow-lg hover:shadow-home-primary/30 transition-all font-medium">
Mulai Belanja 🛍️
</a>
<span class="w-8 h-[2px] bg-home-primary/20"></span>
<a href="{{ route('product.index') }}" wire:navigate
class="flex items-center justify-center w-12 h-12 border border-home-primary rounded-full hover:bg-home-background transition-colors group bg-transparent">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" class="w-5 h-5 text-home-primary">
<path stroke-linecap="round" stroke-linejoin="round"
d="m4.5 19.5 15-15m0 0H8.25m11.25 0v11.25" />
</svg>
</a>
</div>
</div>
{{-- Testimonial Section --}}
<div class="my-3">
@if (!empty($testimonials))
<div
class="testimonials-swiper-container relative w-full mt-auto h-[300px] lg:h-[250px] bg-home-background/40 backdrop-blur-md border border-home-secondary rounded-2xl z-30 shadow-sm overflow-hidden transition-all duration-300">
<div class="swiper testimonials-swiper w-full h-full">
<div class="swiper-wrapper">
@foreach ($testimonials as $testimonial)
<div class="swiper-slide flex flex-col justify-center py-0 px-6 lg:px-10">
<div class="flex items-center gap-4 my-3">
<img src="{{ $testimonial['avatar_url'] }}"
alt="{{ $testimonial['customer_name'] }}"
class="w-12 h-12 rounded-full object-cover ring-2 ring-home-background">
<div>
<h4 class="font-bold text-home-foreground text-sm lg:text-base">
{{ $testimonial['customer_name'] }}</h4>
<div class="flex text-home-primary text-xs">
@for ($i = 0; $i < $testimonial['rating']; $i++)
@endfor
</div>
</div>
</div>
<p
class="text-home-neutral text-base lg:text-lg italic leading-relaxed break-words line-clamp-4 lg:line-clamp-3">
&quot;{{ $testimonial['content'] }}&quot;
</p>
</div>
@endforeach
</div>
</div>
<div class="absolute bottom-4 right-4 flex items-center gap-2 z-30">
<button
class="testimonials-button-prev w-10 h-10 rounded-full bg-home-background/80 backdrop-blur border border-home-background shadow-sm hover:bg-home-background flex justify-center items-center transition-colors group">
<svg xmlns="http://www.w3.org/2000/svg"
class="w-4 h-4 text-home-primary group-hover:scale-110 transition-transform"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 19l-7-7 7-7" />
</svg>
</button>
<button
class="testimonials-button-next w-10 h-10 rounded-full bg-home-primary shadow-md hover:bg-home-primary/80 flex justify-center items-center transition-colors group">
<svg xmlns="http://www.w3.org/2000/svg"
class="w-4 h-4 text-home-background group-hover:scale-110 transition-transform"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
</div>
@endif
</div>
</section>
{{-- Right Section --}}
<section
class="relative w-full lg:w-[65%] flex flex-col lg:flex-row justify-end items-end py-6 lg:pt-12 lg:pb-8 pr-0 lg:px-10 z-10">
<div
class="absolute inset-0 bg-[url('https://images.unsplash.com/photo-1591925463023-1ca6b0636780?q=80&w=1632&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D')] bg-cover bg-center bg-no-repeat">
<div
class="absolute inset-0 bg-gradient-to-t from-home-background/90 via-home-background/50 to-transparent lg:hidden">
</div>
</div>
<div class="relative w-full px-6 lg:px-0 my-3 overflow-hidden">
<div class="swiper-container best-selling-swiper">
<div class="swiper overflow-visible">
<div class="swiper-wrapper !ease-linear">
@foreach ($bestSellingPerfumes as $perfume)
<div class="swiper-slide">
<article
class="bg-white/90 backdrop-blur-md rounded-2xl shadow-lg hover:shadow-xl transition-all flex flex-row items-center p-4 h-auto lg:h-[250px] group border border-white/20">
<figure
class="w-[40%] lg:w-auto h-32 lg:h-full overflow-hidden rounded-xl bg-gray-100">
<img src="{{ $perfume['image'] }}"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
alt="{{ $perfume['name'] }}">
</figure>
<div
class="flex-1 ml-4 lg:ml-6 flex flex-col justify-center space-y-2 lg:space-y-3">
<div>
<h3 class="text-base lg:text-xl text-black font-bold line-clamp-1">
{{ $perfume['name'] }}
</h3>
<div class="flex items-center gap-2 mt-1">
<span class="text-home-primary font-bold text-sm lg:text-lg">
{{ $perfume['formatted_price'] }}
</span>
</div>
<div class="flex items-center gap-1.5 mt-0.5">
<span
class="text-[10px] lg:text-xs font-medium text-gray-400 capitalize">
{{ $perfume['formatted_sold_count'] }}
</span>
</div>
</div>
<div class="flex items-center gap-2 mt-auto">
<a href="{{ route('product.show', ['slug' => $perfume['slug']]) }}"
wire:navigate
class="flex-1 flex justify-center items-center py-2 px-3 bg-home-primary rounded-full text-xs lg:text-sm font-medium hover:bg-home-primary/90 hover:scale-[1.02] transition-all text-home-background shadow-sm">
Checkout!
</a>
<a href="{{ route('product.show', ['slug' => $perfume['slug']]) }}"
wire:navigate
class="w-8 h-8 lg:w-10 lg:h-10 bg-home-primary/10 flex justify-center items-center rounded-full hover:bg-home-primary hover:text-white transition-all flex-shrink-0 group/icon">
<svg xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"
class="size-4 lg:size-5 text-home-primary group-hover/icon:text-white transition-colors">
<path stroke-linecap="round" stroke-linejoin="round"
d="m4.5 19.5 15-15m0 0H8.25m11.25 0v11.25" />
</svg>
</a>
</div>
</div>
</article>
</div>
@endforeach
</div>
</div>
</div>
</div>
</section>
</main>

View File

@ -4,12 +4,11 @@
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center mb-20">
<div class="order-2 lg:order-1 space-y-6">
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-gray-900 leading-tight">
Temukan Parfum Favorit dengan Kualitas Terbaik
Parfum Favorit Kualitas Juara! 🏅✨
</h2>
<p class="text-base md:text-lg text-gray-600 leading-relaxed">
Kami menghadirkan parfum original berkualitas tinggi langsung dari distributor resmi. Setiap aroma
dirancang untuk bertahan lama, memberikan pengalaman wangi yang istimewa, dan memastikan setiap
tetesnya asli dan terpercaya.
Kami bawa parfum original kualitas terbaik langsung dari sumbernya! Wanginya awet, bikin kamu makin
pede dan happy seharian! 💃💖
</p>
</div>

View File

@ -8,58 +8,17 @@
<x-sections.ui.home.cta :$totalMembersCount :$formattedTotalMembersCount :$latestMembers />
</div>
@assets
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css" />
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
<style>
#brand-slider img {
user-select: none;
-webkit-user-drag: none;
filter: brightness(0) invert(1);
opacity: 0.6;
transition: all 0.3s ease;
pointer-events: auto;
.swiper-wrapper.\!ease-linear {
transition-timing-function: linear !important;
}
#brand-slider img:hover {
/* filter: brightness(0) invert(1); */
filter: none;
opacity: 1;
transform: scale(1.05);
}
.cursor-grab {
cursor: grab;
}
.cursor-grabbing {
cursor: grabbing;
}
.fade-enter {
opacity: 0;
transform: translateY(10px);
}
.fade-enter-active {
opacity: 1;
transform: translateY(0);
transition: all .4s ease;
}
@keyframes fadeUp {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.category-item {
opacity: 0;
animation: fadeUp 0.45s ease forwards;
.brand-swiper .swiper-wrapper {
transition-timing-function: linear !important;
}
</style>
@endassets
@ -67,130 +26,54 @@
@script
<script>
document.addEventListener('livewire:navigated', () => {
const track = document.getElementById('brand-track');
const originalChildren = Array.from(track.children);
const originalHTML = track.innerHTML;
if (originalChildren.length === 0) return;
const itemWidth = originalChildren[0].offsetWidth;
const gapStyle = window.getComputedStyle(track).gap;
const gap = parseFloat(gapStyle) || 0;
const singleSetWidth = (itemWidth + gap) * originalChildren.length;
let currentWidth = singleSetWidth;
while (currentWidth < window.innerWidth + singleSetWidth * 2) {
track.innerHTML += originalHTML;
currentWidth += singleSetWidth;
}
let position = 0;
const speed = 1;
let isDown = false;
let startX;
let scrollLeft;
let animationId;
track.addEventListener('mousedown', (e) => {
isDown = true;
track.classList.add('cursor-grabbing');
track.classList.remove('cursor-grab');
startX = e.pageX - track.offsetLeft;
scrollLeft = position;
e.preventDefault();
cancelAnimationFrame(animationId);
// Initialize Hero Best Selling Swiper
const bestSellingSwiper = new Swiper('.best-selling-swiper .swiper', {
loop: true,
speed: 10000,
autoplay: {
delay: 0,
disableOnInteraction: false,
},
slidesPerView: 1,
spaceBetween: 24,
breakpoints: {
1024: {
slidesPerView: 2,
}
},
allowTouchMove: true,
grabCursor: true,
freeMode: true,
});
window.addEventListener('mouseup', () => {
if (!isDown) return;
isDown = false;
track.classList.remove('cursor-grabbing');
track.classList.add('cursor-grab');
animate();
// Initialize Testimonials Swiper
const testimonialsSwiper = new Swiper('.testimonials-swiper', {
loop: true,
speed: 800,
autoplay: {
delay: 5000,
disableOnInteraction: false,
},
navigation: {
nextEl: '.testimonials-button-next',
prevEl: '.testimonials-button-prev',
},
slidesPerView: 1,
grabCursor: true,
});
window.addEventListener('mousemove', (e) => {
if (!isDown) return;
e.preventDefault();
const x = e.pageX - track.offsetLeft;
const walk = (x - startX) * 1;
position = scrollLeft + walk;
track.style.transform = `translateX(${position}px)`;
// Initialize Brand Swiper
const brandSwiper = new Swiper('.brand-swiper', {
loop: true,
speed: 3000,
autoplay: {
delay: 0,
disableOnInteraction: false,
},
slidesPerView: 'auto',
allowTouchMove: true,
grabCursor: true,
});
function animate() {
if (isDown) return;
position -= speed;
if (position <= -singleSetWidth) {
position += singleSetWidth;
} else if (position > 0) {
position -= singleSetWidth;
}
track.style.transform = `translateX(${position}px)`;
animationId = requestAnimationFrame(animate);
}
animate();
const links = document.querySelectorAll('.pagination-link');
const container = document.getElementById('category-container');
if (links.length > 0 && container) {
links.forEach(link => {
link.addEventListener('click', e => {
e.preventDefault();
const targetPage = new URL(link.href).searchParams.get('page') || '1';
// Update all dots (mobile & desktop)
links.forEach(l => {
const lPage = new URL(l.href).searchParams.get('page') || '1';
const isActive = lPage === targetPage;
const isMobile = l.closest('nav').classList.contains(
'lg:hidden');
if (isActive) {
l.classList.add('bg-home-primary');
if (isMobile) {
l.classList.add('w-8');
l.classList.remove('w-2', 'bg-gray-300');
} else {
l.classList.add('h-12');
l.classList.remove('h-6', 'bg-gray-200');
}
} else {
l.classList.remove('bg-home-primary', 'w-8', 'h-12');
if (isMobile) {
l.classList.add('bg-gray-300', 'w-2');
} else {
l.classList.add('bg-gray-200', 'h-6');
}
}
});
fetch(link.href)
.then(res => res.text())
.then(html => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const newContent = doc.querySelector('#category-container')
.innerHTML;
container.classList.add('fade-enter');
setTimeout(() => {
container.innerHTML = newContent;
container.classList.remove('fade-enter');
container.classList.add('fade-enter-active');
}, 120);
});
});
});
}
});
</script>
@endscript

View File

@ -1,12 +0,0 @@
<section class="overflow-hidden relative pt-4 bg-home-foreground" id="brand-slider" aria-label="Brand Slider">
<div class="flex gap-6 items-center cursor-grab active:cursor-grabbing" id="brand-track" style="touch-action: pan-y;">
@foreach ($brands as $brand)
<div class="flex flex-col items-center flex-shrink-0 py-5">
<img src="{{ $brand['image'] }}" alt="{{ $brand['name'] }}" class="w-12 h-12 object-contain rounded-xl">
<h3 class="mt-1 text-base font-bold text-center text-white">
{{ $brand['name'] }}
</h3>
</div>
@endforeach
</div>
</section>

View File

@ -1,4 +0,0 @@
<main class="flex flex-col lg:flex-row justify-between lg:min-h-screen w-full relative overflow-x-hidden">
@include('livewire.home.partials.jumbotron.left')
@include('livewire.home.partials.jumbotron.right')
</main>

View File

@ -1,49 +0,0 @@
<section
class="relative w-full lg:w-[35%] pt-24 lg:pt-0 py-0 px-6 lg:px-10 lg:pb-8 flex flex-col lg:min-h-screen lg:justify-between space-y-6 lg:space-y-0 z-20 bg-white">
<div class="flex flex-col justify-center flex-1 space-y-6 lg:pt-48">
@if ($totalMembersCount > 0)
<header class="flex flex-wrap items-center gap-4">
<div class="flex items-center">
@foreach ($latestMembers as $member)
<figure
class="border-2 border-home-background w-8 h-8 lg:w-10 lg:h-10 rounded-full overflow-hidden shadow-sm {{ !$loop->first ? '-ms-2' : '' }}">
<img src="https://ui-avatars.com/api/?name={{ urlencode($member->user->name ?? $member->name) }}&background=random&color=fff"
alt="{{ $member->user->name ?? $member->name }}" class="w-full h-full object-cover">
</figure>
@endforeach
</div>
<div class="border border-home-primary/30 py-1 px-3 rounded-full text-sm lg:text-lg text-home-primary">
<span class="text-home-primary font-bold">{{ $totalMembersCount }}</span>+ Customer Mempercayai Kami
</div>
</header>
@endif
<h1 class="text-home-foreground text-4xl lg:text-6xl leading-tight text-start font-bold">
Temukan<br>
<span class="italic font-serif font-light text-home-primary">
signature scent-mu
</span> hari ini
</h1>
<p class="text-lg w-[90%] lg:w-full lg:text-lg text-home-foreground/80">
Setiap parfum punya cerita. Pilih aroma yang sesuai dengan gaya hidupmu dan kesan yang ingin kamu tampilkan.
</p>
<div class="flex items-center">
<a href="{{ route('product.index') }}" wire:navigate
class="bg-home-primary text-home-background py-3 px-6 rounded-full hover:shadow-lg hover:shadow-home-primary/30 transition-all font-medium">
Jelajahi Toko
</a>
<span class="w-8 h-[2px] bg-home-primary/20"></span>
<a href="{{ route('product.index') }}" wire:navigate
class="flex items-center justify-center w-12 h-12 border border-home-primary rounded-full hover:bg-home-background transition-colors group bg-transparent">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" class="w-5 h-5 text-home-primary">
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 19.5 15-15m0 0H8.25m11.25 0v11.25" />
</svg>
</a>
</div>
</div>
@include('livewire.home.partials.jumbotron.testimonial')
</section>

View File

@ -1,58 +0,0 @@
<section
class="relative w-full lg:w-[65%] min-h-[600px] lg:min-h-screen hidden lg:flex flex-col lg:flex-row justify-end items-end pb-8 pr-0 lg:px-10 z-10">
<div
class="absolute inset-0 bg-[url('https://images.unsplash.com/photo-1591925463023-1ca6b0636780?q=80&w=1632&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D')] bg-cover bg-center bg-no-repeat">
<div
class="absolute inset-0 bg-gradient-to-t from-home-background/90 via-home-background/50 to-transparent lg:hidden">
</div>
</div>
<div class="relative w-full px-6 lg:px-0 grid grid-cols-1 lg:grid-cols-2 gap-6 mb-24 lg:mb-0">
@foreach ($bestSellingPerfumes as $perfume)
<article
class="bg-white backdrop-blur-md rounded-2xl shadow-lg hover:shadow-xl transition-shadow flex flex-row items-center p-4 h-auto lg:h-[250px] group">
<figure class="w-[40%] lg:w-auto h-32 lg:h-full overflow-hidden rounded-xl bg-gray-100">
<img src="{{ $perfume['image'] }}"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
alt="{{ $perfume['name'] }}">
</figure>
<div class="flex-1 ml-4 lg:ml-6 flex flex-col justify-center space-y-2 lg:space-y-3">
<div>
<h3 class="text-base lg:text-xl text-black font-bold line-clamp-1">{{ $perfume['name'] }}
</h3>
<div class="flex items-center gap-2 mt-1">
<span class="text-home-primary font-bold text-sm lg:text-lg">
Rp {{ number_format($perfume['sale_price'], 0, ',', '.') }}
</span>
</div>
<div class="flex items-center gap-1.5 mt-0.5">
<span class="text-[10px] lg:text-xs font-medium text-gray-400 capitalize">
{{ number_format($perfume['items_sum_quantity'], 0, ',', '.') }} Terjual
</span>
</div>
</div>
<div class="flex items-center gap-2 mt-auto">
<a href="{{ route('product.show', ['slug' => $perfume['slug']]) }}" wire:navigate
class="flex-1 justify-center items-center border border-gray-200 py-2 px-3 bg-home-primary rounded-full text-xs lg:text-sm font-medium hover:bg-home-primary/80 transition-colors text-home-background">
Belanja Sekarang
</a>
<a href="{{ route('product.show', ['slug' => $perfume['slug']]) }}" wire:navigate
class="w-8 h-8 lg:w-10 lg:h-10 bg-home-primary flex justify-center items-center rounded-full hover:bg-home-primary/80 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" class="size-4 lg:size-5 text-home-background">
<path stroke-linecap="round" stroke-linejoin="round"
d="m4.5 19.5 15-15m0 0H8.25m11.25 0v11.25" />
</svg>
</a>
</div>
</div>
</article>
@endforeach
</div>
</section>

View File

@ -1,85 +0,0 @@
<div class="mb-5 lg:mb-0">
@if (!empty($testimonials))
<article x-data="{
current: 0,
total: {{ count($testimonials) }},
timer: null,
start() {
this.timer = setInterval(() => this.next(), 6000);
},
stop() {
clearInterval(this.timer);
},
next() {
this.current = (this.current + 1) % this.total;
this.stop();
this.start();
},
prev() {
this.current = (this.current - 1 + this.total) % this.total;
this.stop();
this.start();
},
getClass(index) {
const isCurrent = this.current === index;
let isBefore = index < this.current;
if (this.current === 0 && index === this.total - 1) isBefore = true;
if (this.current === this.total - 1 && index === 0) isBefore = false;
return {
'opacity-100 z-10 translate-x-0': isCurrent,
'opacity-0 z-0': !isCurrent,
'-translate-x-full': !isCurrent && isBefore,
'translate-x-full': !isCurrent && !isBefore
};
}
}" x-init="start()"
class="relative w-full mt-auto h-[300px] lg:h-[250px] bg-home-background/40 backdrop-blur-md border border-home-secondary rounded-2xl z-30 shadow-sm overflow-hidden transition-all duration-300">
<div class="relative w-full h-full py-0 px-6">
@foreach ($testimonials as $index => $testimonial)
<div class="testimonial-slide absolute inset-0 py-0 px-6 flex flex-col justify-center transition-all duration-500 ease-in-out {{ $index === 0 ? 'opacity-100 z-10 translate-x-0' : 'opacity-0 z-0 translate-x-full' }}"
:class="getClass({{ $index }})">
<div class="flex items-center gap-4 mb-3">
<img src="https://ui-avatars.com/api/?name={{ urlencode($testimonial['user']['customer']['name'] ?? 'User') }}&background=random&color=fff"
alt="{{ $testimonial['user']['customer']['name'] ?? 'User' }}"
class="w-12 h-12 rounded-full object-cover ring-2 ring-home-background">
<div>
<h4 class="font-bold text-home-foreground text-sm lg:text-base">
{{ $testimonial['user']['customer']['name'] ?? 'User' }}</h4>
<div class="flex text-home-primary text-xs">
@for ($i = 0; $i < $testimonial['rating']; $i++)
@endfor
</div>
</div>
</div>
<p class="text-home-neutral text-base lg:text-lg italic leading-relaxed">
&quot;{{ $testimonial['content'] }}&quot;
</p>
</div>
@endforeach
</div>
<div class="absolute bottom-4 right-4 flex items-center gap-2 z-30">
<button @click="prev()"
class="w-10 h-10 rounded-full bg-home-background/80 backdrop-blur border border-home-background shadow-sm hover:bg-home-background flex justify-center items-center transition-colors group">
<svg xmlns="http://www.w3.org/2000/svg"
class="w-4 h-4 text-home-primary group-hover:scale-110 transition-transform" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</button>
<button @click="next()"
class="w-10 h-10 rounded-full bg-home-primary shadow-md hover:bg-home-primary/80 flex justify-center items-center transition-colors group">
<svg xmlns="http://www.w3.org/2000/svg"
class="w-4 h-4 text-home-background group-hover:scale-110 transition-transform" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
</article>
@endif
</div>

View File

@ -17,7 +17,7 @@ class="w-full h-full object-cover">
{{-- Search Bar --}}
<div class="max-w-xl mx-auto relative group">
<input wire:model.live.debounce.500="search" type="search" placeholder="Cari produk..."
<input wire:model.live.debounce.500="search" type="search" placeholder="Cari produk impianmu... 🎁✨"
aria-label="Cari produk"
class="w-full py-4 pl-6 pr-14 rounded-full bg-white/10 backdrop-blur-md border border-white/20 text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-home-primary focus:bg-white/20 transition-all shadow-lg text-lg">
<button
@ -38,7 +38,8 @@ class="absolute right-2 top-2 bottom-2 w-10 h-10 bg-home-primary rounded-full fl
<aside class="hidden lg:block lg:col-span-1 space-y-8">
<div class="bg-white rounded-2xl border border-home-foreground/10 p-6 shadow-sm sticky top-32">
{{-- Product type filter --}}
<h3 class="font-bold text-home-primary mb-6 border-b border-home-foreground/10 pb-4">Jenis Produk
<h3 class="font-bold text-home-primary mb-6 border-b border-home-foreground/10 pb-4">Mau Cari Apa?
🛍️
</h3>
<div class="space-y-4">
@foreach ([['value' => 'perfume', 'label' => 'Parfum'], ['value' => 'bottle', 'label' => 'Botol'], ['value' => 'arabian', 'label' => 'Arabian']] as $type)
@ -55,7 +56,8 @@ class="ml-3 text-home-foreground/70 group-hover:text-home-primary transition-col
<div class="border-t border-home-foreground/10 my-8"></div>
@if ($productType === 'perfume')
<h3 class="font-bold text-home-primary mb-6 border-b border-home-foreground/10 pb-4">Kategori
<h3 class="font-bold text-home-primary mb-6 border-b border-home-foreground/10 pb-4">Pilih
Kategori
</h3>
<div class="space-y-4">
@foreach ($availableCategories as $id => $name)
@ -72,7 +74,8 @@ class="ml-3 text-home-foreground/70 group-hover:text-home-primary transition-col
<div class="border-t border-home-foreground/10 my-8"></div>
@endif
<h3 class="font-bold text-home-primary mb-6 border-b border-home-foreground/10 pb-4">Harga</h3>
<h3 class="font-bold text-home-primary mb-6 border-b border-home-foreground/10 pb-4">Budget Kamu 💰
</h3>
<div class="space-y-4">
@foreach (['< 100k', '100k - 500k', '500k - 1jt', '> 1jt'] as $price)
<label class="flex items-center cursor-pointer group">
@ -220,7 +223,7 @@ class="w-10 h-10 rounded-lg bg-home-foreground text-white flex items-center just
<section class="mt-24 pt-16 border-t border-home-foreground/10">
<div class="flex items-center justify-between mb-12">
<h2 class="text-2xl md:text-3xl font-bold text-home-primary">Mungkin Anda Suka</h2>
<h2 class="text-2xl md:text-3xl font-bold text-home-primary">Pilihan Spesial Buat Kamu ❤️</h2>
<div class="flex gap-3">
<button id="slideLeft"
class="w-12 h-12 rounded-full border border-home-foreground/10 flex items-center justify-center text-home-foreground hover:bg-home-primary hover:text-white hover:border-home-primary transition-all shadow-sm">

View File

@ -103,7 +103,7 @@ class="text-xs font-bold text-home-primary bg-home-primary/10 px-3 py-1 rounded-
{{-- Description --}}
@if ($product->description)
<div class="space-y-3">
<h3 class="text-lg font-bold text-home-foreground">Deskripsi</h3>
<h3 class="text-lg font-bold text-home-foreground">Tentang Produk </h3>
<p class="text-home-foreground/70 leading-relaxed text-justify">
{!! $product->description !!}
</p>
@ -133,7 +133,7 @@ class="text-xs font-bold text-home-primary bg-home-primary/10 px-3 py-1 rounded-
{{-- Share Buttons --}}
<div class="flex items-center gap-4 pt-6 border-t border-home-foreground/10">
<span class="text-sm font-bold text-home-foreground">Bagikan:</span>
<span class="text-sm font-bold text-home-foreground">Bagikan ke temanmu! 📲</span>
{{-- WhatsApp --}}
<a href="https://wa.me/?text={{ urlencode($product->name . ' - ' . url()->current()) }}"
@ -174,8 +174,8 @@ class="w-10 h-10 rounded-full border border-home-foreground/10 flex items-center
@if (count($relatedProducts) > 0)
<section class="mt-24 pt-16 border-t border-home-foreground/10">
<div class="mb-12">
<h2 class="text-2xl md:text-3xl font-bold text-home-primary">Produk Terkait</h2>
<p class="text-home-foreground/60 mt-2">Produk lain yang mungkin Anda suka</p>
<h2 class="text-2xl md:text-3xl font-bold text-home-primary">Produk Lainnya 🌟</h2>
<p class="text-home-foreground/60 mt-2">Mungkin kamu juga suka ini nih! 😉</p>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">

View File

@ -11,6 +11,11 @@
</div>
<div class="mt-6">
<div class="mb-6">
<flux:input icon="magnifying-glass" placeholder="Cari item..." wire:model.live.debounce.500ms="search"
autocomplete="off" />
</div>
<div class="space-y-4">
<flux:card>
<flux:table>
@ -27,7 +32,7 @@
<flux:table.cell>{{ $item->itemable?->name }}</flux:table.cell>
<flux:table.cell>{{ $item->qty_system }}</flux:table.cell>
<flux:table.cell>
<flux:input wire:model.blur="form.outlet_stock.{{ $item->id }}"
<flux:input wire:model.live.debounce.500ms="form.outlet_stock.{{ $item->id }}"
x-mask:dynamic="$money($input, ',')" autocomplete="off" />
</flux:table.cell>
<flux:table.cell variant="strong">
@ -40,9 +45,15 @@
</flux:card>
<flux:card>
<flux:textarea label="Catatan" placeholder="Blue emotion tumpah 10ml" wire:model.blur="form.note"
<flux:textarea label="Catatan" placeholder="Blue emotion tumpah 10ml" wire:model="form.note"
autocomplete="off" rows="2" />
</flux:card>
<div class="flex justify-start">
<flux:button variant="primary" class="sm:w-auto cursor-pointer" wire:click="save">
Simpan
</flux:button>
</div>
</div>
</div>
</flux:main>