refactor(home): membuat layouting

- memperbaiki custom js
- menyimpan menu untuk header di view service dan di foreach
- menghapus file yang tidak berguna
- membuat halaman outlet
This commit is contained in:
Yoga Pangestu 2025-12-05 14:20:56 +07:00
parent 358ca2d932
commit 1aa8f3da42
16 changed files with 457 additions and 418 deletions

View File

@ -27,7 +27,7 @@ public function mount()
{
$this->brands = Brand::orderBy('sort_order')
->get()
->map(fn(Brand $brand) => [
->map(fn (Brand $brand) => [
'name' => $brand->name,
'image' => $brand->getFirstMediaUrl('image') ? Storage::url($brand->getFirstMediaUrl('image')) : asset('assets/images/logo.png'),
])
@ -35,7 +35,7 @@ public function mount()
$this->perfumeFeatures = PerfumeFeature::orderBy('sort_order')
->get()
->map(fn(PerfumeFeature $perfumeFeature) => [
->map(fn (PerfumeFeature $perfumeFeature) => [
'name' => $perfumeFeature->name,
'description' => $perfumeFeature->description,
])
@ -43,7 +43,7 @@ public function mount()
$this->faqs = Faq::orderBy('sort_order')
->get()
->map(fn(Faq $faq) => [
->map(fn (Faq $faq) => [
'question' => $faq->question,
'answer' => $faq->answer,
])
@ -51,7 +51,7 @@ public function mount()
$this->whyChooseUs = ChooseUs::orderBy('sort_order')
->get()
->map(fn(ChooseUs $chooseUs) => [
->map(fn (ChooseUs $chooseUs) => [
'name' => $chooseUs->name,
'description' => $chooseUs->description,
])
@ -64,7 +64,7 @@ public function render()
'pageTitle' => 'Beranda',
]);
}
public function goToProduct()
{
return redirect('/product');

View File

@ -0,0 +1,17 @@
<?php
namespace App\Livewire\Home;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('components.layouts.home', [
'title' => 'Outlet',
])]
class Outlet extends Component
{
public function render()
{
return view('livewire.home.outlets');
}
}

View File

@ -10,7 +10,6 @@
])]
class Index extends Component
{
public function render()
{
return view('livewire.home.product.index');

View File

@ -262,7 +262,65 @@ public function boot(): void
],
];
$view->with('sidebar', $sidebar);
$header = [
[
'title' => 'Beranda',
'type' => 'link',
'route' => 'homepage',
'match' => 'homepage',
],
[
'title' => 'Outlet',
'type' => 'link',
'route' => 'outlet',
'match' => 'outlet',
],
[
'title' => 'Produk',
'type' => 'dropdown',
'action' => 'goToProduct',
'items' => [
[
'title' => 'Parfum',
'route' => 'product.perfume.index',
'match' => 'product.perfume.*',
],
[
'title' => 'Botol',
'route' => 'product.bottle.index',
'match' => 'product.bottle.*',
],
[
'title' => 'Arabian',
'route' => 'product.arabian.index',
'match' => 'product.arabian.*',
],
],
],
[
'title' => 'Voucher',
'type' => 'link',
'route' => 'voucher.index',
'match' => 'voucher.*',
],
[
'title' => 'Artikel',
'type' => 'link',
'route' => 'article.index',
'match' => 'article.*',
],
[
'title' => 'FAQ',
'type' => 'link',
'route' => 'faq',
'match' => 'faq',
],
];
$view->with([
'sidebar' => $sidebar,
'header' => $header,
]);
});
}
}

View File

@ -1,20 +1,175 @@
<!DOCTYPE html>
<html lang="en">
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
@include('components.partials.home._meta')
@include('components.partials.home._css')
</head>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ $title }} | {{ config('app.name') }}</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
@fluxAppearance
<link rel="icon" type="image/png" href="{{ asset('assets/images/logo.png') }}">
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=inter:400,500,600&display=swap" rel="stylesheet" />
</head>
<body>
<body>
@include('components.partials.home._header')
@include("components.partials.home._header")
{{ $slot }}
@include("components.partials.home._footer")
@include('components.partials.home._js')
</body>
@persist('toast')
<flux:toast position="bottom end" class="ps-6" />
@endpersist
{{ $slot }}
@fluxScripts
<script>
document.addEventListener('livewire:navigated', () => {
const navbar = document.getElementById('navbar');
const navLinks = document.getElementById('nav-links');
const cart = document.getElementById('cart');
const login = document.getElementById('login');
const profile = document.getElementById('profile');
const cartIcon = document.getElementById('cart-icon');
const cartSvg = document.getElementById('cart-svg');
const profileIcon = document.getElementById('profile-icon');
const profileSvg = document.getElementById('profile-svg');
const mobileBtn = document.getElementById('mobile-menu-btn');
const mobileMenu = document.getElementById('mobile-menu');
const hamburgerIcon = document.getElementById('hamburger-icon');
const closeIcon = document.getElementById('close-icon');
function setIconsLight() {
cartIcon.classList.remove('bg-black', 'bg-gray-100');
cartIcon.classList.add('bg-white');
cartSvg.classList.remove('text-white', 'text-gray-800');
cartSvg.classList.add('text-black');
profileIcon.classList.remove('bg-black', 'bg-gray-100');
profileIcon.classList.add('bg-white');
profileSvg.classList.remove('text-white', 'text-gray-800');
profileSvg.classList.add('text-black');
}
function setIconsDark() {
cartIcon.classList.add('bg-black');
cartIcon.classList.remove('bg-white', 'bg-gray-100');
cartSvg.classList.add('text-white');
cartSvg.classList.remove('text-black', 'text-gray-800');
profileIcon.classList.add('bg-black');
profileIcon.classList.remove('bg-white', 'bg-gray-100');
profileSvg.classList.add('text-white');
profileSvg.classList.remove('text-black', 'text-gray-800');
}
function setIconsScrolled() {
cartIcon.classList.add('bg-gray-100');
cartIcon.classList.remove('bg-black', 'bg-white');
cartSvg.classList.add('text-gray-800');
cartSvg.classList.remove('text-white', 'text-black');
profileIcon.classList.add('bg-gray-100');
profileIcon.classList.remove('bg-black', 'bg-white');
profileSvg.classList.add('text-gray-800');
profileSvg.classList.remove('text-white', 'text-black');
}
mobileBtn.addEventListener('click', () => {
const isClosed = mobileMenu.classList.contains('hidden');
if (isClosed) {
mobileMenu.classList.remove('hidden');
void mobileMenu.offsetWidth;
mobileMenu.classList.remove('opacity-0');
mobileMenu.classList.add('opacity-100');
hamburgerIcon.classList.add('hidden');
closeIcon.classList.remove('hidden');
mobileBtn.setAttribute('aria-expanded', 'true');
navbar.classList.remove('bg-white/40', 'backdrop-blur-md', 'shadow-sm',
'border-gray-200');
navbar.classList.add('bg-transparent');
setIconsLight();
} else {
mobileMenu.classList.remove('opacity-100');
mobileMenu.classList.add('opacity-0');
setTimeout(() => {
mobileMenu.classList.add('hidden');
}, 500);
hamburgerIcon.classList.remove('hidden');
closeIcon.classList.add('hidden');
mobileBtn.setAttribute('aria-expanded', 'false');
const scrolled = window.scrollY > 10;
if (scrolled) {
navbar.classList.add('bg-white/40', 'backdrop-blur-md', 'shadow-sm',
'border-gray-200');
navbar.classList.remove('bg-transparent');
setIconsScrolled();
} else {
navbar.classList.add('bg-transparent');
navbar.classList.remove('bg-white/40', 'backdrop-blur-md', 'shadow-sm',
'border-gray-200');
setIconsDark();
}
}
});
window.addEventListener('scroll', () => {
const isMenuOpen = !mobileMenu.classList.contains('hidden');
if (isMenuOpen) return;
const scrolled = window.scrollY > 10;
if (scrolled) {
navbar.classList.add('bg-white/40', 'backdrop-blur-md', 'shadow-sm', 'border-gray-200');
navbar.classList.remove('bg-transparent');
navLinks.classList.add('bg-white', 'border-gray-300', 'text-black');
navLinks.classList.remove('bg-transparent', 'border-black', 'text-black');
cart.classList.add('border-gray-300', 'bg-white');
cart.classList.remove('border-black');
cart.querySelector('span').classList.add('text-gray-800');
login.classList.add('border-gray-300', 'text-gray-800', 'bg-white');
login.classList.remove('border-black', 'text-black');
profile.classList.add('border-gray-300');
profile.classList.remove('border-black');
setIconsScrolled();
} else {
navbar.classList.add('bg-transparent');
navbar.classList.remove('bg-white/40', 'backdrop-blur-md', 'shadow-sm', 'border-b',
'border-gray-200');
navLinks.classList.add('bg-transparent', 'border-black', 'text-black');
navLinks.classList.remove('bg-white', 'border-gray-300');
cart.classList.add('border-black');
cart.classList.remove('border-gray-300', 'bg-white');
cart.querySelector('span').classList.remove('text-gray-800');
cart.querySelector('span').classList.add('text-black');
login.classList.add('border-black', 'text-black');
login.classList.remove('border-gray-300', 'text-gray-800', 'bg-white');
profile.classList.add('border-black');
profile.classList.remove('border-gray-300');
setIconsDark();
}
});
});
</script>
</body>
</html>

View File

@ -1,6 +0,0 @@
{{-- <link rel="shortcut icon" href="{{ asset('assets/img/favicon.png') }}">
<link rel="stylesheet" href="{{ asset('assets/css/plugins.css') }}">
<link rel="stylesheet" href="{{ asset('assets/css/style.css') }}">
<link rel="stylesheet" href="{{ asset('assets/css/colors/grape.css') }}">
<link rel="preload" href="{{ asset('assets/css/fonts/space.css') }}" as="style" onload="this.rel='stylesheet'"> --}}
@vite(['resources/css/app.css', 'resources/js/app.js'])

View File

@ -26,27 +26,38 @@ class="w-10 h-8 lg:w-16 lg:h-12 transition-all duration-500">
<nav id="nav-links"
class="hidden lg:flex items-center gap-6 font-medium px-8 py-3 rounded-full transition-all duration-500 border"
aria-label="Main Navigation">
<a href="#" class="hover:opacity-80 transition-opacity">Beranda</a>
<a href="#" class="hover:opacity-80 transition-opacity">Outlet</a>
<div class="relative group">
<button wire:click="goToProduct" class="flex items-center gap-2 hover:opacity-80 transition-opacity">
<span>Produk</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="size-4 group-hover:rotate-180 transition-transform duration-300">
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg>
</button>
<div class="absolute left-0 top-full hidden group-hover:block pt-2 w-40">
<div class="bg-white/95 backdrop-blur-md shadow-lg rounded-lg py-2 border border-gray-200">
<a href="#" class="block px-4 py-2 hover:bg-gray-50 transition-colors">Parfum</a>
<a href="#" class="block px-4 py-2 hover:bg-gray-50 transition-colors">Botol</a>
<a href="#" class="block px-4 py-2 hover:bg-gray-50 transition-colors">Arabian</a>
@foreach ($header as $item)
@if ($item['type'] === 'link')
<a href="{{ route($item['route']) }}" class="hover:opacity-80 transition-opacity" wire:navigate>
{{ $item['title'] }}
</a>
@endif
@if ($item['type'] === 'dropdown')
<div class="relative group">
<button wire:click="{{ $item['action'] }}"
class="flex items-center gap-2 hover:opacity-80 transition-opacity">
<span>{{ $item['title'] }}</span>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor"
class="size-4 group-hover:rotate-180 transition-transform duration-300">
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
</svg>
</button>
<div class="absolute left-0 top-full hidden group-hover:block pt-2 w-40">
<div class="bg-white/95 backdrop-blur-md shadow-lg rounded-lg py-2 border border-gray-200">
@foreach ($item['items'] as $sub)
<a href="{{ $sub['route'] }}"
class="block px-4 py-2 hover:bg-gray-50 transition-colors">
{{ $sub['title'] }}
</a>
@endforeach
</div>
</div>
</div>
</div>
</div>
<a wire:click="goToVoucher" class="hover:opacity-80 transition-opacity cursor-pointer">Voucher</a>
<a href="#" class="hover:opacity-80 transition-opacity">Artikel</a>
<a href="#" class="hover:opacity-80 transition-opacity">FAQ</a>
@endif
@endforeach
</nav>
<section class="flex items-center lg:gap-3 z-50">
@ -110,151 +121,3 @@ class="mt-8 px-8 py-3 bg-white text-black font-bold rounded-full hover:bg-gray-1
</button>
</nav>
</header>
<script>
document.addEventListener('DOMContentLoaded', () => {
const navbar = document.getElementById('navbar');
const navLinks = document.getElementById('nav-links');
const cart = document.getElementById('cart');
const login = document.getElementById('login');
const profile = document.getElementById('profile');
const cartIcon = document.getElementById('cart-icon');
const cartSvg = document.getElementById('cart-svg');
const profileIcon = document.getElementById('profile-icon');
const profileSvg = document.getElementById('profile-svg');
const mobileBtn = document.getElementById('mobile-menu-btn');
const mobileMenu = document.getElementById('mobile-menu');
const hamburgerIcon = document.getElementById('hamburger-icon');
const closeIcon = document.getElementById('close-icon');
function setIconsLight() {
cartIcon.classList.remove('bg-black', 'bg-gray-100');
cartIcon.classList.add('bg-white');
cartSvg.classList.remove('text-white', 'text-gray-800');
cartSvg.classList.add('text-black');
profileIcon.classList.remove('bg-black', 'bg-gray-100');
profileIcon.classList.add('bg-white');
profileSvg.classList.remove('text-white', 'text-gray-800');
profileSvg.classList.add('text-black');
}
function setIconsDark() {
cartIcon.classList.add('bg-black');
cartIcon.classList.remove('bg-white', 'bg-gray-100');
cartSvg.classList.add('text-white');
cartSvg.classList.remove('text-black', 'text-gray-800');
profileIcon.classList.add('bg-black');
profileIcon.classList.remove('bg-white', 'bg-gray-100');
profileSvg.classList.add('text-white');
profileSvg.classList.remove('text-black', 'text-gray-800');
}
function setIconsScrolled() {
cartIcon.classList.add('bg-gray-100');
cartIcon.classList.remove('bg-black', 'bg-white');
cartSvg.classList.add('text-gray-800');
cartSvg.classList.remove('text-white', 'text-black');
profileIcon.classList.add('bg-gray-100');
profileIcon.classList.remove('bg-black', 'bg-white');
profileSvg.classList.add('text-gray-800');
profileSvg.classList.remove('text-white', 'text-black');
}
mobileBtn.addEventListener('click', () => {
const isClosed = mobileMenu.classList.contains('hidden');
if (isClosed) {
mobileMenu.classList.remove('hidden');
void mobileMenu.offsetWidth;
mobileMenu.classList.remove('opacity-0');
mobileMenu.classList.add('opacity-100');
hamburgerIcon.classList.add('hidden');
closeIcon.classList.remove('hidden');
mobileBtn.setAttribute('aria-expanded', 'true');
navbar.classList.remove('bg-white/40', 'backdrop-blur-md', 'shadow-sm',
'border-gray-200');
navbar.classList.add('bg-transparent');
setIconsLight();
} else {
mobileMenu.classList.remove('opacity-100');
mobileMenu.classList.add('opacity-0');
setTimeout(() => {
mobileMenu.classList.add('hidden');
}, 500);
hamburgerIcon.classList.remove('hidden');
closeIcon.classList.add('hidden');
mobileBtn.setAttribute('aria-expanded', 'false');
const scrolled = window.scrollY > 10;
if (scrolled) {
navbar.classList.add('bg-white/40', 'backdrop-blur-md', 'shadow-sm',
'border-gray-200');
navbar.classList.remove('bg-transparent');
setIconsScrolled();
} else {
navbar.classList.add('bg-transparent');
navbar.classList.remove('bg-white/40', 'backdrop-blur-md', 'shadow-sm',
'border-gray-200');
setIconsDark();
}
}
});
window.addEventListener('scroll', () => {
const isMenuOpen = !mobileMenu.classList.contains('hidden');
if (isMenuOpen) return;
const scrolled = window.scrollY > 10;
if (scrolled) {
navbar.classList.add('bg-white/40', 'backdrop-blur-md', 'shadow-sm', 'border-gray-200');
navbar.classList.remove('bg-transparent');
navLinks.classList.add('bg-white', 'border-gray-300', 'text-black');
navLinks.classList.remove('bg-transparent', 'border-black', 'text-black');
cart.classList.add('border-gray-300', 'bg-white');
cart.classList.remove('border-black');
cart.querySelector('span').classList.add('text-gray-800');
login.classList.add('border-gray-300', 'text-gray-800', 'bg-white');
login.classList.remove('border-black', 'text-black');
profile.classList.add('border-gray-300');
profile.classList.remove('border-black');
setIconsScrolled();
} else {
navbar.classList.add('bg-transparent');
navbar.classList.remove('bg-white/40', 'backdrop-blur-md', 'shadow-sm', 'border-b',
'border-gray-200');
navLinks.classList.add('bg-transparent', 'border-black', 'text-black');
navLinks.classList.remove('bg-white', 'border-gray-300');
cart.classList.add('border-black');
cart.classList.remove('border-gray-300', 'bg-white');
cart.querySelector('span').classList.remove('text-gray-800');
cart.querySelector('span').classList.add('text-black');
login.classList.add('border-black', 'text-black');
login.classList.remove('border-gray-300', 'text-gray-800', 'bg-white');
profile.classList.add('border-black');
profile.classList.remove('border-gray-300');
setIconsDark();
}
});
});
</script>

View File

@ -1,2 +0,0 @@
{{-- <script src="{{ asset('assets/js/plugins.js') }}"></script>
<script src="{{ asset('assets/js/theme.js') }}"></script> --}}

View File

@ -1,8 +0,0 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description"
content="An impressive and flawless site template that includes various UI elements and countless features, attractive ready-made blocks and rich pages, basically everything you need to create a unique and professional website.">
<meta name="keywords"
content="bootstrap 5, business, corporate, creative, gulp, marketing, minimal, modern, multipurpose, one page, responsive, saas, sass, seo, startup, html5 template, site template">
<meta name="author" content="elemis">
<title>{{ $title }} - {{ config('app.name') }}</title>

View File

@ -15,32 +15,39 @@
['id' => 4, 'q' => 'Bagaimana kebijakan pengembalian barang?', 'a' => 'Pengembalian dalam 30 hari dengan syarat barang belum digunakan, segel utuh, dan menyertakan video unboxing.'],
['id' => 5, 'q' => 'Metode pembayaran apa saja yang tersedia?', 'a' => 'Kami menerima Transfer bank (BCA, Mandiri), Kartu Kredit, E-wallet (GoPay, OVO), dan cicilan 0%.'],
['id' => 6, 'q' => 'Bagaimana cara menggunakan voucher?', 'a' => 'Masukkan kode voucher pada halaman checkout di kolom "Kode Promo", diskon akan otomatis terpotong.'],
] as $faq)
<article
] as $index => $faq)
<article x-data="{ open: null }"
class="bg-white rounded-2xl border border-home-foreground/10 overflow-hidden break-inside-avoid mb-6 shadow-sm">
<button
class="w-full px-6 py-5 flex items-center justify-between hover:bg-home-foreground/5 transition-colors accordion-trigger"
data-accordion="{{ $faq['id'] }}" aria-expanded="false"
aria-controls="faq-content-{{ $faq['id'] }}">
<button @click="open === {{ $index }} ? open = null : open = {{ $index }}"
:aria-expanded="open === {{ $index }}"
class="w-full px-6 py-5 flex items-center justify-between hover:bg-home-foreground/5 transition-colors">
<h3 class="text-lg font-bold text-home-foreground text-left pr-4">
{{ $faq['q'] }}
</h3>
<svg xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-home-foreground accordion-icon transition-transform duration-300 flex-shrink-0"
fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
class="w-6 h-6 text-home-foreground transition-transform duration-300"
:class="{ 'rotate-180': open === {{ $index }} }" fill="none" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
</button>
<div id="faq-content-{{ $faq['id'] }}"
class="accordion-content hidden px-6 py-5 border-t border-home-foreground/10"
data-content="{{ $faq['id'] }}">
<p class="text-home-foreground/70 leading-relaxed">{{ $faq['a'] }}</p>
<div x-show="open === {{ $index }}" x-collapse
x-transition:enter="transition ease-out duration-300"
x-transition:leave="transition ease-in duration-200"
class="px-6 pb-5 border-t border-home-foreground/10 text-home-foreground/70">
<p class="leading-relaxed">
{{ $faq['a'] }}
</p>
</div>
</article>
@endforeach
</div>
<div class="mt-10 bg-home-foreground/5 rounded-2xl border border-home-foreground/10 p-8 text-center">
@ -53,38 +60,3 @@ class="bg-home-foreground text-white font-bold px-8 py-3 rounded-lg hover:opacit
</div>
</section>
</div>
<script>
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll('.accordion-trigger').forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.accordion;
const content = document.querySelector(`[data-content="${id}"]`);
const icon = btn.querySelector('.accordion-icon');
document.querySelectorAll('.accordion-content').forEach(el => {
if (el.dataset.content !== id) {
el.classList.add('hidden');
const otherBtn = document.querySelector(
`[data-accordion="${el.dataset.content}"]`);
if (otherBtn) {
otherBtn.setAttribute('aria-expanded', 'false');
otherBtn.querySelector('.accordion-icon').style.transform =
'rotate(0deg)';
}
}
});
content.classList.toggle('hidden');
if (content.classList.contains('hidden')) {
icon.style.transform = 'rotate(0deg)';
btn.setAttribute('aria-expanded', 'false');
} else {
icon.style.transform = 'rotate(180deg)';
btn.setAttribute('aria-expanded', 'true');
}
});
});
});
</script>

View File

@ -6,3 +6,104 @@
@include('livewire.home.partials.benefits')
@include('livewire.home.partials.cta')
</div>
@assets
<style>
#brand-slider img {
user-select: none;
-webkit-user-drag: none;
filter: grayscale(100%);
opacity: 0.6;
transition: all 0.3s ease;
}
#brand-slider img:hover {
filter: grayscale(0%);
opacity: 1;
}
.fade-enter {
opacity: 0;
transform: translateY(10px);
}
.fade-enter-active {
opacity: 1;
transform: translateY(0);
transition: all .4s ease;
}
</style>
@endassets
@script
<script>
document.addEventListener('livewire:navigated', () => {
// brand slider
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;
function animate() {
position -= speed;
if (Math.abs(position) >=
singleSetWidth) {
position = 0;
}
track.style.transform = `translateX(${position}px)`;
requestAnimationFrame(animate);
}
animate();
// category pagination
const links = document.querySelectorAll('.pagination-link');
const container = document.getElementById('category-container');
links.forEach(link => {
link.addEventListener('click', e => {
e.preventDefault();
links.forEach(l => l.classList.remove('bg-black'));
links.forEach(l => l.classList.add('bg-gray-400'));
e.target.classList.remove('bg-gray-400');
e.target.classList.add('bg-black');
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

@ -0,0 +1,3 @@
<div class="">
Outlet
</div>

View File

@ -23,74 +23,42 @@
];
@endphp
<section class="bg-white py-20 px-6 lg:px-48 relative overflow-hidden font-sans text-home-foreground">
<div class="absolute top-24 left-[40%] w-4 h-4 bg-gray-200 rounded-full opacity-50"></div>
<div class="absolute top-32 left-[30%] w-2 h-2 bg-orange-200 rounded-full opacity-50"></div>
<div class="absolute bottom-10 right-[20%] w-2 h-2 bg-red-200 rounded-full opacity-50"></div>
<div class="grid lg:grid-cols-2 gap-12 mb-20 items-start">
<section class="bg-white py-20 px-6 lg:px-48 font-sans text-home-foreground">
<div class="grid lg:grid-cols-2 gap-12 items-start">
<figure class="w-full">
<img src="https://images.unsplash.com/photo-1523275335684-37898b6baf30?q=80"
class="w-full h-[500px] object-cover rounded-2xl shadow" alt="Parfum Premium">
class="w-full h-[500px] object-cover rounded-2xl shadow">
</figure>
<article class="space-y-4 mt-4 lg:mt-0">
<h2 class="text-4xl leading-tight mb-6 text-home-foreground">
Keunggulan Produk Kami.
</h2>
<article class="space-y-4">
<h2 class="text-4xl leading-tight mb-6">Keunggulan Produk Kami.</h2>
<p class="text-gray-700 mb-8 max-w-3xl leading-relaxed">
Setiap parfum dibuat dengan standar tinggi agar kamu mendapatkan kualitas terbaik.
Dan kalau kamu ingin pengalaman yang lebih eksklusif, ada paket membership yang bikin kamu jauh lebih
hemat dan leluasa mencoba banyak aroma.
</p>
@foreach ($accordions as $acc)
<div class="border-b border-gray-200 pb-4 accordion-item">
<button class="w-full flex justify-between items-center py-2 text-left group focus:outline-none"
onclick="toggleAccordion(this)" aria-expanded="false">
@foreach ($accordions as $index => $acc)
<div x-data="{ open: null }" class="border-b border-gray-200 pb-4 accordion-item">
<button @click="open === {{ $index }} ? open = null : open = {{ $index }}"
class="w-full flex justify-between items-center py-2 text-left group focus:outline-none"
:aria-expanded="open === {{ $index }}">
<span class="text-lg font-semibold text-home-foreground group-hover:text-gray-600 transition">
{{ $acc['title'] }}
</span>
<span
class="text-xl font-light text-gray-400 transition-transform duration-300 icon-plus">+</span>
<span class="icon-plus text-xl font-light text-gray-400 transition-transform duration-300"
:class="{ 'rotate-45': open === {{ $index }} }">
+
</span>
</button>
<div
class="max-h-0 overflow-hidden transition-all duration-500 ease-in-out content text-gray-700 text-sm leading-relaxed">
<div class="pt-2 pb-2">{{ $acc['content'] }}</div>
<div x-show="open === {{ $index }}" x-collapse
x-transition:enter="transition ease-out duration-300"
x-transition:leave="transition ease-in duration-200"
class="min-h-0 content overflow-hidden text-gray-700 text-sm leading-relaxed">
<div class="pt-2 pb-2">
{{ $acc['content'] }}
</div>
</div>
</div>
@endforeach
</article>
</div>
</section>
<script>
function toggleAccordion(button) {
const content = button.nextElementSibling;
const icon = button.querySelector('.icon-plus');
document.querySelectorAll('.accordion-item').forEach(item => {
const otherBtn = item.querySelector('button');
const otherContent = item.querySelector('.content');
const otherIcon = item.querySelector('.icon-plus');
if (otherBtn !== button) {
otherContent.style.maxHeight = null;
otherIcon.textContent = '+';
otherIcon.style.transform = 'rotate(0deg)';
otherBtn.setAttribute('aria-expanded', 'false');
}
});
if (content.style.maxHeight) {
content.style.maxHeight = null;
icon.textContent = '+';
icon.style.transform = 'rotate(0deg)';
button.setAttribute('aria-expanded', 'false');
} else {
content.style.maxHeight = content.scrollHeight + 'px';
icon.style.transform = 'rotate(45deg)';
button.setAttribute('aria-expanded', 'true');
}
}
</script>

View File

@ -95,51 +95,3 @@ class="w-[3px] h-10 rounded-full pagination-link transition duration-300 {{ $i =
</div>
</section>
<style>
.fade-enter {
opacity: 0;
transform: translateY(10px);
}
.fade-enter-active {
opacity: 1;
transform: translateY(0);
transition: all .4s ease;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', () => {
const links = document.querySelectorAll('.pagination-link');
const container = document.getElementById('category-container');
links.forEach(link => {
link.addEventListener('click', e => {
e.preventDefault();
links.forEach(l => l.classList.remove('bg-black'));
links.forEach(l => l.classList.add('bg-gray-400'));
e.target.classList.remove('bg-gray-400');
e.target.classList.add('bg-black');
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>

View File

@ -1,6 +1,5 @@
<section class="overflow-hidden relative pt-4" id="brand-slider" aria-label="Teknologi yang digunakan">
<section class="overflow-hidden relative pt-4" id="brand-slider" aria-label="Brand Slider">
<div class="flex gap-6 items-center whitespace-nowrap will-change-transform" id="brand-track">
<img src="https://images.seeklogo.com/logo-png/44/2/next-js-logo-png_seeklogo-449824.png" alt="Next.js"
class="w-32 h-32 object-contain rounded-xl flex-shrink-0">
<img src="https://logo.svgcdn.com/devicon/laravel-original-wordmark.png" alt="Laravel"
@ -17,59 +16,5 @@ class="w-32 h-32 object-contain rounded-xl flex-shrink-0">
class="w-32 h-32 object-contain rounded-xl flex-shrink-0">
<img src="https://brandlogos.net/wp-content/uploads/2023/11/tailwind_css-logo_brandlogos.net_v91ni.png"
alt="Tailwind CSS" class="w-32 h-32 object-contain rounded-xl flex-shrink-0">
</div>
</section>
<style>
#brand-slider img {
user-select: none;
-webkit-user-drag: none;
filter: grayscale(100%);
opacity: 0.6;
transition: all 0.3s ease;
}
#brand-slider img:hover {
filter: grayscale(0%);
opacity: 1;
}
</style>
<script>
document.addEventListener("DOMContentLoaded", function() {
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;
function animate() {
position -= speed;
if (Math.abs(position) >= singleSetWidth) {
position = 0;
}
track.style.transform = `translateX(${position}px)`;
requestAnimationFrame(animate);
}
animate();
});
</script>

View File

@ -1,15 +1,37 @@
<?php
use App\Livewire\Home\Index as HomeComponent;
use App\Livewire\Home\Product\Index as ProductComponent;
use App\Livewire\Home\Voucher\Index as VoucherComponent;
use App\Livewire\Home\Article\Index as ArticleComponent;
use App\Livewire\Home\Article\Index as ArticleIndex;
use App\Livewire\Home\Faq\Index as FaqComponent;
use App\Livewire\Home\Index as HomeComponent;
use App\Livewire\Home\Outlet as OutletComponent;
use App\Livewire\Home\Product\Index as ProductComponent;
use App\Livewire\Home\Voucher\Index as VoucherIndex;
use Illuminate\Support\Facades\Route;
Route::get('/', HomeComponent::class)->name('homepage');
Route::get('/product', ProductComponent::class)->name('product');
Route::get('/voucher', VoucherComponent::class)->name('voucher');
Route::get('/article', ArticleComponent::class)->name('article');
Route::get('/outlets', OutletComponent::class)->name('outlet');
Route::prefix('products')
->name('product.')
->group(function () {
Route::prefix('perfumes')
->name('perfume.')
->group(function () {
Route::get('/', ProductComponent::class)->name('index');
});
});
Route::prefix('vouchers')
->name('voucher.')
->group(function () {
Route::get('/', VoucherIndex::class)->name('index');
});
Route::prefix('articles')
->name('article.')
->group(function () {
Route::get('/', ArticleIndex::class)->name('index');
});
Route::get('/faq', FaqComponent::class)->name('faq');
// Route::get('/', HomeComponent::class)->name('produk');