feat: Implement article and product show pages, refactor home components, and update migrations

This commit is contained in:
Yoga Pangestu 2025-12-26 20:35:03 +07:00
parent b469fbff53
commit 3c1b6fc915
42 changed files with 1709 additions and 1230 deletions

View File

@ -82,3 +82,42 @@ function convertNumberToIndonesianWords($angka)
}
}
}
if (! function_exists('formatToWhatsApp')) {
function formatToWhatsApp(string $phone): ?string
{
// Remove spaces, dashes, parentheses
$cleaned = preg_replace('/[\s\-\(\)]/', '', $phone);
if ($cleaned === null || $cleaned === '') {
return null;
}
// Remove leading +
if (str_starts_with($cleaned, '+62')) {
$cleaned = substr($cleaned, 1);
}
// Replace 08 with 62
if (str_starts_with($cleaned, '08')) {
$cleaned = '62'.substr($cleaned, 1);
}
// If starts with 8, assume Indonesia
if (str_starts_with($cleaned, '8')) {
$cleaned = '62'.$cleaned;
}
// Final validation
if (! str_starts_with($cleaned, '62')) {
return null;
}
// Indonesia phone length sanity check
if (strlen($cleaned) < 11 || strlen($cleaned) > 15) {
return null;
}
return $cleaned;
}
}

View File

@ -77,8 +77,8 @@ public function columns(): array
->searchable()
->sortable(),
Column::make('Batas Per Pembali', 'limit_per_user')
->format(fn ($value) => $value ? $value : 'Tidak Terbatas')
Column::make('Batas Per Pembeli', 'limit_per_user')
->format(fn ($value) => $value ? formatCurrencyNumber($value) : 'Tidak Terbatas')
->searchable()
->sortable(),

View File

@ -30,7 +30,7 @@ class VoucherForm extends Form
public ?string $quota = null;
public ?string $limit_per_user = null;
public string $limit_per_user = '';
public string $summary = '';
@ -74,10 +74,10 @@ public function rules(): array
),
],
'quota' => ['nullable', new UnsignedInteger],
'limit_per_user' => ['nullable', new UnsignedInteger],
'limit_per_user' => ['required', new UnsignedInteger],
'summary' => ['required', 'string', 'max:200'],
'start_date' => ['required', 'date', Rule::when(
$this->start_date !== $this->voucher->start_date,
$this->voucher && $this->start_date !== $this->voucher->start_date,
'after_or_equal:today'
)],
'end_date' => ['nullable', 'date', 'after_or_equal:start_date'],
@ -181,9 +181,9 @@ private function prepareSavedData(): array
'discount_amount' => parseRupiahToInt($this->discount_amount),
'min_purchase' => parseRupiahToInt($this->min_purchase),
'max_discount' => $this->type == VoucherType::PERCENTAGE->value ? parseRupiahToInt($this->max_discount) : null,
'quota' => $this->quota,
'available_count' => empty($this->quota) ? 0 : $this->quota,
'limit_per_user' => $this->limit_per_user,
'quota' => parseRupiahToInt($this->quota),
'available_count' => empty($this->quota) ? 0 : parseRupiahToInt($this->quota),
'limit_per_user' => parseRupiahToInt($this->limit_per_user),
'summary' => $this->summary,
'start_date' => $this->start_date,
'end_date' => $this->end_date,

View File

@ -2,33 +2,107 @@
namespace App\Livewire\Home\Article;
use App\Enums\CategoryType;
use App\Models\Article;
use App\Models\Category;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithoutUrlPagination;
use Livewire\WithPagination;
#[Layout('components.layouts.home', [
'title' => 'Article',
'title' => 'Artikel',
])]
class Index extends Component
{
use WithoutUrlPagination, WithPagination;
public bool $isDetail = false;
public ?string $slug = null;
public ?string $search = null;
public function mount(?string $slug = null): void
public ?string $sort = null;
public array $categories = [];
public array $popularArticles = [];
public function mount(): void
{
if ($slug) {
$this->isDetail = true;
$this->slug = $slug;
}
$this->categories = Category::query()
->where('type', CategoryType::ARTICLE)
->orderBy('sort_order')
->get()
->toArray();
$this->popularArticles = Article::published()
->with(['author', 'categories'])
->orderBy('views', 'desc')
->take(5)
->get()
->map(function ($article) {
$article->thumbnail = $article->getFirstMediaUrl('thumbnail') ? $article->getFirstMediaUrl('thumbnail') : asset('assets/images/dark-logo.png');
return $article;
})
->toArray();
}
#[Url(as: 'category')]
public ?string $categorySlug = null;
public function setCategory(?string $slug = null): void
{
$this->categorySlug = $slug;
$this->resetPage();
}
public function updatedSearch(): void
{
$this->resetPage();
}
public function updateSort(): void
{
$this->resetPage();
}
public function render(): View
{
return view(
$this->isDetail
? 'livewire.home.article.show'
: 'livewire.home.article.index'
);
$articles = Article::with(['author', 'categories'])
->published()
->when($this->search, function (Builder $query) {
$query->where('title', 'like', '%'.$this->search.'%');
})
->when($this->sort, function (Builder $query) {
match ($this->sort) {
'latest' => $query->orderBy('created_at', 'desc'),
'Alphabetic' => $query->orderBy('title', 'asc'),
'views' => $query->orderBy('views', 'desc'),
default => $query->latest(),
};
}, fn ($query) => $query->latest())
->when($this->categorySlug, function (Builder $query) {
$query->whereHas('categories', function ($q) {
$q->where('slug', $this->categorySlug);
});
})
->latest()
->paginate(9)
->through(function (Article $article) {
$article->thumbnail = $article->getFirstMediaUrl('thumbnail') ? $article->getFirstMediaUrl('thumbnail') : asset('assets/images/dark-logo.png');
return $article;
});
return view('livewire.home.article.index', [
'pageTitle' => 'Artikel',
'pageDesc' => 'Baca tips profesional seputar perawatan wewangian, tren lifestyle, dan rekomendasi terbaik.',
'articles' => $articles,
]);
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace App\Livewire\Home\Article;
use App\Models\Article;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('components.layouts.home', [
'title' => 'Detail Artikel',
])]
class Show extends Component
{
public Article $article;
public array $latestArticles = [];
public ?Article $previousArticle = null;
public ?Article $nextArticle = null;
public function mount(Article $article): void
{
$article->load(['author', 'author.employee', 'categories']);
$this->article = $article;
$this->article->increment('views');
$article->thumbnail = $article->getFirstMediaUrl('thumbnail') ? $article->getFirstMediaUrl('thumbnail') : asset('assets/images/dark-logo.png');
$this->latestArticles = Article::where('id', '!=', $article->id)
->published()
->orderBy('published_at', 'desc')
->take(5)
->get()
->map(function ($article) {
$article->thumbnail = $article->getFirstMediaUrl('thumbnail') ? $article->getFirstMediaUrl('thumbnail') : asset('assets/images/dark-logo.png');
return $article;
})
->toArray();
$this->previousArticle = Article::where('id', '<', $article->id)
->published()
->orderBy('published_at', 'desc')
->first();
$this->nextArticle = Article::where('id', '>', $article->id)
->published()
->orderBy('published_at', 'asc')
->first();
}
public function render(): View
{
return view('livewire.home.article.show', [
'pageTitle' => $this->article->title,
]);
}
}

33
app/Livewire/Home/Faq.php Normal file
View File

@ -0,0 +1,33 @@
<?php
namespace App\Livewire\Home;
use App\Models\Faq as FaqModel;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('components.layouts.home', [
'title' => 'FAQs',
])]
class Faq extends Component
{
public ?string $search = null;
public function render(): View
{
$faqs = FaqModel::orderBy('sort_order')
->when($this->search, function ($query) {
$query->where('question', 'like', '%'.$this->search.'%')
->orWhere('answer', 'like', '%'.$this->search.'%');
})
->get()
->toArray();
return view('livewire.home.faqs', [
'pageTitle' => 'FAQs',
'pageDesc' => 'Daftar pertanyaan yang sering diajukan beserta jawabannya.',
'faqs' => $faqs,
]);
}
}

View File

@ -1,18 +0,0 @@
<?php
namespace App\Livewire\Home\Faq;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('components.layouts.home', [
'title' => 'FAQ',
])]
class Index extends Component
{
public function render(): View
{
return view('livewire.home.faq.index');
}
}

View File

@ -97,7 +97,8 @@ public function render(): View
{
return view('livewire.home.index', [
'pageTitle' => 'Beranda',
'categories' => Category::orderBy('sort_order')
'categories' => Category::perfume()
->orderBy('sort_order')
->paginate(12)
->through(function (Category $category) {
$category->image = $category->getFirstMediaUrl('image')

View File

@ -42,7 +42,7 @@ public function mount(): void
public function showDetail(int $outletId): void
{
$this->selectedOutlet = OutletModel::with(['facilities', 'openingHours', 'media'])->find($outletId);
$this->selectedOutlet = OutletModel::with(['facilities', 'openingHours'])->find($outletId);
}
public function closeModal(): void

View File

@ -2,10 +2,16 @@
namespace App\Livewire\Home\Product;
use App\Models\Bottle;
use App\Models\Category;
use App\Models\Perfume;
use App\Models\Product;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithoutUrlPagination;
use Livewire\WithPagination;
#[Layout('components.layouts.home', [
@ -13,54 +19,173 @@
])]
class Index extends Component
{
use WithPagination;
use WithoutUrlPagination, WithPagination;
public ?string $search = null;
public ?string $sort = 'latest';
#[Url(as: 'type')]
public string $productType = 'perfume';
#[Url(as: 'categories')]
public array $selectedCategories = [];
#[Url(as: 'price')]
public ?string $priceRange = null;
public array $availableCategories = [];
public array $recommendations = [];
public function mount(): void
{
$this->availableCategories = Category::perfume()
->orderBy('sort_order')
->pluck('name', 'id')
->toArray();
$this->recommendations = Perfume::with(['brand', 'categories', 'items'])
->withCount('items')
->orderBy('items_count', 'desc')
->take(5)
->take(8)
->get()
->map(function ($perfume) {
return [
'id' => $perfume->id,
'name' => $perfume->name,
'category' => $perfume->categories->first()?->name,
'price' => $perfume->sale_price,
'image' => $perfume->getFirstMediaUrl() ?: 'https://placehold.co/400x500/png?text='.urlencode($perfume->name),
'rating' => 4.5,
];
->map(function (Perfume $perfume) {
$perfume->image = $perfume->getFirstMediaUrl('image') ?: asset('assets/images/dark-logo.png');
return $perfume;
})
->toArray();
}
public function updatedSearch(): void
{
$this->resetPage();
}
public function updatedSort(): void
{
$this->resetPage();
}
public function updatedProductType(): void
{
$this->resetPage();
}
public function updatedSelectedCategories(): void
{
$this->resetPage();
}
public function updatedPriceRange(): void
{
$this->resetPage();
}
public function render(): View
{
$perfumes = Perfume::with(['brand', 'categories', 'items'])
->latest()
->paginate(9)
->through(function ($perfume) {
return [
'id' => $perfume->id,
'name' => $perfume->name,
'category' => $perfume->categories->first()?->name,
'price' => $perfume->sale_price,
'image' => $perfume->getFirstMediaUrl() ?: 'https://placehold.co/400x500/png?text='.urlencode($perfume->name),
'rating' => 4.5,
'reviews' => $perfume->items->count(),
'is_new' => $perfume->created_at->diffInDays(now()) <= 30,
'colors' => ['#78350f', '#1f2937'],
];
// Determine which model to query based on productType
$query = match ($this->productType) {
'bottle' => Bottle::query(),
'arabian' => Product::query(),
default => Perfume::with(['brand', 'categories']), // 'perfume'
};
// Load items count for badge calculation
$query->withCount([
'items as total_sales' => function ($q) {
$q->whereHas('order', function ($orderQuery) {
$orderQuery->where('created_at', '>=', now()->subMonth());
});
},
]);
// Apply search filter
$query->when($this->search, function (Builder $q) {
$q->where('name', 'like', '%'.$this->search.'%');
});
// Apply category filter (only for perfumes)
if ($this->productType === 'perfume' && $this->selectedCategories) {
$query->whereHas('categories', function (Builder $q) {
$q->whereIn('name', $this->selectedCategories);
});
}
// Apply price range filter
$query->when($this->priceRange, function (Builder $q) {
match ($this->priceRange) {
'< 100k' => $q->where('sale_price', '<', 100000),
'100k - 500k' => $q->whereBetween('sale_price', [100000, 500000]),
'500k - 1jt' => $q->whereBetween('sale_price', [500000, 1000000]),
'> 1jt' => $q->where('sale_price', '>', 1000000),
default => null,
};
});
// Apply sorting
$query->when($this->sort, function (Builder $q) {
match ($this->sort) {
'price_low' => $q->orderBy('sale_price', 'asc'),
'price_high' => $q->orderBy('sale_price', 'desc'),
'popular' => $q->withCount('items')->orderBy('items_count', 'desc'),
default => $q->latest(), // 'latest'
};
}, fn (Builder $q) => $q->latest());
// Paginate and transform results
$products = $query->paginate(9)->through(function ($product) {
$product->image = $product->getFirstMediaUrl('image') ?: asset('assets/images/dark-logo.png');
// Add categories for perfumes if not already loaded
if ($this->productType === 'perfume' && ! $product->relationLoaded('categories')) {
$product->load('categories');
}
// Determine badge with priority
$product->badge = $this->determineBadge($product);
return $product;
});
return view('livewire.home.product.index', [
'pageTitle' => 'Produk',
'perfumes' => $perfumes,
'pageDesc' => 'Temukan aroma yang mencerminkan kepribadian Anda, memadukan kemewahan parfum dan desain botol eksklusif.',
'products' => $products,
]);
}
/**
* Determine badge for product based on priority
* Priority: 1. Baru, 2. Terlaris, 3. Hot
*/
private function determineBadge($product): ?array
{
// Priority 1: Terlaris (sales >= 500 in last month)
if (isset($product->total_sales) && $product->total_sales >= 500) {
return [
'label' => 'Terlaris',
'class' => 'bg-red-500 text-white',
];
}
// Priority 2: Baru (created within last month)
if ($product->created_at && $product->created_at->isAfter(now()->subMonth())) {
return [
'label' => 'Baru',
'class' => 'bg-home-primary text-white',
];
}
// Priority 3: Hot (high views - top 20% or > 100 views)
if (isset($product->views) && $product->views > 100) {
return [
'label' => 'Hot',
'class' => 'bg-orange-500 text-white',
];
}
return null;
}
}

View File

@ -0,0 +1,89 @@
<?php
namespace App\Livewire\Home\Product;
use App\Models\Bottle;
use App\Models\Perfume;
use App\Models\Product;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('components.layouts.home', [
'title' => 'Detail Produk',
])]
class Show extends Component
{
public $product;
public $productType;
public array $relatedProducts = [];
public function mount(string $slug): void
{
// Try to find product in all tables
$this->product = Perfume::where('slug', $slug)->first();
if ($this->product) {
$this->productType = 'perfume';
$this->product->load(['brand', 'categories']);
} else {
$this->product = Bottle::where('slug', $slug)->first();
if ($this->product) {
$this->productType = 'bottle';
} else {
$this->product = Product::where('slug', $slug)->firstOrFail();
$this->productType = 'arabian';
}
}
// Increment views
$this->product->increment('views');
// Load related products
$this->loadRelatedProducts();
}
private function loadRelatedProducts(): void
{
$query = match ($this->productType) {
'perfume' => Perfume::query()
->where('id', '!=', $this->product->id)
->when($this->product->categories->isNotEmpty(), function ($q) {
$q->whereHas('categories', function ($categoryQuery) {
$categoryQuery->whereIn('categories.id', $this->product->categories->pluck('id'));
});
})
->with(['brand', 'categories']),
'bottle' => Bottle::query()->where('id', '!=', $this->product->id),
'arabian' => Product::query()->where('id', '!=', $this->product->id),
};
$this->relatedProducts = $query
->inRandomOrder()
->take(4)
->get()
->map(function ($product) {
return [
'id' => $product->id,
'slug' => $product->slug,
'name' => $product->name,
'price' => $product->sale_price,
'image' => $product->getFirstMediaUrl('image') ?: asset('assets/images/dark-logo.png'),
'category' => $this->productType === 'perfume' ? $product->categories->first()?->name : null,
];
})
->toArray();
}
public function render(): View
{
$product = $this->product;
$product->image = $product->getFirstMediaUrl('image') ?: asset('assets/images/dark-logo.png');
return view('livewire.home.product.show', [
'pageTitle' => $this->product->name,
'pageDesc' => 'Detail produk',
]);
}
}

View File

@ -5,10 +5,12 @@
use App\Models\Outlet;
use App\Models\Voucher as VoucherModel;
use App\Traits\Components\WithToast;
use Carbon\Carbon;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Layout;
use Livewire\Component;
use Livewire\WithoutUrlPagination;
use Livewire\WithPagination;
#[Layout('components.layouts.home', [
@ -16,11 +18,11 @@
])]
class Voucher extends Component
{
use WithPagination, WithToast;
use WithoutUrlPagination, WithPagination, WithToast;
public $outlets;
public array $outlets = [];
public string $search = '';
public ?string $search = null;
public ?string $outlet_id = null;
@ -32,7 +34,7 @@ class Voucher extends Component
public function mount(): void
{
$this->outlets = Outlet::query()->operational()->pluck('name', 'id');
$this->outlets = Outlet::query()->operational()->pluck('name', 'id')->toArray();
}
public function updatedSearch(): void
@ -50,13 +52,10 @@ public function updatedSort(): void
$this->resetPage();
}
public function showDetail(string $hashId): void
public function showDetail(VoucherModel $voucher): void
{
$this->selectedVoucher = VoucherModel::byHash($hashId);
$this->selectedVoucher->load('outlets');
$this->selectedVoucher->min_purchase_formatted = formatCurrencyNumber($this->selectedVoucher->min_purchase, 'Rp');
$this->selectedVoucher->end_date_formatted = formatDateLocalized($this->selectedVoucher->end_date);
$this->selectedVoucher = $voucher;
$this->selectedVoucher->load(['outlets', 'tiers']);
$this->showDetailModal = true;
}
@ -67,80 +66,141 @@ public function closeDetail(): void
$this->selectedVoucher = null;
}
public function claim(string $hashId): void
public function claim(VoucherModel $voucher): void
{
$user = auth()->user();
if (! auth()->check()) {
$this->redirect(route('login'), navigate: true);
return;
}
$voucher = VoucherModel::byHash($hashId);
$user = auth()->user();
// 1. Check if voucher exists and is active
if (
Carbon::parse($voucher->start_date)->gt(now()) ||
(
$voucher->end_date &&
Carbon::parse($voucher->end_date)->lt(now())
)
) {
$this->toast('Voucher tidak tersedia atau sudah kadaluarsa.', 'Gagal', 'danger');
// Basic checks
if (! $voucher) {
return;
}
// 2. Check Tier Requirement (before transaction)
$userTierId = $user->membership?->tier_id;
$voucher->load('tiers'); // Ensure tiers are loaded
$voucherTierIds = $voucher->tiers->pluck('id');
if (
$voucherTierIds->isNotEmpty() &&
(! $userTierId || ! $voucherTierIds->contains($userTierId))
) {
$this->toast('Voucher ini hanya tersedia untuk tier tertentu.', 'Gagal', 'danger');
return;
}
// 3. Execute claim within transaction for data consistency
$result = DB::transaction(function () use ($user, $voucher) {
// Lock voucher row to prevent race condition
$v = VoucherModel::whereKey($voucher->id)
->lockForUpdate()
->first();
if (! $v) {
return 'not_found';
}
// Check if available_count (stock) is sufficient
// available_count harus > 0 untuk bisa di-claim
if ($v->quota !== null && $v->available_count <= 0) {
return 'stock_empty';
}
// Check how many times this user has claimed this voucher
// Using SUM quantity to calculate total voucher that has been claimed
$userClaimedQuantity = DB::table('user_voucher')
->where('user_id', $user->id)
->where('voucher_id', $v->id)
->sum('quantity');
// Check if user has reached the limit
if ($v->limit_per_user !== null && $userClaimedQuantity >= $v->limit_per_user) {
return 'limit_reached';
}
// Check if user already has a record for this voucher
$existingPivot = DB::table('user_voucher')
->where('user_id', $user->id)
->where('voucher_id', $v->id)
->first();
if ($existingPivot) {
// User already has this voucher, increment the quantity
DB::table('user_voucher')
->where('user_id', $user->id)
->where('voucher_id', $v->id)
->update([
'quantity' => $existingPivot->quantity + 1,
'updated_at' => now(),
]);
} else {
// User doesn't have this voucher yet, create new record
$user->vouchers()->attach($v->id, [
'quantity' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
}
// Decrease available stock
$v->decrement('available_count');
return 'success';
});
// Handle transaction results
if ($result === 'not_found') {
$this->toast('Voucher tidak ditemukan.', 'Gagal', 'danger');
return;
}
if ($voucher->quota !== null && $voucher->available_count >= $voucher->quota) {
if ($result === 'stock_empty') {
$this->toast('Klaim gagal! Kuota voucher sudah habis.', 'Gagal', 'danger');
return;
}
// Check if user already reached limit
$claimedCount = $user->vouchers()->where('voucher_id', $voucher->id)->exists();
if ($voucher->limit_per_user !== null) {
$claimedCount = $user->vouchers()->where('voucher_id', $voucher->id)->count();
if ($claimedCount >= $voucher->limit_per_user) {
$this->toast('Anda sudah mencapai batas klaim untuk voucher ini.', 'Peringatan', 'warning');
return;
}
} elseif ($claimedCount) {
// If limit_per_user is null, assume it can only be claimed once?
// Or can it be claimed multiple times?
// Usually vouchers are one-time claim per user unless specified.
// Based on common logic, if limit_per_user is null, maybe it's unlimited?
// But usually it's 1. Let's stick to the limit_per_user check.
}
// Check if voucher is for specific tier
$userTierId = $user->membership?->tier_id;
if ($voucher->tiers()->exists() && ! $voucher->tiers()->where('tiers.id', $userTierId)->exists()) {
$this->toast('Voucher ini tidak tersedia untuk tier Anda.', 'Gagal', 'danger');
if ($result === 'limit_reached') {
$this->toast('Anda sudah mencapai batas maksimal klaim voucher ini.', 'Peringatan', 'warning');
return;
}
try {
// Claim with transaction
DB::transaction(function () use ($user, $voucher) {
// Re-check quota inside transaction
$v = VoucherModel::where('id', $voucher->id)->lockForUpdate()->first();
if ($v->quota !== null && $v->available_count >= $v->quota) {
throw new \Exception('Kuota voucher sudah habis.');
}
$user->vouchers()->attach($v->id);
$v->increment('available_count');
});
$this->toast('Voucher berhasil diklaim! Silakan cek di menu Voucher Saya.', 'Berhasil', 'success');
} catch (\Throwable $e) {
$this->toast($e->getMessage(), 'Gagal', 'danger');
if ($result === 'success') {
$this->toast(
'Voucher berhasil diklaim! Silakan cek di menu Voucher di halaman dasbor.',
'Berhasil',
'success'
);
}
}
public function render(): View
{
$vouchers = VoucherModel::where(function ($query) {
$query->where('name', 'like', '%'.$this->search.'%')
->orWhere('code', 'like', '%'.$this->search.'%');
})
$vouchers = VoucherModel::query()
->with(['tiers', 'outlets'])
->active()
->show()
->where(function ($query) {
$query->where('name', 'like', '%'.$this->search.'%')
->orWhere('code', 'like', '%'.$this->search.'%');
})
->when($this->outlet_id, function ($query) {
$query->whereHas('outlets', function ($q) {
$q->where('outlets.id', $this->outlet_id);
@ -150,21 +210,33 @@ public function render(): View
match ($this->sort) {
'latest' => $query->orderBy('created_at', 'desc'),
'biggest' => $query->orderBy('discount_amount', 'desc'),
'ending-soon' => $query->orderByRaw('end_date IS NULL')
->orderBy('end_date', 'asc'),
'ending-soon' => $query->orderByRaw('end_date IS NULL')->orderBy('end_date', 'asc'),
default => $query->latest(),
};
})
->active()
->latest()
->paginate(3)
->through(function (VoucherModel $voucher) {
$voucher->min_purchase_formatted = formatCurrencyNumber($voucher->min_purchase, 'Rp');
$voucher->end_date = formatDateLocalized($voucher->end_date);
}, fn ($query) => $query->latest())
->paginate(6);
return $voucher;
});
$user = auth()->user();
$userTierId = $user?->membership?->tier_id;
$voucherIds = $vouchers->pluck('id');
if ($this->selectedVoucher) {
$voucherIds->push($this->selectedVoucher->id);
}
$userClaimedCounts = $user
? DB::table('user_voucher')
->where('user_id', $user->id)
->whereIn('voucher_id', $voucherIds->unique())
->select('voucher_id', DB::raw('SUM(quantity) as total_quantity'))
->groupBy('voucher_id')
->pluck('total_quantity', 'voucher_id')
->toArray()
: [];
return view('livewire.home.vouchers', [
'pageTitle' => 'Voucher',
'pageDesc' => 'Nikmati berbagai voucher menarik untuk belanja lebih hemat dan menyenangkan.',
'vouchers' => $vouchers,
]);
}

View File

@ -3,6 +3,8 @@
namespace App\Models;
use App\Enums\ArticleStatus;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -29,6 +31,12 @@ protected function casts(): array
];
}
#[Scope]
protected function published(Builder $query): void
{
$query->where('status', ArticleStatus::PUBLISHED);
}
public function getSlugOptions(): SlugOptions
{
return SlugOptions::create()

View File

@ -24,6 +24,7 @@ protected function casts(): array
return [
'cost_price' => 'int',
'sale_price' => 'int',
'views' => 'int',
];
}

View File

@ -27,6 +27,7 @@ protected function casts(): array
'concentration' => Concentration::class,
'cost_price' => 'int',
'sale_price' => 'int',
'views' => 'int',
];
}

View File

@ -24,6 +24,7 @@ protected function casts(): array
return [
'cost_price' => 'int',
'sale_price' => 'int',
'views' => 'int',
];
}

View File

@ -64,6 +64,12 @@ public function forTier(Builder $query, ?int $tierId): void
});
}
#[Scope]
public function show(Builder $query): void
{
$query->where('is_show', IsShow::SHOW->value);
}
public function outlets(): BelongsToMany
{
return $this->belongsToMany(Outlet::class);

View File

@ -5,6 +5,7 @@
use App\Enums\PriceRequestStatus;
use App\Models\PriceRequest;
use App\Settings\ContactSettings;
use App\Settings\GeneralSettings;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
@ -327,7 +328,8 @@ public function boot(): void
$view->with([
'sidebar' => $sidebar,
'header' => $header,
'setting' => app(ContactSettings::class),
'contactSettings' => app(ContactSettings::class),
'generalSettings' => app(GeneralSettings::class),
]);
});
}

View File

@ -24,8 +24,8 @@ public function up(): void
$table->unsignedInteger('min_purchase')->nullable();
$table->unsignedInteger('max_discount')->nullable();
$table->unsignedInteger('quota')->nullable();
$table->unsignedInteger('available_count')->default(0);
$table->unsignedInteger('limit_per_user')->nullable();
$table->unsignedInteger('available_count')->nullable();
$table->unsignedInteger('limit_per_user')->default(1);
$table->date('start_date');
$table->date('end_date')->nullable();
$table->enum('is_show', IsShow::values())->default(IsShow::SHOW)->comment(IsShow::comment());

View File

@ -17,7 +17,7 @@ public function up(): void
$table->string('name', 20);
$table->string('slug', 30);
$table->text('description')->nullable();
$table->integer('type')->default(CategoryType::PERFUME->value)->comment(CategoryType::comment());
$table->enum('type', CategoryType::values())->default(CategoryType::PERFUME)->comment(CategoryType::comment());
$table->unsignedInteger('sort_order');
$table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();

View File

@ -25,6 +25,7 @@ public function up(): void
$table->string('middle_notes')->nullable();
$table->string('top_notes')->nullable();
$table->text('description')->nullable();
$table->unsignedInteger('views')->default(0);
$table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();
$table->softDeletes();

View File

@ -19,6 +19,7 @@ public function up(): void
$table->unsignedInteger('cost_price')->default(0);
$table->unsignedInteger('sale_price')->default(0);
$table->text('description')->nullable();
$table->unsignedInteger('views')->default(0);
$table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();
$table->softDeletes();

View File

@ -19,6 +19,7 @@ public function up(): void
$table->unsignedInteger('cost_price')->default(0);
$table->unsignedInteger('sale_price')->default(0);
$table->text('description')->nullable();
$table->unsignedInteger('views')->default(0);
$table->timestamp('created_at')->useCurrent();
$table->timestamp('updated_at')->nullable()->useCurrentOnUpdate();
$table->softDeletes();

View File

@ -0,0 +1,130 @@
# Product Badge System
## Overview
Sistem badge produk yang menampilkan label dinamis pada setiap produk berdasarkan kondisi tertentu. Hanya 1 badge yang ditampilkan per produk sesuai dengan prioritas yang telah ditentukan.
## Prioritas Badge
Badge ditampilkan berdasarkan prioritas berikut (dari tertinggi ke terendah):
### 1. **Terlaris** (Prioritas Tertinggi)
- **Kondisi**: Produk memiliki penjualan ≥ 500 unit dalam 1 bulan terakhir
- **Warna**: Merah (`bg-red-500 text-white`)
- **Label**: "Terlaris"
### 2. **Baru**
- **Kondisi**: Produk dibuat dalam 1 bulan terakhir
- **Warna**: Primary (`bg-home-primary text-white`)
- **Label**: "Baru"
### 3. **Hot** (Prioritas Terendah)
- **Kondisi**: Produk memiliki views > 100
- **Warna**: Oranye (`bg-orange-500 text-white`)
- **Label**: "Hot"
## Implementasi
### Database Migration
Kolom `view` telah ditambahkan ke tabel:
- `perfumes`
- `bottles`
- `products`
File migration: `2025_12_26_001500_add_view_column_to_product_tables.php`
### Backend Logic
File: `app/Livewire/Home/Product/Index.php`
#### Method `render()`
- Menambahkan `withCount` untuk menghitung total penjualan dalam 1 bulan terakhir
- Memanggil method `determineBadge()` untuk setiap produk
#### Method `determineBadge()`
```php
private function determineBadge($product): ?array
{
// Priority 1: Baru (created within last month)
if ($product->created_at && $product->created_at->isAfter(now()->subMonth())) {
return [
'label' => 'Baru',
'class' => 'bg-blue-500 text-white',
];
}
// Priority 2: Terlaris (sales >= 500 in last month)
if (isset($product->total_sales) && $product->total_sales >= 500) {
return [
'label' => 'Terlaris',
'class' => 'bg-red-500 text-white',
];
}
// Priority 3: Hot (high views - > 100 views)
if (isset($product->view) && $product->view > 100) {
return [
'label' => 'Hot',
'class' => 'bg-orange-500 text-white',
];
}
return null;
}
```
### Frontend Display
File: `resources/views/livewire/home/product/index.blade.php`
Badge ditampilkan secara kondisional:
```blade
@if (isset($product->badge) && $product->badge)
<span class="absolute top-4 left-4 {{ $product->badge['class'] }} text-[10px] uppercase tracking-wider font-bold px-3 py-1 rounded-full z-10 shadow-md">
{{ $product->badge['label'] }}
</span>
@endif
```
## Kustomisasi
### Mengubah Threshold
Anda dapat mengubah nilai threshold di method `determineBadge()`:
- **Baru**: Ubah `now()->subMonth()` menjadi periode lain (misal: `now()->subWeeks(2)`)
- **Terlaris**: Ubah `>= 500` menjadi nilai lain
- **Hot**: Ubah `> 100` menjadi nilai lain
### Menambah Badge Baru
Tambahkan kondisi baru di method `determineBadge()` dengan memperhatikan prioritas:
```php
// Priority 4: Diskon (contoh)
if (isset($product->discount) && $product->discount > 0) {
return [
'label' => 'Diskon',
'class' => 'bg-green-500 text-white',
];
}
```
### Mengubah Warna Badge
Ubah class Tailwind di array return:
```php
return [
'label' => 'Baru',
'class' => 'bg-purple-500 text-white', // Ganti warna di sini
];
```
## Tracking View Count
Untuk meningkatkan view count produk, tambahkan logika di halaman detail produk:
```php
// Di controller/livewire detail produk
$product->increment('view');
```
## Notes
- Badge hanya ditampilkan jika kondisi terpenuhi
- Jika tidak ada kondisi yang terpenuhi, tidak ada badge yang ditampilkan
- Sistem menggunakan prioritas, jadi jika produk memenuhi multiple kondisi, hanya badge dengan prioritas tertinggi yang ditampilkan

File diff suppressed because one or more lines are too long

View File

@ -1,8 +1,8 @@
<div class="flex justify-center flex-col">
<dotlottie-wc src="https://lottie.host/4282c234-6d0f-4a8d-ad04-3d82f2783814/FuhTCVgPGi.lottie" autoplay loop
<dotlottie-wc src="{{ asset('assets/animations/boxing-cat.json') }}" autoplay loop
style="width: 200px; height: 200px; margin: 0 auto;">
</dotlottie-wc>
<flux:text class="block text-center italic mt-5">
<p class="block text-center italic mt-5">
{!! isset($message) ? $message : 'Yahhh, sepertinya belum ada data yang tersedia.' !!}
</flux:text>
</p>
</div>

View File

@ -20,12 +20,11 @@
@endpersist
{{ $slot }}
@include('components.sections.ui.home._footer')
@fluxScripts
</body>
</html>

View File

@ -9,9 +9,8 @@
class="w-10 h-10 object-contain brightness-0 invert">
<h2 class="text-2xl font-bold tracking-wide text-white">Yadi Parfum</h2>
</div>
<p class="text-gray-400 text-sm leading-relaxed">
Menyediakan berbagai macam varian parfum berkualitas tinggi dengan wangi yang tahan lama dan harga
terjangkau.
<p class="text-gray-400 text-sm leading-relaxed text-justify">
{{ $generalSettings->site_description }}
</p>
<div class="flex items-center gap-4 mt-2">
<a href="#"
@ -76,7 +75,7 @@ class="hover:text-home-secondary transition-colors duration-300 flex items-cente
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg></div>
<span class="leading-relaxed">{{ $setting->address }}</span>
<span class="leading-relaxed">{{ $contactSettings->address }}</span>
</div>
<div class="flex items-center gap-4">
<div><svg class="w-5 h-5 text-home-primary" fill="none" stroke="currentColor"
@ -85,7 +84,7 @@ class="hover:text-home-secondary transition-colors duration-300 flex items-cente
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z">
</path>
</svg></div>
<span>{{ $setting->email }}</span>
<span>{{ $contactSettings->email }}</span>
</div>
<div class="flex items-center gap-4">
<div><svg class="w-5 h-5 text-home-primary" fill="none" stroke="currentColor"
@ -94,7 +93,7 @@ class="hover:text-home-secondary transition-colors duration-300 flex items-cente
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z">
</path>
</svg></div>
<span>{{ $setting->phone_number }}</span>
<span>{{ $contactSettings->phone_number }}</span>
</div>
</div>
</div>

View File

@ -47,7 +47,7 @@ class="hidden lg:flex items-center gap-6 font-medium px-8 py-3 rounded-full tran
aria-label="Main Navigation">
@foreach ($header as $item)
<a href="{{ route($item['route']) }}"
class="{{ request()->routeIs($item['route']) ? 'font-bold text-home-primary' : 'hover:opacity-80' }} transition-opacity"
class="{{ request()->routeIs($item['match']) ? 'font-bold text-home-primary' : 'hover:opacity-80' }} transition-opacity"
wire:navigate>
{{ $item['title'] }}
</a>
@ -118,34 +118,11 @@ class="fixed inset-0 h-screen w-screen bg-home-foreground/80 backdrop-blur-md z-
$inactiveClass = 'text-white hover:text-gray-300 hover:opacity-80';
@endphp
<a href="{{ route('homepage') }}" wire:navigate @click="isOpen = false"
class="{{ $baseClass }} {{ request()->routeIs('homepage') ? $activeClass : $inactiveClass }}">
Beranda
</a>
<a href="{{ route('outlet') }}" wire:navigate @click="isOpen = false"
class="{{ $baseClass }} {{ request()->routeIs('outlet') ? $activeClass : $inactiveClass }}">
Outlet
</a>
<a href="{{ route('product.index') }}" wire:navigate @click="isOpen = false"
class="{{ $baseClass }} {{ request()->routeIs('product.*') ? $activeClass : $inactiveClass }}">
Produk
</a>
<a href="{{ route('voucher') }}" wire:navigate @click="isOpen = false"
class="{{ $baseClass }} {{ request()->routeIs('voucher') ? $activeClass : $inactiveClass }}">
Voucher
</a>
<a href="{{ route('article.index') }}" wire:navigate @click="isOpen = false"
class="{{ $baseClass }} {{ request()->routeIs('article.*') ? $activeClass : $inactiveClass }}">
Artikel
</a>
<a href="{{ route('faq') }}" wire:navigate @click="isOpen = false"
class="{{ $baseClass }} {{ request()->routeIs('faq') ? $activeClass : $inactiveClass }}">
FAQ
</a>
@foreach ($header as $item)
<a href="{{ route($item['route']) }}" wire:navigate @click="isOpen = false"
class="{{ $baseClass }} {{ request()->routeIs($item['match']) ? $activeClass : $inactiveClass }}">
{{ $item['title'] }}
</a>
@endforeach
</nav>
</header>

View File

@ -10,14 +10,15 @@ class="w-full h-full object-cover">
{{-- Content --}}
<div class="relative z-10 container mx-auto px-6 text-center">
<h1 class="text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-6">Artikel Terbaru</h1>
<h1 class="text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-6">{{ $pageTitle }}</h1>
<p class="text-white/80 text-lg md:text-xl max-w-2xl mx-auto mb-10">
Baca tips profesional seputar perawatan wewangian, tren lifestyle, dan rekomendasi terbaik.
{{ $pageDesc }}
</p>
{{-- Search Bar --}}
<div class="max-w-xl mx-auto relative group">
<input type="search" placeholder="Cari artikel menarik..." aria-label="Cari artikel"
<input wire:model.live.debounce.500="search" type="search" placeholder="Cari artikel..."
aria-label="Cari artikel"
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
class="absolute right-2 top-2 bottom-2 w-10 h-10 bg-home-primary rounded-full flex items-center justify-center text-white hover:bg-white hover:text-home-primary transition-all duration-300">
@ -30,20 +31,17 @@ class="absolute right-2 top-2 bottom-2 w-10 h-10 bg-home-primary rounded-full fl
</div>
</section>
<main class="px-6 lg:px-32 xl:px-48 py-24 bg-white min-h-screen font-sans">k
<main class="px-6 lg:px-32 xl:px-48 py-24 bg-white text-black font-sans">
<div class="grid grid-cols-1 lg:grid-cols-4 gap-12">
<section class="lg:col-span-3">
{{-- Toolbar (Sort Only) --}}
<div class="flex justify-start mb-10">
<div class="relative w-full md:w-auto">
<select aria-label="Urutkan artikel"
<select wire:model.live="sort" aria-label="Urutkan Artikel"
class="appearance-none px-6 py-3 pr-10 rounded-xl border border-home-foreground/10 bg-white text-home-foreground font-medium focus:outline-none focus:ring-2 focus:ring-home-primary focus:border-transparent transition cursor-pointer min-w-[160px] shadow-sm">
<option value="">Urutkan</option>
<option value="latest">Terbaru</option>
<option value="name">Nama</option>
<option value="Alphabetic">Alfabet</option>
<option value="views">Terpopuler</option>
</select>
<svg class="absolute right-4 top-4 w-4 h-4 text-home-foreground/50 pointer-events-none"
@ -55,208 +53,92 @@ class="appearance-none px-6 py-3 pr-10 rounded-xl border border-home-foreground/
</div>
<div class="space-y-8">
<article
class="bg-white rounded-2xl border border-home-foreground/10 overflow-hidden hover:shadow-xl hover:shadow-home-primary/5 hover:border-home-primary/20 transition-all duration-300 group">
<div class="flex flex-col sm:flex-row h-full">
<div class="sm:w-2/5 h-64 sm:h-auto overflow-hidden relative">
<img src="https://placehold.co/600x400/png?text=Tips+Parfum" alt="Tips Memilih Parfum"
loading="lazy"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500">
<div
class="absolute inset-0 bg-home-primary/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
</div>
</div>
<div class="p-8 sm:w-3/5 flex flex-col justify-between">
<div>
<div class="flex items-center gap-3 mb-4">
<span
class="text-xs font-bold text-home-primary bg-home-primary/10 px-3 py-1 rounded-full border border-home-primary/10 uppercase tracking-wider">Perawatan</span>
<span class="text-xs text-home-foreground/50 flex items-center gap-1">
<svg class="w-3 h-3" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
5 min read
</span>
@forelse($articles as $article)
<article
class="bg-white rounded-2xl border border-home-foreground/10 overflow-hidden hover:shadow-xl hover:shadow-home-primary/5 hover:border-home-primary/20 transition-all duration-300 group">
<div class="flex flex-col sm:flex-row h-full">
<div class="sm:w-2/5 h-64 sm:h-auto overflow-hidden relative">
<img src="{{ $article->thumbnail }}" alt="{{ $article->title }}" loading="lazy"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500">
<div
class="absolute inset-0 bg-home-primary/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
</div>
<h3
class="text-2xl font-bold text-home-foreground mb-3 group-hover:text-home-primary transition-colors line-clamp-2 leading-tight">
Tips Memilih Parfum yang Tepat Sesuai Kepribadian Anda
</h3>
<p class="text-home-foreground/60 text-sm mb-4 line-clamp-3 leading-relaxed">
Memilih parfum yang tepat bukan hanya tentang aroma yang disukai, tetapi juga
tentang menemukan signature scent yang mencerminkan karakter...
</p>
</div>
<div class="p-8 sm:w-3/5 flex flex-col justify-between">
<div>
<div class="flex items-center gap-3 mb-4 flex-wrap">
@foreach ($article->categories->take(3) as $category)
<span
class="text-xs font-bold text-home-primary bg-home-primary/10 px-3 py-1 rounded-full border border-home-primary/10 uppercase tracking-wider">
{{ $category->name }}
</span>
@endforeach
<div class="flex items-center justify-between pt-6 border-t border-home-foreground/5">
<div class="flex items-center gap-3">
<img src="https://placehold.co/100x100/png?text=User" alt="Author"
class="w-9 h-9 rounded-full ring-2 ring-white shadow-sm">
<div>
<p class="text-sm font-bold text-home-foreground">Siti Rahma</p>
<p class="text-xs text-home-foreground/50">5 Nov 2024</p>
@if ($article->categories->count() > 3)
<span
class="text-xs font-bold text-home-foreground/50 px-3 py-1 rounded-full border border-home-foreground/10">
+{{ $article->categories->count() - 3 }}
</span>
@endif
<span class="text-xs text-home-foreground/50 flex items-center gap-1">
<svg class="w-3 h-3" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
{{ ceil(str_word_count(strip_tags($article->content)) / 200) }} menit
baca
</span>
</div>
<h3
class="text-2xl font-bold text-home-foreground mb-3 group-hover:text-home-primary transition-colors line-clamp-2 leading-tight">
<a href="{{ route('article.show', $article->slug) }}" wire:navigate>
{{ $article->title }}
</a>
</h3>
<div class="text-base text-home-foreground/60 mb-4 line-clamp-6 leading-relaxed"
style="text-align: justify !important;">
{!! $article->excerpt !!}
</div>
</div>
<div
class="w-10 h-10 rounded-full bg-home-foreground/5 flex items-center justify-center group-hover:bg-home-primary group-hover:text-white transition-all duration-300">
<svg class="w-5 h-5 transition-transform group-hover:translate-x-0.5"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M14 5l7 7m0 0l-7 7m7-7H3"></path>
</svg>
</div>
</div>
</div>
</div>
</article>
<article
class="bg-white rounded-2xl border border-home-foreground/10 overflow-hidden hover:shadow-xl hover:shadow-home-primary/5 hover:border-home-primary/20 transition-all duration-300 group">
<div class="flex flex-col sm:flex-row h-full">
<div class="sm:w-2/5 h-64 sm:h-auto overflow-hidden relative">
<img src="https://placehold.co/600x400/png?text=Menyimpan+Parfum"
alt="Cara Menyimpan Parfum" loading="lazy"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500">
</div>
<div class="p-8 sm:w-3/5 flex flex-col justify-between">
<div>
<div class="flex items-center gap-3 mb-4">
<span
class="text-xs font-bold text-home-primary bg-home-primary/10 px-3 py-1 rounded-full border border-home-primary/10 uppercase tracking-wider">Lifestyle</span>
<span class="text-xs text-home-foreground/50 flex items-center gap-1">
<svg class="w-3 h-3" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
8 min read
</span>
</div>
<h3
class="text-2xl font-bold text-home-foreground mb-3 group-hover:text-home-primary transition-colors line-clamp-2 leading-tight">
Panduan Lengkap Cara Menyimpan Parfum agar Tahan Lama
</h3>
<p class="text-home-foreground/60 text-sm mb-4 line-clamp-3 leading-relaxed">
Parfum adalah investasi yang berharga. Penyimpanan yang tepat adalah kunci untuk
menjaga kualitas aroma agar tetap orisinil...
</p>
</div>
<div class="flex items-center justify-between pt-6 border-t border-home-foreground/5">
<div class="flex items-center gap-3">
<img src="https://placehold.co/100x100/png?text=User" alt="Author"
class="w-9 h-9 rounded-full ring-2 ring-white shadow-sm">
<div>
<p class="text-sm font-bold text-home-foreground">Ahmad Rizki</p>
<p class="text-xs text-home-foreground/50">3 Nov 2024</p>
class="flex items-center justify-between pt-6 border-t border-home-foreground/5">
<div class="flex items-center gap-3">
<img src="https://ui-avatars.com/api/?name={{ urlencode($article->author?->employee?->full_name) }}&background=random&color=fff"
alt="{{ $article->author?->employee?->full_name }}"
class="w-9 h-9 rounded-full ring-2 ring-white shadow-sm">
<div>
<p class="text-sm font-bold text-home-foreground">
{{ $article->author?->employee?->full_name }}</p>
<p class="text-xs text-home-foreground/50">
{{ formatDateLocalized($article->published_at) }}
</p>
</div>
</div>
</div>
<div
class="w-10 h-10 rounded-full bg-home-foreground/5 flex items-center justify-center group-hover:bg-home-primary group-hover:text-white transition-all duration-300">
<svg class="w-5 h-5 transition-transform group-hover:translate-x-0.5"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M14 5l7 7m0 0l-7 7m7-7H3"></path>
</svg>
</div>
</div>
</div>
</div>
</article>
<article
class="bg-white rounded-2xl border border-home-foreground/10 overflow-hidden hover:shadow-xl hover:shadow-home-primary/5 hover:border-home-primary/20 transition-all duration-300 group">
<div class="flex flex-col sm:flex-row h-full">
<div class="sm:w-2/5 h-64 sm:h-auto overflow-hidden relative">
<img src="https://placehold.co/600x400/png?text=Parfum+Unisex" alt="Parfum Unisex"
loading="lazy"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500">
</div>
<div class="p-8 sm:w-3/5 flex flex-col justify-between">
<div>
<div class="flex items-center gap-3 mb-4">
<span
class="text-xs font-bold text-home-primary bg-home-primary/10 px-3 py-1 rounded-full border border-home-primary/10 uppercase tracking-wider">Tren</span>
<span class="text-xs text-home-foreground/50 flex items-center gap-1">
<svg class="w-3 h-3" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<a href="{{ route('article.show', $article->slug) }}" wire:navigate
class="w-10 h-10 rounded-full bg-home-foreground/5 flex items-center justify-center group-hover:bg-home-primary group-hover:text-white transition-all duration-300">
<svg class="w-5 h-5 transition-transform group-hover:translate-x-0.5"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
d="M14 5l7 7m0 0l-7 7m7-7H3"></path>
</svg>
7 min read
</span>
</div>
<h3
class="text-2xl font-bold text-home-foreground mb-3 group-hover:text-home-primary transition-colors line-clamp-2 leading-tight">
Parfum Unisex Terbaik 2024: Pilihan Untuk Semua
</h3>
<p class="text-home-foreground/60 text-sm mb-4 line-clamp-3 leading-relaxed">
Parfum unisex semakin populer karena fleksibilitas dan keunikannya. Kami
mengumpulkan rekomendasi terbaik tahun ini...
</p>
</div>
<div class="flex items-center justify-between pt-6 border-t border-home-foreground/5">
<div class="flex items-center gap-3">
<img src="https://placehold.co/100x100/png?text=User" alt="Author"
class="w-9 h-9 rounded-full ring-2 ring-white shadow-sm">
<div>
<p class="text-sm font-bold text-home-foreground">Budi Santoso</p>
<p class="text-xs text-home-foreground/50">28 Oct 2024</p>
</div>
</div>
<div
class="w-10 h-10 rounded-full bg-home-foreground/5 flex items-center justify-center group-hover:bg-home-primary group-hover:text-white transition-all duration-300">
<svg class="w-5 h-5 transition-transform group-hover:translate-x-0.5"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M14 5l7 7m0 0l-7 7m7-7H3"></path>
</svg>
</a>
</div>
</div>
</div>
</div>
</article>
</article>
@empty
@include('components.animations.lottie.not-found')
@endforelse
</div>
<nav class="flex items-center justify-center mt-16" aria-label="Pagination">
<div
class="flex items-center gap-3 bg-white p-2 rounded-full shadow-sm border border-home-foreground/5">
<button
class="w-10 h-10 flex items-center justify-center rounded-full border border-home-foreground/10 text-home-foreground/30 cursor-not-allowed hover:bg-gray-50 transition-colors"
disabled>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
clip-rule="evenodd"></path>
</svg>
</button>
@for ($page = 1; $page <= 3; $page++)
@if ($page == 1)
<button
class="w-10 h-10 flex items-center justify-center rounded-full bg-home-primary text-white font-bold shadow-md shadow-home-primary/20"
aria-current="page">{{ $page }}</button>
@else
<button
class="w-10 h-10 flex items-center justify-center rounded-full text-home-foreground hover:bg-home-primary/10 hover:text-home-primary transition-colors font-medium">{{ $page }}</button>
@endif
@endfor
<button
class="w-10 h-10 flex items-center justify-center rounded-full border border-home-foreground/10 text-home-foreground hover:bg-home-primary/10 hover:text-home-primary hover:border-home-primary transition-all">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"></path>
</svg>
</button>
</div>
</nav>
<div class="mt-16">
{{ $articles->links('livewire::custom-pagination') }}
</div>
</section>
<aside class="lg:col-span-1">
@ -266,30 +148,20 @@ class="w-10 h-10 flex items-center justify-center rounded-full border border-hom
<h3 class="text-lg font-bold text-home-primary mb-6 border-b border-home-foreground/5 pb-4">
Kategori</h3>
<div class="space-y-1">
<button
class="w-full text-left px-4 py-3 rounded-xl text-home-foreground/70 hover:bg-home-primary/5 hover:text-home-primary transition-all text-sm font-medium flex justify-between items-center group">
Perawatan Parfum
<button wire:click="setCategory(null)"
class="w-full text-left px-4 py-3 rounded-xl transition-all text-sm font-medium flex justify-between items-center group {{ !$categorySlug ? 'bg-home-primary/10 text-home-primary' : 'text-home-foreground/70 hover:bg-home-primary/5 hover:text-home-primary' }}">
Semua Artikel
<span
class="text-home-primary opacity-0 -translate-x-2 group-hover:translate-x-0 group-hover:opacity-100 transition-all duration-300"></span>
</button>
<button
class="w-full text-left px-4 py-3 rounded-xl text-home-foreground/70 hover:bg-home-primary/5 hover:text-home-primary transition-all text-sm font-medium flex justify-between items-center group">
Lifestyle
<span
class="text-home-primary opacity-0 -translate-x-2 group-hover:translate-x-0 group-hover:opacity-100 transition-all duration-300"></span>
</button>
<button
class="w-full text-left px-4 py-3 rounded-xl text-home-foreground/70 hover:bg-home-primary/5 hover:text-home-primary transition-all text-sm font-medium flex justify-between items-center group">
Tren Terkini
<span
class="text-home-primary opacity-0 -translate-x-2 group-hover:translate-x-0 group-hover:opacity-100 transition-all duration-300"></span>
</button>
<button
class="w-full text-left px-4 py-3 rounded-xl text-home-foreground/70 hover:bg-home-primary/5 hover:text-home-primary transition-all text-sm font-medium flex justify-between items-center group">
Tips & Trik
<span
class="text-home-primary opacity-0 -translate-x-2 group-hover:translate-x-0 group-hover:opacity-100 transition-all duration-300"></span>
class="text-home-primary transition-all duration-300 {{ !$categorySlug ? 'translate-x-0 opacity-100' : '-translate-x-2 opacity-0 group-hover:translate-x-0 group-hover:opacity-100' }}"></span>
</button>
@foreach ($categories as $category)
<button wire:click="setCategory('{{ $category['slug'] }}')"
class="w-full text-left px-4 py-3 rounded-xl transition-all text-sm font-medium flex justify-between items-center group {{ $categorySlug === $category['slug'] ? 'bg-home-primary/10 text-home-primary' : 'text-home-foreground/70 hover:bg-home-primary/5 hover:text-home-primary' }}">
{{ $category['name'] }}
<span
class="text-home-primary transition-all duration-300 {{ $categorySlug === $category['slug'] ? 'translate-x-0 opacity-100' : '-translate-x-2 opacity-0 group-hover:translate-x-0 group-hover:opacity-100' }}"></span>
</button>
@endforeach
</div>
</nav>
@ -297,43 +169,31 @@ class="text-home-primary opacity-0 -translate-x-2 group-hover:translate-x-0 grou
<h3 class="text-lg font-bold text-home-primary mb-6 border-b border-home-foreground/5 pb-4">
Populer</h3>
<div class="space-y-5">
<a href="#" class="block group">
<p
class="text-sm font-bold text-home-foreground group-hover:text-home-primary transition-colors line-clamp-2 leading-snug">
Tips Memilih Parfum yang Tepat Sesuai Kepribadian
</p>
<p class="text-xs text-home-foreground/40 mt-2 font-medium flex items-center gap-1">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z">
</path>
</svg>
2.5K views
</p>
</a>
<div class="border-t border-home-foreground/5"></div>
<a href="#" class="block group">
<p
class="text-sm font-bold text-home-foreground group-hover:text-home-primary transition-colors line-clamp-2 leading-snug">
Panduan Lengkap Cara Menyimpan Parfum
</p>
<p class="text-xs text-home-foreground/40 mt-2 font-medium flex items-center gap-1">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z">
</path>
</svg>
1.8K views
</p>
</a>
@foreach ($popularArticles as $popular)
<a href="{{ route('article.show', $popular['slug']) }}" class="block group">
<p
class="text-sm font-bold text-home-foreground group-hover:text-home-primary transition-colors line-clamp-2 leading-snug">
{{ $popular['title'] }}
</p>
<p
class="text-xs text-home-foreground/40 mt-2 font-medium flex items-center gap-1">
<svg class="w-3 h-3" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z">
</path>
</svg>
{{ $popular['views'] }}x dilihat
</p>
</a>
<div class="border-t border-home-foreground/5"></div>
@endforeach
</div>
</nav>
<section
{{-- <section
class="bg-home-primary rounded-2xl p-8 text-center relative overflow-hidden group shadow-lg shadow-home-primary/20">
<div
class="absolute top-0 right-0 w-32 h-32 bg-home-secondary/20 rounded-full blur-2xl -mr-10 -mt-10">
@ -359,7 +219,7 @@ class="w-full py-3 px-4 rounded-xl border border-white/20 bg-white/10 text-white
class="w-full bg-home-secondary text-home-primary font-bold py-3 rounded-xl hover:bg-white transition-all duration-300 text-sm shadow-lg">Subscribe</button>
</form>
</div>
</section>
</section> --}}
</div>
</aside>
@ -368,3 +228,7 @@ class="w-full bg-home-secondary text-home-primary font-bold py-3 rounded-xl hove
</main>
</div>
@assets
<script src="https://unpkg.com/@lottiefiles/dotlottie-wc@0.8.5/dist/dotlottie-wc.js" type="module"></script>
@endassets

View File

@ -1,201 +1,167 @@
@php
$article = (object) [
'title' => 'Meluncurkan "Oud Royal": Mahakarya Aroma Eksotis dari Timur Tengah',
'slug' => 'meluncurkan-oud-royal',
'category' => 'Event & Peluncuran',
'author' => 'Tim Editorial',
'date' => '12 Desember 2025',
'read_time' => '5 menit baca',
'image' => 'https://cdn.mos.cms.futurecdn.net/VzUqgr8pfbNcfXrpzeVBPE-1920-80.jpg',
'content_intro' =>
'Setelah penantian panjang dan riset mendalam selama dua tahun di jantung Kalimantan dan Dubai, kami dengan bangga mempersembahkan koleksi terbaru kami: Oud Royal. Ini bukan sekadar parfum, melainkan sebuah perjalanan aroma yang melintasi batas budaya.',
'tags' => ['Luxury', 'Oud', 'New Arrival', 'Event'],
];
$relatedArticles = [
[
'title' => 'Cara Membedakan Parfum Original dan Palsu',
'date' => '10 Des 2025',
'image' => 'https://placehold.co/400x300/png?text=Tips+Parfum',
],
[
'title' => 'Tren Wewangian Musk di Tahun 2026',
'date' => '08 Des 2025',
'image' => 'https://placehold.co/400x300/png?text=Tren+Musk',
],
[
'title' => 'Kenapa Parfum Tidak Tahan Lama di Kulit Kering?',
'date' => '05 Des 2025',
'image' => 'https://placehold.co/400x300/png?text=Kulit+Kering',
],
];
@endphp
<div class="flex flex-col min-h-screen bg-home-background font-sans">
<div class="flex flex-col bg-home-background font-sans">
<div class="fixed top-0 left-0 h-1 bg-home-primary z-50 transition-all duration-100" id="progressBar"
style="width: 0%"></div>
<main class="flex-grow pb-24 pt-36">
<header class="px-6 lg:px-32 xl:px-48 mb-12 text-center max-w-6xl mx-auto">
<div class="flex items-center justify-center gap-3 mb-6 text-sm font-bold tracking-widest uppercase">
<span class="text-home-primary bg-home-primary/10 px-3 py-1 rounded-full">{{ $article->category }}</span>
<span class="text-home-foreground/40">&bull;</span>
<span class="text-home-foreground/60">{{ $article->date }}</span>
<header class="px-6 lg:px-32 xl:px-48 mb-12 text-center mx-auto">
<div
class="flex flex-wrap items-center justify-center gap-3 mb-6 text-sm font-bold tracking-widest uppercase">
@foreach ($article->categories as $category)
<span
class="text-xs font-bold text-home-primary bg-home-primary/10 px-3 py-1 rounded-full border border-home-primary/10 uppercase tracking-wider">
{{ $category->name }}
</span>
@endforeach
</div>
<h1 class="text-3xl md:text-5xl font-bold text-home-foreground mb-6 leading-tight">
{{ $article->title }}
</h1>
<div class="flex items-center justify-center gap-4 text-sm text-home-foreground/60">
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded-full bg-home-foreground/10 overflow-hidden">
<img src="https://placehold.co/100x100/png?text=A" alt="Author"
class="w-full h-full object-cover">
<img src="https://ui-avatars.com/api/?name={{ urlencode($article->author?->employee?->full_name) }}"
alt="Author" class="w-full h-full object-cover">
</div>
<span>Oleh <strong class="text-home-foreground">{{ $article->author }}</strong></span>
<span>Oleh <strong
class="text-home-foreground">{{ $article->author?->employee?->full_name }}</strong></span>
</div>
<span>&bull;</span>
<span>{{ $article->read_time }}</span>
<span>{{ formatDateLocalized($article->published_at) }}</span>
<span>&bull;</span>
<span>{{ $article->views }}x dilihat</span>
</div>
</header>
<div class="px-6 lg:px-32 xl:px-48 mb-16">
<div class="aspect-video w-full rounded-xl overflow-hidden shadow-2xl">
<img src="{{ $article->image }}" alt="{{ $article->title }}" class="w-full h-full object-cover">
<img src="{{ $article->thumbnail }}" alt="{{ $article->title }}" class="w-full h-full object-cover">
</div>
</div>
<div class="px-6 lg:px-32 xl:px-48 grid grid-cols-1 lg:grid-cols-12 gap-12">
<article class="lg:col-span-8">
<div class="flex items-center gap-4 mb-8 pb-8 border-b border-home-foreground/10 lg:hidden">
<div class="flex items-center gap-4 mb-8 pb-8 border-b border-home-foreground/10">
<span class="text-sm font-bold text-home-foreground">Bagikan:</span>
<button
class="w-10 h-10 rounded-full border border-home-foreground/10 flex items-center justify-center text-home-foreground hover:bg-home-primary hover:text-white transition-colors"><svg
class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
{{-- WhatsApp --}}
<a href="https://wa.me/?text={{ urlencode($article->title . ' ' . url()->current()) }}"
target="_blank" rel="noopener noreferrer"
class="w-10 h-10 rounded-full border border-home-foreground/10 flex items-center justify-center text-home-foreground hover:bg-home-primary hover:text-white transition-colors">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path
d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z" />
</svg></button>
<button
class="w-10 h-10 rounded-full border border-home-foreground/10 flex items-center justify-center text-home-foreground hover:bg-home-primary hover:text-white transition-colors"><svg
class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L0 24l6.335-1.662c1.72.94 3.659 1.437 5.634 1.437h.005c6.515 0 11.825-5.309 11.828-11.825a11.823 11.823 0 00-3.414-8.369" />
</svg>
</a>
{{-- Facebook --}}
<a href="https://www.facebook.com/sharer/sharer.php?u={{ urlencode(url()->current()) }}"
target="_blank" rel="noopener noreferrer"
class="w-10 h-10 rounded-full border border-home-foreground/10 flex items-center justify-center text-home-foreground hover:bg-home-primary hover:text-white transition-colors">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path
d="M9 8h-3v4h3v12h5v-12h3.642l.358-4h-4v-1.667c0-.955.192-1.333 1.115-1.333h2.885v-5h-3.808c-3.596 0-5.192 1.583-5.192 4.615v3.385z" />
</svg></button>
<button
class="w-10 h-10 rounded-full border border-home-foreground/10 flex items-center justify-center text-home-foreground hover:bg-home-primary hover:text-white transition-colors"><svg
class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z">
</path>
</svg></button>
d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" />
</svg>
</a>
{{-- X (Twitter) --}}
<a href="https://twitter.com/intent/tweet?text={{ urlencode($article->title) }}&url={{ urlencode(url()->current()) }}"
target="_blank" rel="noopener noreferrer"
class="w-10 h-10 rounded-full border border-home-foreground/10 flex items-center justify-center text-home-foreground hover:bg-home-primary hover:text-white transition-colors">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
</a>
</div>
<div class="prose prose-lg max-w-none text-home-foreground/80 leading-relaxed">
<p
class="first-letter:text-6xl first-letter:font-bold first-letter:text-home-primary first-letter:float-left first-letter:mr-3 first-letter:mt-[-10px] mb-8 text-xl text-home-foreground font-medium">
{{ $article->content_intro }}
</p>
<p class="mb-6">
Dalam dunia wewangian, Oud (Gaharu) sering disebut sebagai "emas cair". Harganya yang fantastis
dan aromanya yang kompleks menjadikannya bahan primadona di kalangan kolektor parfum luxury.
Yadi Parfum mengambil langkah berani dengan menghadirkan interpretasi Oud yang lebih modern,
namun tetap menghormati tradisi Timur Tengah.
</p>
<h3 class="text-2xl font-bold text-home-foreground mt-10 mb-4">Filosofi di Balik Botol</h3>
<p class="mb-6">
"Kami ingin menciptakan sesuatu yang abadi," ujar Yadi, Founder dari Yadi Parfum. "Botol Oud
Royal didesain dengan kaca kristal berat dan tutup magnetik berlapis emas, merefleksikan
kualitas cairan di dalamnya."
</p>
<blockquote
class="border-l-4 border-home-primary pl-6 py-2 my-10 italic text-xl text-home-foreground font-serif bg-home-primary/5 rounded-r-lg">
"Parfum adalah seni yang tidak terlihat, puisi yang dihirup, dan kenangan yang tak terhapuskan."
</blockquote>
<p class="mb-6">
Peluncuran yang diadakan di Grand Ballroom Ritz Carlton Jakarta dihadiri oleh lebih dari 200
tamu undangan, termasuk selebriti, influencer kecantikan, dan kolektor parfum ternama. Acara ini
dimeriahkan dengan instalasi seni aroma interaktif.
</p>
<div class="my-10 rounded-2xl overflow-hidden shadow-lg">
<img src="https://placehold.co/800x400/png?text=Suasana+Event+Peluncuran" alt="Event"
class="w-full">
<p class="text-sm text-center text-home-foreground/50 py-3 italic bg-gray-50">Suasana peluncuran
Oud Royal di Jakarta.</p>
</div>
<h3 class="text-2xl font-bold text-home-foreground mt-10 mb-4">Ketersediaan</h3>
<p class="mb-6">
Oud Royal kini tersedia secara eksklusif di website resmi Yadi Parfum dan butik flagship kami di
Central Park Mall. Mengingat kelangkaan bahan bakunya, produksi batch pertama sangat terbatas.
</p>
</div>
<div class="mt-12 pt-8 border-t border-home-foreground/10">
<div class="flex flex-wrap gap-2">
@foreach ($article->tags as $tag)
<a href="#"
class="px-4 py-2 bg-home-foreground/5 text-home-foreground/70 rounded-lg text-sm font-bold hover:bg-home-primary hover:text-white transition-all">#{{ $tag }}</a>
@endforeach
<div
class="first-letter:text-6xl first-letter:font-bold first-letter:text-home-primary first-letter:float-left first-letter:mr-3 first-letter:mt-[-10px] mb-8 text-home-foreground text-justify">
{!! $article->content !!}
</div>
</div>
<div class="grid grid-cols-2 gap-6 mt-12">
<a href="#"
class="group block p-6 border border-home-foreground/10 rounded-2xl hover:border-home-primary/30 transition-all">
<span
class="text-xs text-home-foreground/40 font-bold uppercase mb-1 block group-hover:text-home-primary">Sebelumnya</span>
<h4
class="font-bold text-home-foreground line-clamp-2 group-hover:text-home-primary transition-colors">
Review Jujur: Koleksi Floral Series 2024</h4>
</a>
<a href="#"
class="group block p-6 border border-home-foreground/10 rounded-2xl text-right hover:border-home-primary/30 transition-all">
<span
class="text-xs text-home-foreground/40 font-bold uppercase mb-1 block group-hover:text-home-primary">Selanjutnya</span>
<h4
class="font-bold text-home-foreground line-clamp-2 group-hover:text-home-primary transition-colors">
Tips Menyimpan Parfum Agar Awet Bertahun-tahun</h4>
</a>
@if ($previousArticle)
<a href="{{ route('article.show', $previousArticle->slug) }}" wire:navigate
class="group block p-6 border border-home-foreground/10 rounded-2xl hover:border-home-primary/30 transition-all">
<span
class="text-xs text-home-foreground/40 font-bold uppercase mb-1 block group-hover:text-home-primary">Sebelumnya</span>
<h4
class="font-bold text-home-foreground line-clamp-2 group-hover:text-home-primary transition-colors">
{{ $previousArticle->title }}
</h4>
</a>
@endif
@if ($nextArticle)
<a href="{{ route('article.show', $nextArticle->slug) }}" wire:navigate
class="group block p-6 border border-home-foreground/10 rounded-2xl text-right hover:border-home-primary/30 transition-all">
<span
class="text-xs text-home-foreground/40 font-bold uppercase mb-1 block group-hover:text-home-primary">Selanjutnya</span>
<h4
class="font-bold text-home-foreground line-clamp-2 group-hover:text-home-primary transition-colors">
{{ $nextArticle->title }}</h4>
</a>
@endif
</div>
</article>
<aside class="lg:col-span-4 pl-0 lg:pl-8 border-l-0 lg:border-l border-home-foreground/5">
<div class="sticky top-32 space-y-10">
<div>
<h3 class="text-lg font-bold text-home-primary mb-6 flex items-center gap-2">
<span class="w-1 h-6 bg-home-primary rounded-full"></span>
Populer Minggu Ini
</h3>
<div class="space-y-6">
@foreach ($relatedArticles as $index => $item)
<a href="#" class="flex gap-4 group">
<div class="w-20 h-20 flex-shrink-0 rounded-xl overflow-hidden">
<img src="{{ $item['image'] }}"
class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500">
</div>
<div>
<span
class="text-xs text-home-foreground/40 font-bold">{{ sprintf('%02d', $index + 1) }}</span>
<h4
class="font-bold text-home-foreground text-sm leading-snug group-hover:text-home-primary transition-colors mt-1">
{{ $item['title'] }}</h4>
<span
class="text-xs text-home-foreground/50 mt-2 block">{{ $item['date'] }}</span>
</div>
</a>
@endforeach
<div x-data="{
progress: 0,
updateProgress() {
const winScroll = window.pageYOffset || document.documentElement.scrollTop
const height = document.documentElement.scrollHeight - document.documentElement.clientHeight
this.progress = height > 0 ? (winScroll / height) * 100 : 0
}
}" x-init="updateProgress();
window.addEventListener('scroll', updateProgress)">
<!-- progress bar -->
<div class="fixed top-0 left-0 w-full h-1 bg-transparent z-50">
<div class="h-full bg-home-primary transition-all duration-75"
:style="`width: ${progress}%`" id="progressBar"></div>
</div>
<!-- konten kamu -->
<div>
<h3 class="text-lg font-bold text-home-primary mb-6 flex items-center gap-2">
<span class="w-1 h-6 bg-home-primary rounded-full"></span>
Artikel Terbaru
</h3>
<div class="space-y-6">
@foreach ($latestArticles as $latest)
<a href="{{ route('article.show', $latest['slug']) }}" wire:navigate
class="flex gap-4 group">
<div class="w-20 h-20 flex-shrink-0 rounded-xl overflow-hidden">
<img src="{{ $latest['thumbnail'] }}"
class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500">
</div>
<div>
<h4
class="font-bold text-home-foreground text-sm leading-snug group-hover:text-home-primary transition-colors mt-1">
{{ $latest['title'] }}
</h4>
<span class="text-xs text-home-foreground/50 mt-2 block">
{{ formatDateLocalized($latest['published_at']) }}
</span>
<span class="text-xs text-home-foreground/40 font-bold">
{{ $latest['views'] }}x dilihat
</span>
</div>
</a>
@endforeach
</div>
</div>
</div>
<section
{{-- Not used yet --}}
{{-- <section
class="bg-home-primary rounded-2xl p-8 text-center relative overflow-hidden group shadow-lg shadow-home-primary/20">
<div
class="absolute top-0 right-0 w-32 h-32 bg-home-secondary/20 rounded-full blur-2xl -mr-10 -mt-10">
@ -221,7 +187,7 @@ class="w-full py-3 px-4 rounded-xl border border-white/20 bg-white/10 text-white
class="w-full bg-home-secondary text-home-primary font-bold py-3 rounded-xl hover:bg-white transition-all duration-300 text-sm shadow-lg">Subscribe</button>
</form>
</div>
</section>
</section> --}}
</div>
</aside>
@ -231,11 +197,21 @@ class="w-full bg-home-secondary text-home-primary font-bold py-3 rounded-xl hove
</main>
</div>
<script>
window.onscroll = function() {
let winScroll = document.body.scrollTop || document.documentElement.scrollTop;
let height = document.documentElement.scrollHeight - document.documentElement.clientHeight;
let scrolled = (winScroll / height) * 100;
document.getElementById("progressBar").style.width = scrolled + "%";
};
</script>
@script
<script>
window.onscroll = function() {
let winScroll = document.body.scrollTop || document.documentElement.scrollTop;
let height = document.documentElement.scrollHeight - document.documentElement.clientHeight;
let scrolled = (winScroll / height) * 100;
document.getElementById("progressBar").style.width = scrolled + "%";
};
</script>
@endscript
@assets
<style>
.prose p:empty {
min-height: 1.25rem;
}
</style>
@endassets

View File

@ -1,102 +0,0 @@
<div>
{{-- Hero Section --}}
<section class="relative h-[500px] flex items-center justify-center overflow-hidden">
{{-- Background Image --}}
<div class="absolute inset-0 z-0">
<img src="https://images.pexels.com/photos/2789781/pexels-photo-2789781.jpeg" alt="Perfume Background"
class="w-full h-full object-cover">
<div class="absolute inset-0 bg-black/40"></div>
</div>
{{-- Content --}}
<div class="relative z-10 container mx-auto px-6 text-center">
<h1 class="text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-6">Artikel Terbaru</h1>
<p class="text-white/80 text-lg md:text-xl max-w-2xl mx-auto mb-10">
Baca tips profesional seputar perawatan wewangian, tren lifestyle, dan rekomendasi terbaik.
</p>
{{-- Search Bar --}}
<div class="max-w-xl mx-auto relative group">
<input type="search" placeholder="Cari artikel menarik..." aria-label="Cari artikel"
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
class="absolute right-2 top-2 bottom-2 w-10 h-10 bg-home-primary rounded-full flex items-center justify-center text-white hover:bg-white hover:text-home-primary transition-all duration-300">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</button>
</div>
</div>
</section>
<main class="px-6 lg:px-32 xl:px-48 py-24 bg-white min-h-screen font-sans">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 items-start" x-data="{ activeAccordion: null }">
@foreach ([
['q' => 'Bagaimana cara melakukan pemesanan?', 'a' => 'Anda dapat melakukan pemesanan melalui website atau aplikasi kami. Pilih produk yang diinginkan, masukkan ke keranjang, dan ikuti langkah checkout.'],
['q' => 'Berapa lama waktu pengiriman?', 'a' => 'Pengiriman untuk area Jakarta 1-2 hari kerja dan luar Jakarta 3-5 hari kerja tergantung ekspedisi yang dipilih.'],
['q' => 'Apakah produk original?', 'a' => 'Semua produk 100% original dari distributor resmi dengan sertifikat keaslian dan garansi uang kembali.'],
['q' => 'Bagaimana kebijakan pengembalian barang?', 'a' => 'Pengembalian dalam 30 hari dengan syarat barang belum digunakan, segel utuh, dan menyertakan video unboxing.'],
['q' => 'Metode pembayaran apa saja yang tersedia?', 'a' => 'Kami menerima Transfer bank (BCA, Mandiri), Kartu Kredit, E-wallet (GoPay, OVO), dan cicilan 0%.'],
['q' => 'Bagaimana cara menggunakan voucher?', 'a' => 'Masukkan kode voucher pada halaman checkout di kolom "Kode Promo", diskon akan otomatis terpotong.'],
] as $index => $faq)
<article
class="bg-white rounded-2xl border border-home-foreground/10 overflow-hidden shadow-sm hover:shadow-md transition-shadow group">
<button
@click="activeAccordion = (activeAccordion === {{ $index }} ? null : {{ $index }})"
class="w-full px-6 py-5 flex items-center justify-between text-left focus:outline-none transition-colors"
:class="activeAccordion === {{ $index }} ? 'bg-home-primary/5' :
'bg-white hover:bg-home-foreground/5'">
<h3 class="text-lg font-bold pr-4 transition-colors"
:class="activeAccordion === {{ $index }} ? 'text-home-primary' :
'text-home-foreground group-hover:text-home-primary'">
{{ $faq['q'] }}
</h3>
<span class="flex-shrink-0 ml-4 p-1 rounded-full transition-all duration-300"
:class="activeAccordion === {{ $index }} ? 'bg-home-primary text-white rotate-180' :
'bg-home-foreground/10 text-home-foreground group-hover:bg-home-primary group-hover:text-white'">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</span>
</button>
<div x-show="activeAccordion === {{ $index }}" x-collapse x-cloak
class="px-6 pb-6 pt-2 bg-home-primary/5 border-t border-home-primary/10 text-home-foreground/70 leading-relaxed text-sm md:text-base">
{{ $faq['a'] }}
</div>
</article>
@endforeach
</div>
<div
class="mt-16 bg-home-primary rounded-2xl p-10 text-center relative overflow-hidden shadow-xl shadow-home-primary/20">
<div class="absolute top-0 right-0 w-48 h-48 bg-white/10 rounded-full blur-3xl -mr-12 -mt-12"></div>
<div class="absolute bottom-0 left-0 w-32 h-32 bg-home-secondary/20 rounded-full blur-2xl -ml-8 -mb-8">
</div>
<div class="relative z-10">
<h3 class="text-2xl font-bold text-white mb-3">Masih ada pertanyaan?</h3>
<p class="text-white/80 mb-8 max-w-xl mx-auto">Jika Anda tidak menemukan jawaban yang dicari, tim
customer service kami siap membantu Anda 24/7.</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<button
class="bg-white text-home-primary font-bold px-8 py-3 rounded-xl hover:bg-home-secondary hover:text-white transition-all shadow-lg">
Chat WhatsApp
</button>
<button
class="bg-home-primary border border-white/20 text-white font-bold px-8 py-3 rounded-xl hover:bg-white/10 transition-all">
Kirim Email
</button>
</div>
</div>
</div>
</section>
</div>

View File

@ -0,0 +1,112 @@
<div>
{{-- Hero Section --}}
<section class="relative h-[500px] flex items-center justify-center overflow-hidden">
{{-- Background Image --}}
<div class="absolute inset-0 z-0">
<img src="https://images.pexels.com/photos/2789781/pexels-photo-2789781.jpeg" alt="Perfume Background"
class="w-full h-full object-cover">
<div class="absolute inset-0 bg-black/40"></div>
</div>
{{-- Content --}}
<div class="relative z-10 container mx-auto px-6 text-center">
<h1 class="text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-6">{{ $pageTitle }}</h1>
<p class="text-white/80 text-lg md:text-xl max-w-2xl mx-auto mb-10">
{{ $pageDesc }}
</p>
{{-- Search Bar --}}
<div class="max-w-xl mx-auto relative group">
<input wire:model.live.debounce.500="search" type="search" placeholder="Cari pertanyaan..."
aria-label="Cari artikel"
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
class="absolute right-2 top-2 bottom-2 w-10 h-10 bg-home-primary rounded-full flex items-center justify-center text-white hover:bg-white hover:text-home-primary transition-all duration-300">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</button>
</div>
</div>
</section>
<main class="px-6 lg:px-32 xl:px-48 py-24 bg-white text-black font-sans">
@if (!empty($faqs))
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 items-start" x-data="{ activeAccordion: null }">
@foreach ($faqs as $index => $faq)
<article
class="bg-white rounded-2xl border border-home-foreground/10 overflow-hidden shadow-sm hover:shadow-md transition-shadow group">
<button
@click="activeAccordion = (activeAccordion === {{ $index }} ? null : {{ $index }})"
class="w-full px-6 py-5 flex items-center justify-between text-left focus:outline-none transition-colors"
:class="activeAccordion === {{ $index }} ? 'bg-home-primary/5' :
'bg-white hover:bg-home-foreground/5'">
<h3 class="text-lg font-bold pr-4 transition-colors"
:class="activeAccordion === {{ $index }} ? 'text-home-primary' :
'text-home-foreground group-hover:text-home-primary'">
{{ $faq['question'] }}
</h3>
<span class="flex-shrink-0 ml-4 p-1 rounded-full transition-all duration-300"
:class="activeAccordion === {{ $index }} ? 'bg-home-primary text-white rotate-180' :
'bg-home-foreground/10 text-home-foreground group-hover:bg-home-primary group-hover:text-white'">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none"
viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</span>
</button>
<div x-show="activeAccordion === {{ $index }}"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 max-h-0" x-transition:enter-end="opacity-100 max-h-96"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 max-h-96" x-transition:leave-end="opacity-0 max-h-0"
x-cloak
class="px-6 pb-6 pt-2 bg-home-primary/5 border-t border-home-primary/10 text-home-foreground/70 leading-relaxed text-sm md:text-base overflow-hidden">
{{ $faq['answer'] }}
</div>
</article>
@endforeach
</div>
@else
@include('components.animations.lottie.not-found')
@endif
<div
class="mt-16 bg-home-primary rounded-2xl p-10 text-center relative overflow-hidden shadow-xl shadow-home-primary/20">
<div class="absolute top-0 right-0 w-48 h-48 bg-white/10 rounded-full blur-3xl -mr-12 -mt-12"></div>
<div class="absolute bottom-0 left-0 w-32 h-32 bg-home-secondary/20 rounded-full blur-2xl -ml-8 -mb-8">
</div>
<div class="relative z-10">
<h3 class="text-2xl font-bold text-white mb-3">Masih ada pertanyaan?</h3>
<p class="text-white/80 mb-8 max-w-xl mx-auto">
Jika Anda tidak menemukan jawaban yang dicari, silakan hubungi kami dan kami siap membantu Anda
24/7.
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<!-- WhatsApp -->
<a href="https://wa.me/{{ formatToWhatsApp($contactSettings->phone_number) }}" target="_blank"
class="bg-white text-home-primary font-bold px-8 py-3 rounded-xl hover:bg-home-secondary hover:text-white transition-all shadow-lg text-center">
Chat WhatsApp
</a>
<!-- Email -->
<a href="mailto:{{ $contactSettings->email }}"
class="bg-home-primary border border-white/20 text-white font-bold px-8 py-3 rounded-xl hover:bg-white/10 transition-all text-center">
Kirim Email
</a>
</div>
</div>
</div>
</main>
</div>
@assets
<script src="https://unpkg.com/@lottiefiles/dotlottie-wc@0.8.5/dist/dotlottie-wc.js" type="module"></script>
@endassets

View File

@ -98,7 +98,7 @@ class="w-12 h-12 rounded-xl border border-home-foreground/10 flex items-center j
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 scale-100 translate-y-0"
x-transition:leave-end="opacity-0 scale-95 translate-y-2"
class="absolute bottom-full right-0 mb-3 w-48 bg-white rounded-2xl shadow-xl border border-home-foreground/5 py-2 z-30"
class="absolute bottom-full mb-3 w-48 bg-white rounded-2xl shadow-xl border border-home-foreground/5 py-2 z-30 {{ $outlet['maps_url'] ? 'right-0' : 'left-0' }}"
style="display: none;">
<div
@ -107,7 +107,7 @@ class="px-4 py-2 text-[10px] font-bold text-home-foreground/40 uppercase trackin
</div>
{{-- WhatsApp --}}
<a href="https://wa.me/?text={{ urlencode('Cek outlet ' . $outlet['name'] . ' di Pangestu Yadi Parfum: ' . ($outlet['maps_url'] ?: url()->current())) }}"
<a href="https://wa.me/?text={{ urlencode('Cek ' . $outlet['name'] . ' di Yadi Parfum: ' . ($outlet['maps_url'] ?: url()->current())) }}"
target="_blank"
class="flex items-center gap-3 px-4 py-2 text-sm text-home-foreground hover:bg-home-primary/5 hover:text-home-primary transition-colors">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">

View File

@ -1,38 +1,24 @@
<div class="flex flex-col min-h-screen bg-white font-sans">
<div>
{{-- Hero Section --}}
<section class="relative h-[500px] flex items-center justify-center overflow-hidden">
{{-- Background Image --}}
<div class="absolute inset-0 z-0">
<img src="https://images.pexels.com/photos/22589353/pexels-photo-22589353.jpeg"
alt="Perfume Collection Background" class="w-full h-full object-cover">
<div class="absolute inset-0 bg-black/50"></div>
<img src="https://images.pexels.com/photos/22589353/pexels-photo-22589353.jpeg" alt="Article Background"
class="w-full h-full object-cover">
<div class="absolute inset-0 bg-black/40"></div>
</div>
{{-- Content --}}
<div class="relative z-10 container mx-auto px-6 text-center">
<h1 class="text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-6">
@if ($productType === 'perfume')
Koleksi Parfum
@elseif($productType === 'bottle')
Koleksi Botol
@else
Koleksi Arabian
@endif
</h1>
<h1 class="text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-6">{{ $pageTitle }}</h1>
<p class="text-white/80 text-lg md:text-xl max-w-2xl mx-auto mb-10">
@if ($productType === 'perfume')
Temukan aroma khas yang mencerminkan kepribadian dan gaya Anda.
@elseif($productType === 'bottle')
Pilihan botol berkualitas untuk parfum Anda.
@else
Koleksi produk Arabian eksklusif dan mewah.
@endif
{{ $pageDesc }}
</p>
{{-- Search Bar --}}
<div class="max-w-xl mx-auto relative group">
<input type="text" placeholder="Cari produk..."
<input wire:model.live.debounce.500="search" type="search" placeholder="Cari produk..."
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
class="absolute right-2 top-2 bottom-2 w-10 h-10 bg-home-primary rounded-full flex items-center justify-center text-white hover:bg-white hover:text-home-primary transition-all duration-300">
@ -45,20 +31,20 @@ class="absolute right-2 top-2 bottom-2 w-10 h-10 bg-home-primary rounded-full fl
</div>
</section>
<main class="flex-grow px-6 lg:px-32 xl:px-48 py-24">
<main class="px-6 lg:px-32 xl:px-48 py-24 bg-white text-black font-sans">
<div class="grid grid-cols-1 lg:grid-cols-4 gap-12 mb-24">
<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">
{{-- Filter Jenis Produk --}}
{{-- Product type filter --}}
<h3 class="font-bold text-home-primary mb-6 border-b border-home-foreground/10 pb-4">Jenis Produk
</h3>
<div class="space-y-4">
@foreach ([['value' => 'perfume', 'label' => 'Parfum'], ['value' => 'bottle', 'label' => 'Botol'], ['value' => 'arabian', 'label' => 'Arabian']] as $type)
<label class="flex items-center cursor-pointer group">
<input type="radio" name="productType" value="{{ $type['value'] }}"
wire:model.live="productType" {{ $productType === $type['value'] ? 'checked' : '' }}
wire:model.live="productType"
class="w-5 h-5 border-home-foreground/20 text-home-primary focus:ring-home-primary cursor-pointer accent-home-primary">
<span
class="ml-3 text-home-foreground/70 group-hover:text-home-primary transition-colors text-sm font-medium {{ $productType === $type['value'] ? 'font-bold text-home-primary' : '' }}">{{ $type['label'] }}</span>
@ -68,25 +54,30 @@ 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>
<h3 class="font-bold text-home-primary mb-6 border-b border-home-foreground/10 pb-4">Kategori</h3>
<div class="space-y-4">
@foreach (['Pria', 'Wanita', 'Unisex', 'Luxury', 'Best Seller', 'New Arrival'] as $cat)
<label class="flex items-center cursor-pointer group">
<input type="checkbox"
class="w-5 h-5 rounded border-home-foreground/20 text-home-primary focus:ring-home-primary cursor-pointer accent-home-primary">
<span
class="ml-3 text-home-foreground/70 group-hover:text-home-primary transition-colors text-sm font-medium">{{ $cat }}</span>
</label>
@endforeach
</div>
@if ($productType === 'perfume')
<h3 class="font-bold text-home-primary mb-6 border-b border-home-foreground/10 pb-4">Kategori
</h3>
<div class="space-y-4">
@foreach ($availableCategories as $id => $name)
<label class="flex items-center cursor-pointer group">
<input type="checkbox" value="{{ $name }}"
wire:model.live="selectedCategories"
class="w-5 h-5 rounded border-home-foreground/20 text-home-primary focus:ring-home-primary cursor-pointer accent-home-primary">
<span
class="ml-3 text-home-foreground/70 group-hover:text-home-primary transition-colors text-sm font-medium">{{ $name }}</span>
</label>
@endforeach
</div>
<div class="border-t border-home-foreground/10 my-8"></div>
<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>
<div class="space-y-4">
@foreach (['< 100k', '100k - 500k', '500k - 1jt', '> 1jt'] as $price)
<label class="flex items-center cursor-pointer group">
<input type="radio" name="price"
<input type="radio" name="price" value="{{ $price }}"
wire:model.live="priceRange"
class="w-5 h-5 border-home-foreground/20 text-home-primary focus:ring-home-primary cursor-pointer accent-home-primary">
<span
class="ml-3 text-home-foreground/70 group-hover:text-home-primary transition-colors text-sm font-medium">{{ $price }}</span>
@ -101,12 +92,12 @@ class="ml-3 text-home-foreground/70 group-hover:text-home-primary transition-col
{{-- Toolbar (Sort Only) --}}
<div class="flex justify-end mb-8">
<div class="relative min-w-[200px]">
<select
<select wire:model.live="sort"
class="w-full appearance-none px-6 py-3 pr-10 rounded-xl border border-home-foreground/10 bg-white text-home-foreground font-medium focus:outline-none focus:ring-2 focus:ring-home-primary focus:border-transparent transition cursor-pointer shadow-sm">
<option>Urutkan: Terbaru</option>
<option>Harga Terendah</option>
<option>Harga Tertinggi</option>
<option>Terpopuler</option>
<option value="latest">Urutkan: Terbaru</option>
<option value="price_low">Harga Terendah</option>
<option value="price_high">Harga Tertinggi</option>
<option value="popular">Terpopuler</option>
</select>
<svg class="absolute right-4 top-4 w-4 h-4 text-home-foreground/50 pointer-events-none"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -116,85 +107,113 @@ class="w-full appearance-none px-6 py-3 pr-10 rounded-xl border border-home-fore
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
@foreach ($perfumes as $perfume)
<article
class="group bg-white rounded-2xl border border-home-foreground/10 overflow-hidden hover:shadow-xl hover:shadow-home-primary/5 hover:border-home-primary/30 hover:-translate-y-1 transition-all duration-300 flex flex-col h-full relative">
@if ($products->isNotEmpty())
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
@foreach ($products as $product)
<article
class="group bg-white rounded-2xl border border-home-foreground/10 overflow-hidden hover:shadow-xl hover:shadow-home-primary/5 hover:border-home-primary/30 hover:-translate-y-1 transition-all duration-300 flex flex-col h-full relative">
@if ($perfume['is_new'])
<span
class="absolute top-4 left-4 bg-home-primary text-white text-[10px] uppercase tracking-wider font-bold px-3 py-1 rounded-full z-10 shadow-md">New</span>
@endif
<div class="relative h-72 overflow-hidden bg-home-primary/5">
<img src="{{ $perfume['image'] }}" alt="{{ $perfume['name'] }}"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500">
<div
class="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center gap-3">
<a href="#"
class="w-10 h-10 bg-white rounded-full flex items-center justify-center text-home-foreground hover:bg-home-primary hover:text-white transition-all shadow-lg transform hover:scale-110"
title="Lihat Detail">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z">
</path>
</svg>
</a>
<button
class="w-10 h-10 bg-white rounded-full flex items-center justify-center text-home-foreground hover:bg-home-primary hover:text-white transition-all shadow-lg transform hover:scale-110"
title="Simpan ke Wishlist">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z">
</path>
</svg>
</button>
</div>
</div>
<div class="p-5 flex flex-col flex-grow">
<div class="flex justify-between items-start mb-2">
@if (isset($product->badge) && $product->badge)
<span
class="text-[10px] font-bold tracking-widest text-home-primary uppercase bg-home-primary/10 px-2 py-1 rounded">{{ $perfume['category'] }}</span>
<div class="flex items-center gap-1">
class="absolute top-4 left-4 {{ $product->badge['class'] }} text-[10px] uppercase tracking-wider font-bold px-3 py-1 rounded-full z-10 shadow-md">
{{ $product->badge['label'] }}
</span>
@endif
<div class="relative h-72 overflow-hidden bg-home-primary/5">
<img src="{{ $product['image'] }}" alt="{{ $product['name'] }}"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500">
<div
class="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center gap-3">
<a href="{{ route('product.show', $product->slug) }}" wire:navigate
class="w-10 h-10 bg-white rounded-full flex items-center justify-center text-home-foreground hover:bg-home-primary hover:text-white transition-all shadow-lg transform hover:scale-110"
title="Lihat Detail">
<svg class="w-5 h-5" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z">
</path>
</svg>
</a>
{{-- Not used yet --}}
{{-- <button
class="w-10 h-10 bg-white rounded-full flex items-center justify-center text-home-foreground hover:bg-home-primary hover:text-white transition-all shadow-lg transform hover:scale-110"
title="Simpan ke Wishlist">
<svg class="w-5 h-5" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z">
</path>
</svg>
</button> --}}
</div>
</div>
<div class="p-5 flex flex-col flex-grow">
@if ($productType === 'perfume')
<div class="flex flex-wrap gap-2 mb-2">
@foreach ($product->categories->take(3) as $category)
<span
class="text-xs font-bold text-home-primary bg-home-primary/10 px-3 py-1 rounded-full border border-home-primary/10 uppercase tracking-wider whitespace-nowrap">
{{ $category->name }}
</span>
@endforeach
@if ($product->categories->count() > 3)
<span
class="text-xs font-bold text-home-foreground/50 px-3 py-1 rounded-full border border-home-foreground/10 whitespace-nowrap">
+{{ $product->categories->count() - 3 }}
</span>
@endif
{{-- Not used yet --}}
{{-- <div class="flex items-center gap-1">
<svg class="w-3 h-3 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
<path
d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
<span
class="text-xs font-bold text-home-foreground">{{ $perfume['rating'] }}</span>
class="text-xs font-bold text-home-foreground">{{ $product['rating'] }}</span>
<span
class="text-xs text-home-foreground/40">({{ $perfume['reviews'] }})</span>
</div>
</div>
class="text-xs text-home-foreground/40">({{ $product['reviews'] }})</span>
</div> --}}
</div>
@endif
<h3
class="text-lg font-bold text-home-foreground group-hover:text-home-primary transition-colors mb-4 line-clamp-1">
{{ $perfume['name'] }}</h3>
<h3
class="text-lg font-bold text-home-foreground group-hover:text-home-primary transition-colors my-4 line-clamp-1">
<a
href="{{ route('product.show', $product['slug']) }}">{{ $product['name'] }}</a>
</h3>
<div
class="mt-auto pt-4 border-t border-home-foreground/5 flex items-center justify-between">
<span class="text-xl font-bold text-home-primary">Rp
{{ number_format($perfume['price'], 0, ',', '.') }}</span>
<button
<div
class="mt-auto pt-4 border-t border-home-foreground/5 flex items-center justify-between">
<span class="text-xl font-bold text-home-primary">Rp
{{ formatCurrencyNumber($product['sale_price']) }}</span>
{{-- Not used yet --}}
{{-- <button
class="w-10 h-10 rounded-lg bg-home-foreground text-white flex items-center justify-center hover:bg-home-primary transition-colors shadow-md">
<svg class="w-5 h-5" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 4v16m8-8H4"></path>
</svg>
</button>
</button> --}}
</div>
</div>
</div>
</article>
@endforeach
</div>
</article>
@endforeach
</div>
@else
@include('components.animations.lottie.not-found')
@endif
<div class="mt-16 flex justify-center">
{{ $perfumes->links('vendor.livewire.custom-pagination') }}
<div class="mt-16">
{{ $products->links('vendor.livewire.custom-pagination') }}
</div>
</section>
</div>
@ -227,23 +246,41 @@ class="flex gap-6 overflow-x-auto pb-12 snap-x snap-mandatory scroll-smooth hide
<article
class="min-w-[280px] md:min-w-[300px] bg-white rounded-2xl border border-home-foreground/10 overflow-hidden hover:shadow-xl hover:border-home-primary/30 transition-all duration-300 snap-start group flex flex-col h-full">
<div class="h-60 overflow-hidden relative bg-home-primary/5">
<img src="{{ $rec['image'] }}" alt="{{ $rec['name'] }}"
<img src="{{ $rec['image'] }}" alt="{{ $rec['image'] }}"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500">
</div>
<div class="p-5 flex flex-col flex-grow">
<div class="text-[10px] font-bold tracking-widest text-home-primary uppercase mb-2">
{{ $rec['category'] }}</div>
<h3 class="text-lg font-bold text-home-foreground mb-1">{{ $rec['name'] }}</h3>
<div class="flex flex-wrap gap-2 mb-2">
@foreach (array_slice($rec['categories'], 0, 3) as $category)
<span
class="text-xs font-bold text-home-primary bg-home-primary/10 px-3 py-1 rounded-full border border-home-primary/10 uppercase tracking-wider whitespace-nowrap">
{{ $category['name'] }}
</span>
@endforeach
@if (count($rec['categories']) > 3)
<span
class="text-xs font-bold text-home-foreground/50 px-3 py-1 rounded-full border border-home-foreground/10 whitespace-nowrap">
+{{ count($rec['categories']) - 3 }}
</span>
@endif
</div>
<h3 class="text-lg font-bold text-home-foreground mb-1">
<a href="{{ route('product.show', $rec['slug']) }}">
{{ $rec['name'] }}</a>
</h3>
<div class="mt-auto pt-4 flex items-center justify-between">
<span class="font-bold text-home-primary">Rp
{{ number_format($rec['price'], 0, ',', '.') }}</span>
<button
{{ formatCurrencyNumber($rec['sale_price']) }}</span>
{{-- Not used yet --}}
{{-- <button
class="w-8 h-8 rounded bg-home-foreground/5 text-home-foreground hover:bg-home-primary hover:text-white flex items-center justify-center transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 4v16m8-8H4"></path>
</svg>
</button>
</button> --}}
</div>
</div>
</article>
@ -254,26 +291,6 @@ class="w-8 h-8 rounded bg-home-foreground/5 text-home-foreground hover:bg-home-p
</main>
</div>
<script>
document.addEventListener("DOMContentLoaded", () => {
const slider = document.getElementById('productSlider');
const leftBtn = document.getElementById('slideLeft');
const rightBtn = document.getElementById('slideRight');
if (slider && leftBtn && rightBtn) {
rightBtn.addEventListener('click', () => {
slider.scrollBy({
left: 320,
behavior: 'smooth'
});
});
leftBtn.addEventListener('click', () => {
slider.scrollBy({
left: -320,
behavior: 'smooth'
});
});
}
});
</script>
@assets
<script src="https://unpkg.com/@lottiefiles/dotlottie-wc@0.8.5/dist/dotlottie-wc.js" type="module"></script>
@endassets

View File

@ -1,274 +0,0 @@
@php
$product = (object) [
'name' => 'Oud Royal Premium',
'category' => 'Luxury Collection',
'price' => 1200000,
'original_price' => 1500000,
'rating' => 4.9,
'reviews_count' => 128,
'description' => 'Oud Royal adalah wewangian oriental yang memikat, menggabungkan kemewahan kayu gaharu (oud) dengan kelembutan mawar Bulgaria dan sentuhan rempah eksotis. Dirancang untuk pria dan wanita yang menginginkan aroma elegan dan tahan lama hingga 12 jam.',
'stock' => 15,
'sizes' => ['30ml', '50ml', '100ml'],
'images' => [
'https://placehold.co/600x600/png?text=Oud+Royal+1',
'https://placehold.co/600x600/png?text=Oud+Royal+2',
'https://placehold.co/600x600/png?text=Oud+Royal+3',
'https://placehold.co/600x600/png?text=Oud+Royal+4',
],
'notes' => [
'top' => 'Bergamot, Lemon, Pink Pepper',
'middle' => 'Bulgarian Rose, Saffron, Jasmine',
'base' => 'Agarwood (Oud), Amber, Musk, Vanilla'
]
];
$relatedProducts = [
[
'name' => 'Golden Amber',
'category' => 'Oriental',
'price' => 890000,
'image' => 'https://placehold.co/400x500/png?text=Golden+Amber',
'rating' => 4.9,
],
[
'name' => 'Spring Blossom',
'category' => 'Floral',
'price' => 380000,
'image' => 'https://placehold.co/400x500/png?text=Spring+Blossom',
'rating' => 4.7,
],
[
'name' => 'Woody Intense',
'category' => 'Woody',
'price' => 650000,
'image' => 'https://placehold.co/400x500/png?text=Woody+Intense',
'rating' => 4.8,
],
[
'name' => 'Aqua Marine',
'category' => 'Fresh',
'price' => 420000,
'image' => 'https://placehold.co/400x500/png?text=Aqua+Marine',
'rating' => 4.6,
],
[
'name' => 'Spicy Leather',
'category' => 'Leather',
'price' => 750000,
'image' => 'https://placehold.co/400x500/png?text=Spicy+Leather',
'rating' => 4.9,
],
];
@endphp
<div class="flex flex-col min-h-screen bg-home-background font-sans text-home-foreground" x-data="{
selectedSize: '50ml',
mainImage: '{{ $product->images[0] }}',
qty: 1
}">
<main class="flex-grow px-6 lg:px-32 xl:px-48 pb-24 pt-32">
<nav class="flex items-center text-sm text-home-foreground/50 mb-8 overflow-x-auto whitespace-nowrap">
<a href="#" class="hover:text-home-primary transition-colors">Beranda</a>
<svg class="w-3 h-3 mx-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
<a href="#" class="hover:text-home-primary transition-colors">Produk</a>
<svg class="w-3 h-3 mx-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
<a href="#" class="hover:text-home-primary transition-colors">{{ $product->category }}</a>
<svg class="w-3 h-3 mx-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
<span class="text-home-primary font-medium">{{ $product->name }}</span>
</nav>
<section class="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-20 mb-24">
<div class="lg:col-span-7">
<div class="sticky top-32 space-y-6">
<div class="aspect-square bg-home-primary/5 rounded-3xl overflow-hidden border border-home-foreground/5 relative group">
<img :src="mainImage" alt="{{ $product->name }}" class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105">
<div class="absolute top-6 left-6">
<span class="bg-home-primary text-white text-xs font-bold px-3 py-1.5 rounded-full uppercase tracking-wider shadow-lg">Best Seller</span>
</div>
</div>
<div class="grid grid-cols-4 gap-4">
@foreach($product->images as $img)
<button @click="mainImage = '{{ $img }}'"
class="aspect-square rounded-xl overflow-hidden border-2 transition-all duration-300"
:class="mainImage === '{{ $img }}' ? 'border-home-primary ring-2 ring-home-primary/20' : 'border-transparent hover:border-home-foreground/20'">
<img src="{{ $img }}" class="w-full h-full object-cover">
</button>
@endforeach
</div>
</div>
</div>
<div class="lg:col-span-5 flex flex-col h-full">
<div class="mb-auto">
<div class="flex items-center justify-between mb-4">
<span class="text-sm font-bold tracking-widest text-home-primary uppercase bg-home-primary/10 px-3 py-1 rounded">{{ $product->category }}</span>
<div class="flex items-center gap-1 text-yellow-500 text-sm">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/></svg>
<span class="font-bold text-home-foreground">{{ $product->rating }}</span>
<span class="text-home-foreground/40 underline decoration-home-foreground/20 cursor-pointer">({{ $product->reviews_count }} Ulasan)</span>
</div>
</div>
<h1 class="text-3xl md:text-4xl font-bold text-home-foreground mb-4 leading-tight">{{ $product->name }}</h1>
<div class="flex items-end gap-3 mb-8 border-b border-home-foreground/10 pb-8">
<span class="text-3xl font-bold text-home-primary">Rp {{ number_format($product->price, 0, ',', '.') }}</span>
<span class="text-lg text-home-foreground/40 line-through mb-1">Rp {{ number_format($product->original_price, 0, ',', '.') }}</span>
</div>
<div class="mb-8">
<p class="text-home-foreground/70 leading-relaxed text-lg">
{{ $product->description }}
</p>
</div>
<div class="bg-home-foreground/5 rounded-2xl p-6 mb-8 border border-home-foreground/5">
<h3 class="font-bold text-home-foreground mb-4 flex items-center gap-2">
<svg class="w-5 h-5 text-home-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.384-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"></path></svg>
Fragrance Notes
</h3>
<div class="space-y-3">
<div class="flex">
<span class="w-24 text-sm font-bold text-home-foreground/50">Top Notes</span>
<span class="text-sm font-medium text-home-foreground">{{ $product->notes['top'] }}</span>
</div>
<div class="flex">
<span class="w-24 text-sm font-bold text-home-foreground/50">Middle</span>
<span class="text-sm font-medium text-home-foreground">{{ $product->notes['middle'] }}</span>
</div>
<div class="flex">
<span class="w-24 text-sm font-bold text-home-foreground/50">Base Notes</span>
<span class="text-sm font-medium text-home-foreground">{{ $product->notes['base'] }}</span>
</div>
</div>
</div>
<div class="mb-8">
<label class="block text-sm font-bold text-home-foreground mb-3">Pilih Ukuran</label>
<div class="flex flex-wrap gap-3">
@foreach($product->sizes as $size)
<button @click="selectedSize = '{{ $size }}'"
class="px-6 py-3 rounded-xl border-2 font-bold transition-all duration-200"
:class="selectedSize === '{{ $size }}'
? 'border-home-primary bg-home-primary text-white shadow-lg shadow-home-primary/20'
: 'border-home-foreground/10 text-home-foreground hover:border-home-primary/50'">
{{ $size }}
</button>
@endforeach
</div>
</div>
<div class="flex flex-col sm:flex-row gap-4">
<div class="flex items-center border-2 border-home-foreground/10 rounded-xl px-4 w-full sm:w-auto">
<button @click="qty > 1 ? qty-- : null" class="w-8 h-8 flex items-center justify-center text-home-foreground/50 hover:text-home-primary transition-colors font-bold text-xl">-</button>
<input type="text" x-model="qty" readonly class="w-12 text-center bg-transparent font-bold text-home-foreground border-none focus:ring-0">
<button @click="qty++" class="w-8 h-8 flex items-center justify-center text-home-foreground/50 hover:text-home-primary transition-colors font-bold text-xl">+</button>
</div>
<button class="flex-1 bg-home-primary text-white font-bold py-4 rounded-xl hover:bg-home-secondary hover:text-home-primary transition-all duration-300 shadow-xl shadow-home-primary/20 flex items-center justify-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"></path></svg>
Tambah ke Keranjang
</button>
<button class="w-14 h-full border-2 border-home-foreground/10 rounded-xl flex items-center justify-center text-home-foreground/50 hover:text-red-500 hover:border-red-200 hover:bg-red-50 transition-all">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"></path></svg>
</button>
</div>
<div class="mt-6 flex gap-6 text-xs font-bold text-home-foreground/60 uppercase tracking-widest">
<span class="flex items-center gap-2"><svg class="w-4 h-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg> 100% Original</span>
<span class="flex items-center gap-2"><svg class="w-4 h-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg> Garansi Pengiriman</span>
</div>
</div>
</div>
</section>
<section class="mb-24">
<div class="border-b border-home-foreground/10 mb-8">
<div class="flex gap-8 overflow-x-auto">
<button class="pb-4 border-b-2 border-home-primary text-home-primary font-bold text-lg whitespace-nowrap">Detail Produk</button>
<button class="pb-4 border-b-2 border-transparent text-home-foreground/50 font-medium text-lg hover:text-home-foreground transition-colors whitespace-nowrap">Cara Penggunaan</button>
<button class="pb-4 border-b-2 border-transparent text-home-foreground/50 font-medium text-lg hover:text-home-foreground transition-colors whitespace-nowrap">Ulasan (128)</button>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-12 text-home-foreground/70 leading-relaxed">
<div class="lg:col-span-2 space-y-4">
<p>Oud Royal Premium adalah mahakarya wewangian yang diciptakan untuk mereka yang menghargai kualitas tanpa kompromi. Menggunakan minyak gaharu (oud) langka dari Kalimantan yang difermentasi selama 5 tahun, dipadukan dengan kesegaran Bergamot Italia di top notes.</p>
<p>Jantung aromanya mekar dengan mawar Bulgaria yang dipetik saat fajar, memberikan nuansa floral yang mewah namun tidak berlebihan. Base notes yang hangat dari Amber dan Musk memastikan aroma ini menempel di kulit Anda, menciptakan jejak aroma (sillage) yang tak terlupakan.</p>
<ul class="list-disc pl-5 space-y-2 mt-4">
<li>Konsentrasi: Eau de Parfum (EDP)</li>
<li>Longevity: 8-12 Jam</li>
<li>Projection: 2-3 Meter</li>
<li>Gender: Unisex (Condong ke Maskulin)</li>
</ul>
</div>
<div class="bg-home-foreground/5 p-8 rounded-2xl border border-home-foreground/5 h-fit">
<h4 class="font-bold text-home-foreground mb-4">Pengiriman & Pengembalian</h4>
<p class="text-sm mb-4">Pesanan sebelum jam 14.00 dikirim hari yang sama. Pengiriman aman dengan bubble wrap tebal dan box eksklusif.</p>
<p class="text-sm">Garansi uang kembali 100% jika produk pecah atau tidak original (wajib video unboxing).</p>
</div>
</div>
</section>
<section class="border-t border-home-foreground/10 pt-16 mb-16">
<div class="flex items-center justify-between mb-10">
<h2 class="text-2xl md:text-3xl font-bold text-home-primary">Anda Mungkin Juga Suka</h2>
<div class="flex gap-2">
<button id="slideLeft" class="w-10 h-10 rounded-full border border-home-foreground/10 flex items-center justify-center text-home-foreground hover:bg-home-primary hover:text-white transition-all"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg></button>
<button id="slideRight" class="w-10 h-10 rounded-full border border-home-foreground/10 flex items-center justify-center text-home-foreground hover:bg-home-primary hover:text-white transition-all"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg></button>
</div>
</div>
<div id="productSlider"
class="flex gap-6 overflow-x-auto pb-12 snap-x snap-mandatory scroll-smooth hide-scrollbar"
style="scrollbar-width: none; -ms-overflow-style: none;">
@foreach ($relatedProducts as $rec)
<article
class="min-w-[280px] md:min-w-[300px] bg-white rounded-2xl border border-home-foreground/10 overflow-hidden hover:shadow-xl hover:border-home-primary/30 transition-all duration-300 snap-start group flex flex-col h-full">
<div class="h-60 overflow-hidden relative bg-home-primary/5">
<img src="{{ $rec['image'] }}" alt="{{ $rec['name'] }}"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500">
</div>
<div class="p-5 flex flex-col flex-grow">
<div class="text-[10px] font-bold tracking-widest text-home-primary uppercase mb-2">
{{ $rec['category'] }}</div>
<h3 class="text-lg font-bold text-home-foreground mb-1">{{ $rec['name'] }}</h3>
<div class="mt-auto pt-4 flex items-center justify-between">
<span class="font-bold text-home-primary">Rp
{{ number_format($rec['price'], 0, ',', '.') }}</span>
<button
class="w-8 h-8 rounded bg-home-foreground/5 text-home-foreground hover:bg-home-primary hover:text-white flex items-center justify-center transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 4v16m8-8H4"></path>
</svg>
</button>
</div>
</div>
</article>
@endforeach
</div>
</section>
</main>
</div>
<script>
document.addEventListener("DOMContentLoaded", () => {
const slider = document.getElementById('productSlider');
const leftBtn = document.getElementById('slideLeft');
const rightBtn = document.getElementById('slideRight');
if (slider && leftBtn && rightBtn) {
rightBtn.addEventListener('click', () => {
slider.scrollBy({ left: 320, behavior: 'smooth' });
});
leftBtn.addEventListener('click', () => {
slider.scrollBy({ left: -320, behavior: 'smooth' });
});
}
});
</script>

View File

@ -0,0 +1,226 @@
<div>
{{-- Hero Section --}}
<section class="relative h-[500px] flex items-center justify-center overflow-hidden">
{{-- Background Image --}}
<div class="absolute inset-0 z-0">
<img src="https://images.pexels.com/photos/22589353/pexels-photo-22589353.jpeg" alt="Article Background"
class="w-full h-full object-cover">
<div class="absolute inset-0 bg-black/40"></div>
</div>
{{-- Content --}}
<div class="relative z-10 container mx-auto px-6 text-center">
<h1 class="text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-6">{{ $pageTitle }}</h1>
<p class="text-white/80 text-lg md:text-xl max-w-2xl mx-auto mb-10">
{{ $pageDesc }}
</p>
</div>
</section>
<main class="px-6 lg:px-32 xl:px-48 py-24 bg-white text-black font-sans">
{{-- Product Detail Section --}}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 mb-24">
{{-- Product Image --}}
<div class="space-y-4">
<div
class="aspect-square w-full rounded-2xl overflow-hidden shadow-2xl bg-home-primary/5 border border-home-foreground/10">
<img src="{{ $product->image }}" alt="{{ $product->name }}"
class="w-full h-full object-cover hover:scale-105 transition-transform duration-500">
</div>
</div>
{{-- Product Info --}}
<div class="space-y-6">
{{-- Categories (for perfume) --}}
@if ($productType === 'perfume' && $product->categories->isNotEmpty())
<div class="flex flex-wrap gap-2">
@foreach ($product->categories as $category)
<span
class="text-xs font-bold text-home-primary bg-home-primary/10 px-3 py-1 rounded-full border border-home-primary/10 uppercase tracking-wider">
{{ $category->name }}
</span>
@endforeach
</div>
@endif
{{-- Product Name --}}
<h1 class="text-3xl md:text-4xl lg:text-5xl font-bold text-home-foreground leading-tight">
{{ $product->name }}
</h1>
{{-- Brand (for perfume) --}}
@if ($productType === 'perfume' && $product->brand)
<div class="flex items-center gap-2">
<span class="text-home-foreground/60">Brand:</span>
<span class="font-bold text-home-primary">{{ $product->brand->name }}</span>
</div>
@endif
{{-- Concentration (for perfume) --}}
@if ($productType === 'perfume' && isset($product->concentration))
<div class="flex items-center gap-2">
<span class="text-home-foreground/60">Konsentrasi:</span>
<span class="font-medium text-home-foreground">{{ $product->concentration->label() }}</span>
</div>
@endif
{{-- Price --}}
<div class="py-6 border-y border-home-foreground/10">
<div class="flex items-baseline gap-3">
<span class="text-4xl font-bold text-home-primary">Rp
{{ formatCurrencyNumber($product->sale_price) }}</span>
</div>
</div>
{{-- Notes (for perfume) --}}
@if ($productType === 'perfume')
<div class="space-y-4">
@if ($product->top_notes)
<div class="flex gap-4">
<span class="text-home-foreground/60 font-medium min-w-[120px]">Top Notes:</span>
<span class="text-home-foreground">{{ $product->top_notes }}</span>
</div>
@endif
@if ($product->middle_notes)
<div class="flex gap-4">
<span class="text-home-foreground/60 font-medium min-w-[120px]">Middle Notes:</span>
<span class="text-home-foreground">{{ $product->middle_notes }}</span>
</div>
@endif
@if ($product->base_notes)
<div class="flex gap-4">
<span class="text-home-foreground/60 font-medium min-w-[120px]">Base Notes:</span>
<span class="text-home-foreground">{{ $product->base_notes }}</span>
</div>
@endif
</div>
@endif
{{-- Description --}}
@if ($product->description)
<div class="space-y-3">
<h3 class="text-lg font-bold text-home-foreground">Deskripsi</h3>
<p class="text-home-foreground/70 leading-relaxed text-justify">
{!! $product->description !!}
</p>
</div>
@endif
{{-- Stats --}}
<div class="flex items-center gap-6 pt-4">
<div class="flex items-center gap-2 text-home-foreground/60">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z">
</path>
</svg>
<span class="text-sm">{{ $product->views }}x dilihat</span>
</div>
<div class="flex items-center gap-2 text-home-foreground/60">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span class="text-sm">{{ formatDateLocalized($product->created_at) }}</span>
</div>
</div>
{{-- 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>
{{-- WhatsApp --}}
<a href="https://wa.me/?text={{ urlencode($product->name . ' - ' . url()->current()) }}"
target="_blank" rel="noopener noreferrer"
class="w-10 h-10 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">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path
d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L0 24l6.335-1.662c1.72.94 3.659 1.437 5.634 1.437h.005c6.515 0 11.825-5.309 11.828-11.825a11.823 11.823 0 00-3.414-8.369" />
</svg>
</a>
{{-- Facebook --}}
<a href="https://www.facebook.com/sharer/sharer.php?u={{ urlencode(url()->current()) }}"
target="_blank" rel="noopener noreferrer"
class="w-10 h-10 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">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path
d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" />
</svg>
</a>
{{-- X (Twitter) --}}
<a href="https://twitter.com/intent/tweet?text={{ urlencode($product->name) }}&url={{ urlencode(url()->current()) }}"
target="_blank" rel="noopener noreferrer"
class="w-10 h-10 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">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
</a>
</div>
</div>
</div>
{{-- Related Products --}}
@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>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
@foreach ($relatedProducts as $related)
<article
class="group bg-white rounded-2xl border border-home-foreground/10 overflow-hidden hover:shadow-xl hover:border-home-primary/30 transition-all duration-300 flex flex-col h-full">
<div class="h-60 overflow-hidden relative bg-home-primary/5">
<img src="{{ $related['image'] }}" alt="{{ $related['name'] }}"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500">
<div
class="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-center justify-center">
<a href="{{ route('product.show', $related['slug']) }}" wire:navigate
class="w-10 h-10 bg-white rounded-full flex items-center justify-center text-home-foreground hover:bg-home-primary hover:text-white transition-all shadow-lg transform hover:scale-110">
<svg class="w-5 h-5" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z">
</path>
</svg>
</a>
</div>
</div>
<div class="p-5 flex flex-col flex-grow">
@if ($related['category'])
<div
class="text-[10px] font-bold tracking-widest text-home-primary uppercase mb-2">
{{ $related['category'] }}
</div>
@endif
<h3 class="text-lg font-bold text-home-foreground mb-1 line-clamp-2">
{{ $related['name'] }}
</h3>
<div class="mt-auto pt-4 flex items-center justify-between">
<span class="font-bold text-home-primary">Rp
{{ formatCurrencyNumber($related['price']) }}</span>
</div>
</div>
</article>
@endforeach
</div>
</section>
@endif
</main>
</div>

View File

@ -1,24 +1,24 @@
<div class="flex flex-col min-h-screen bg-white">
<div>
{{-- Hero Section --}}
<section class="relative h-[500px] flex items-center justify-center overflow-hidden">
{{-- Background Image --}}
<div class="absolute inset-0 z-0">
<img src="https://images.pexels.com/photos/5650052/pexels-photo-5650052.jpeg" alt="Voucher Background"
<img src="https://images.pexels.com/photos/5650052/pexels-photo-5650052.jpeg" alt="Perfume Background"
class="w-full h-full object-cover">
<div class="absolute inset-0 bg-black/50"></div>
<div class="absolute inset-0 bg-black/40"></div>
</div>
{{-- Content --}}
<div class="relative z-10 container mx-auto px-6 text-center">
<h1 class="text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-6">Voucher Eksklusif</h1>
<h1 class="text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-6">{{ $pageTitle }}</h1>
<p class="text-white/80 text-lg md:text-xl max-w-2xl mx-auto mb-10">
Nikmati potongan harga spesial untuk koleksi parfum favorit Anda.
{{ $pageDesc }}
</p>
{{-- Search Bar --}}
<div class="max-w-xl mx-auto relative group">
<input type="text" placeholder="Cari kode voucher..." aria-label="Cari voucher"
wire:model.live.debounce.500ms="search"
<input wire:model.live.debounce.500="search" type="search" placeholder="Cari voucher..."
aria-label="Cari artikel"
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
class="absolute right-2 top-2 bottom-2 w-10 h-10 bg-home-primary rounded-full flex items-center justify-center text-white hover:bg-white hover:text-home-primary transition-all duration-300">
@ -31,10 +31,8 @@ class="absolute right-2 top-2 bottom-2 w-10 h-10 bg-home-primary rounded-full fl
</div>
</section>
<main class="flex-grow px-6 lg:px-32 xl:px-48 py-24 font-sans flex flex-col">
<section class="flex flex-col lg:flex-row justify-center gap-4 mb-12" aria-label="Filter">
<main class="px-6 lg:px-32 xl:px-48 py-24 bg-white text-black font-sans">
<div class="flex flex-col lg:flex-row justify-center gap-4 mb-12" aria-label="Filter">
<div class="flex flex-col sm:flex-row gap-4">
<div class="relative">
<select wire:model.live="outlet_id" aria-label="Filter Outlet"
@ -64,123 +62,145 @@ class="appearance-none px-6 py-3 pr-10 rounded-xl border border-home-foreground/
</svg>
</div>
</div>
</section>
</div>
<section class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 mb-12">
@foreach ($vouchers as $voucher)
<article
class="bg-white rounded-2xl border border-home-foreground/10 overflow-hidden hover:shadow-xl hover:shadow-home-primary/5 hover:border-home-primary/30 hover:-translate-y-1 transition-all duration-300 group flex flex-col h-full">
@if ($vouchers->isNotEmpty())
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 mb-12">
@foreach ($vouchers as $voucher)
<article
class="bg-white rounded-2xl border border-home-foreground/10 overflow-hidden hover:shadow-xl hover:shadow-home-primary/5 hover:border-home-primary/30 hover:-translate-y-1 transition-all duration-300 group flex flex-col h-full">
<div class="bg-home-primary/5 p-6 relative overflow-hidden flex-1">
<div
class="absolute top-0 right-0 w-24 h-24 bg-home-primary/10 rounded-full blur-2xl -mr-10 -mt-10">
</div>
<div class="relative z-10">
<div class="bg-home-primary/5 p-6 relative overflow-hidden flex-1">
<div
class="inline-flex items-center gap-2 bg-white px-3 py-1 rounded-full border border-home-primary/10 shadow-sm mb-4">
<span class="w-2 h-2 rounded-full bg-home-primary"></span>
<span
class="text-xs font-bold text-home-primary uppercase tracking-wider">{{ $voucher->tags }}</span>
class="absolute top-0 right-0 w-24 h-24 bg-home-primary/10 rounded-full blur-2xl -mr-10 -mt-10">
</div>
<h3
class="text-xl font-bold text-home-foreground mb-2 group-hover:text-home-primary transition-colors line-clamp-2">
{{ $voucher->name }}
</h3>
<p class="text-sm text-home-foreground/60 mb-6 line-clamp-2 leading-relaxed">
{{ $voucher->summary }}
</p>
<div
class="bg-white rounded-xl p-4 border border-dashed border-home-primary/30 flex items-center justify-between group-hover:border-home-primary transition-colors">
<div>
<p
class="text-[10px] text-home-foreground/50 uppercase tracking-widest font-bold mb-1">
Kode Voucher</p>
<p class="text-lg font-bold text-home-primary font-mono tracking-widest select-all">
{{ $voucher->code }}
</p>
</div>
<div class="relative z-10">
<div
class="w-8 h-8 rounded-full bg-home-primary/10 flex items-center justify-center text-home-primary">
class="inline-flex items-center gap-2 bg-white px-3 py-1 rounded-full border border-home-primary/10 shadow-sm mb-4">
<span class="w-2 h-2 rounded-full bg-home-primary"></span>
<span
class="text-xs font-bold text-home-primary uppercase tracking-wider">{{ $voucher->tags }}</span>
</div>
<h3
class="text-xl font-bold text-home-foreground mb-2 group-hover:text-home-primary transition-colors line-clamp-2">
{{ $voucher->name }}
</h3>
<p class="text-sm text-home-foreground/60 mb-6 line-clamp-2 leading-relaxed">
{{ $voucher->summary }}
</p>
<div class="bg-white rounded-xl p-4 border border-dashed border-home-primary/30 flex items-center justify-between cursor-pointer hover:border-home-primary transition-colors"
x-data="{ textToCopy: '{{ $voucher->code }}', copied: false }"
@click="
navigator.clipboard.writeText(textToCopy);
copied = true;
setTimeout(() => copied = false, 1500);
">
<div>
<p
class="text-[10px] text-home-foreground/50 uppercase tracking-widest font-bold mb-1">
Kode Voucher
</p>
<p
class="text-lg font-bold text-home-primary font-mono tracking-widest select-all">
{{ $voucher->code }}
</p>
</div>
<div
class="w-8 h-8 rounded-full bg-home-primary/10 flex items-center justify-center text-home-primary">
<template x-if="!copied">
<svg class="w-4 h-4" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</template>
<template x-if="copied">
<svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M5 13l4 4L19 7" />
</svg>
</template>
</div>
</div>
</div>
</div>
<div class="px-6 pt-4 pb-0 space-y-3">
<div
class="flex justify-between items-center text-sm border-b border-home-foreground/5 pb-3">
<span class="text-home-foreground/50 flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z">
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2-1.343-2-3-2zm0 4c-2.485 0-4.5 1.567-4.5 3.5V18h9v-2.5c0-1.933-2.015-3.5-4.5-3.5z">
</path>
</svg>
</div>
Min. Pembelian
</span>
<span
class="font-bold text-home-foreground">{{ formatCurrencyNumber($voucher->min_purchase, 'Rp') }}</span>
</div>
<div
class="flex justify-between items-center text-sm border-b border-home-foreground/5 pb-3">
<span class="text-home-foreground/50 flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z">
</path>
</svg>
Berlaku Hingga
</span>
<time datetime="{{ formatDateLocalized($voucher->end_date) }}"
class="font-bold text-home-foreground">{{ formatDateLocalized($voucher->end_date) }}</time>
</div>
<div class="flex justify-between items-center text-sm">
<span class="text-home-foreground/50 flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M20 12H4"></path>
</svg>
Sisa Kuota
</span>
<span
class="font-bold text-home-primary bg-home-primary/10 px-2 py-0.5 rounded text-xs">
{{ $voucher->available_count }} / {{ $voucher->quota ?? '∞' }}
</span>
</div>
</div>
</div>
<div class="px-6 pt-4 pb-0 space-y-3">
<div class="flex justify-between items-center text-sm border-b border-home-foreground/5 pb-3">
<span class="text-home-foreground/50 flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3-.895 3-2-1.343-2-3-2zm0 4c-2.485 0-4.5 1.567-4.5 3.5V18h9v-2.5c0-1.933-2.015-3.5-4.5-3.5z">
</path>
</svg>
Min. Pembelian
</span>
<span class="font-bold text-home-foreground">{{ $voucher->min_purchase_formatted }}</span>
<div class="p-6 flex gap-3 mt-auto">
<button wire:click="claim('{{ $voucher->hash }}')"
class="flex-1 bg-home-primary text-white font-bold py-3 rounded-xl hover:bg-home-secondary hover:text-home-primary hover:shadow-lg hover:shadow-home-primary/20 transition-all duration-300 text-sm shadow-md shadow-home-primary/10">
Klaim Voucher
</button>
<button wire:click="showDetail('{{ $voucher->hash }}')"
class="flex-1 border border-home-foreground/20 text-home-foreground font-bold py-3 rounded-xl hover:border-home-primary hover:text-home-primary hover:bg-home-primary/5 transition-all duration-300 text-sm">
Lihat Detail
</button>
</div>
<div class="flex justify-between items-center text-sm border-b border-home-foreground/5 pb-3">
<span class="text-home-foreground/50 flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z">
</path>
</svg>
Berlaku Hingga
</span>
<time datetime="{{ $voucher->end_date }}"
class="font-bold text-home-foreground">{{ $voucher->end_date }}</time>
</div>
<div class="flex justify-between items-center text-sm">
<span class="text-home-foreground/50 flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M20 12H4"></path>
</svg>
Sisa Kuota
</span>
<span class="font-bold text-home-primary bg-home-primary/10 px-2 py-0.5 rounded text-xs">
{{ $voucher->available_count }} / {{ $voucher->quota ?? '∞' }}
</span>
</div>
</div>
</article>
@endforeach
</div>
<div class="p-6 flex gap-3 mt-auto">
<button wire:click="claim('{{ $voucher->hash }}')"
class="flex-1 bg-home-primary text-white font-bold py-3 rounded-xl hover:bg-home-secondary hover:text-home-primary hover:shadow-lg hover:shadow-home-primary/20 transition-all duration-300 text-sm shadow-md shadow-home-primary/10">
Klaim Voucher
</button>
<button wire:click="showDetail('{{ $voucher->hash }}')"
class="flex-1 border border-home-foreground/20 text-home-foreground font-bold py-3 rounded-xl hover:border-home-primary hover:text-home-primary hover:bg-home-primary/5 transition-all duration-300 text-sm">
Lihat Detail
</button>
</div>
</article>
@endforeach
</section>
<div class="mt-auto flex justify-center w-full">
{{ $vouchers->links('vendor.livewire.custom-pagination') }}
</div>
<div class="flex justify-center w-full">
{{ $vouchers->links('vendor.livewire.custom-pagination') }}
</div>
@else
@include('components.animations.lottie.not-found')
@endif
</main>
{{-- Voucher Detail Modal --}}
@if ($showDetailModal && $selectedVoucher)
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
{{-- Backdrop --}}
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm" wire:click="closeDetail"></div>
{{-- Modal Content --}}
<div
class="relative bg-white w-full max-w-2xl rounded-3xl overflow-hidden shadow-2xl animate-in fade-in zoom-in duration-300">
{{-- Modal Header --}}
<div class="relative h-48 bg-home-primary flex items-center justify-center overflow-hidden">
<div class="absolute inset-0 opacity-20">
<div class="absolute top-0 right-0 w-64 h-64 bg-white rounded-full -mr-32 -mt-32"></div>
@ -203,7 +223,6 @@ class="absolute top-6 right-6 w-10 h-10 rounded-full bg-white/10 hover:bg-white/
</button>
</div>
{{-- Modal Body --}}
<div class="p-8 max-h-[60vh] overflow-y-auto custom-scrollbar">
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8">
<div>
@ -224,7 +243,7 @@ class="w-8 h-8 rounded-lg bg-home-primary/5 flex items-center justify-center tex
<div>
<p class="text-sm font-medium text-home-foreground/50">Minimum Pembelian</p>
<p class="font-bold text-home-foreground">
{{ $selectedVoucher->min_purchase_formatted }}</p>
{{ formatCurrencyNumber($selectedVoucher->min_purchase, 'Rp') }}</p>
</div>
</div>
<div class="flex items-start gap-3">
@ -240,7 +259,7 @@ class="w-8 h-8 rounded-lg bg-home-primary/5 flex items-center justify-center tex
<div>
<p class="text-sm font-medium text-home-foreground/50">Masa Berlaku</p>
<p class="font-bold text-home-foreground">Hingga
{{ $selectedVoucher->end_date_formatted }}</p>
{{ formatDateLocalized($selectedVoucher->end_date) }}</p>
</div>
</div>
@if ($selectedVoucher->limit_per_user)
@ -266,7 +285,8 @@ class="w-8 h-8 rounded-lg bg-home-primary/5 flex items-center justify-center tex
</div>
<div>
<h4 class="text-sm font-bold text-home-foreground/40 uppercase tracking-wider mb-4">Outlet
<h4 class="text-sm font-bold text-home-foreground/40 uppercase tracking-wider mb-4">
Outlet
Tersedia
</h4>
<div class="flex flex-wrap gap-2">
@ -291,6 +311,27 @@ class="px-4 py-2 bg-home-foreground/5 border border-home-foreground/10 rounded-x
@endforelse
</div>
</div>
<div>
<h4 class="text-sm font-bold text-home-foreground/40 uppercase tracking-wider mb-4">
Tier
</h4>
<div class="flex flex-wrap gap-2">
@forelse ($selectedVoucher->tiers as $tier)
<span
class="px-4 py-2 bg-home-primary/5 border border-home-primary/10 rounded-xl text-sm font-semibold text-home-primary flex items-center gap-2">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z">
</path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
{{ $tier->name }}
</span>
@endforeach
</div>
</div>
</div>
<div class="bg-home-foreground/5 rounded-2xl p-6">
@ -302,13 +343,12 @@ class="px-4 py-2 bg-home-foreground/5 border border-home-foreground/10 rounded-x
</div>
</div>
{{-- Modal Footer --}}
<div class="p-8 bg-white border-t border-home-foreground/5 flex gap-4">
<button wire:click="closeDetail"
class="flex-1 py-4 px-6 border border-home-foreground/20 text-home-foreground font-bold rounded-2xl hover:bg-home-foreground/5 transition-all">
Tutup
</button>
<button wire:click="claim('{{ $selectedVoucher->hashId }}')"
<button wire:click="claim('{{ $selectedVoucher->hash }}')"
class="flex-[2] py-4 px-6 bg-home-primary text-white font-bold rounded-2xl hover:bg-home-secondary hover:text-home-primary hover:shadow-xl hover:shadow-home-primary/20 transition-all shadow-lg shadow-home-primary/10">
Klaim Sekarang
</button>
@ -317,3 +357,7 @@ class="flex-[2] py-4 px-6 bg-home-primary text-white font-bold rounded-2xl hover
</div>
@endif
</div>
@assets
<script src="https://unpkg.com/@lottiefiles/dotlottie-wc@0.8.5/dist/dotlottie-wc.js" type="module"></script>
@endassets

View File

@ -5,7 +5,7 @@
</div>
<div class="flex gap-2">
@can('view brand')
<a href="{{ route('studio.export.brands') }}" class="inline-flex">
<a href="{{ route('studio.export.brand') }}" class="inline-flex">
<flux:button variant="primary" color="zinc" class="text-sm">
Ekspor Ecxel
</flux:button>

View File

@ -48,7 +48,7 @@
<flux:label>Icon <span class="text-red-500 ms-1">*</span></flux:label>
<flux:description>Lihat dan copy icon nya di <a href="https://heroicons.com/" class="text-blue-500"
target="_blank">Heroicons</a></flux:description>
<flux:input placeholder="tag" wire:model="form.icon" autofocus autocomplete="off" />
<flux:input placeholder="..." wire:model="form.icon" autofocus autocomplete="off" />
<flux:error name="form.icon" />
</flux:field>

View File

@ -54,9 +54,14 @@
<flux:input label="Kuota" placeholder="100" wire:model="form.quota"
x-mask:dynamic="$money($input, ',')" autocomplete="off" />
<flux:input label="Batas Per Customer" placeholder="1"
wire:model="form.limit_per_user" x-mask:dynamic="$money($input, ',')"
autocomplete="off" />
<flux:field>
<flux:label>Batas Per Customer <span class="text-red-500 ms-1">*</span>
</flux:label>
<flux:input placeholder="1" wire:model="form.limit_per_user"
x-mask:dynamic="$money($input, ',')" autocomplete="off" />
<flux:error name="form.limit_per_user" />
</flux:field>
</div>
</div>

View File

@ -1,11 +1,14 @@
<?php
use App\Http\Controllers\ProfileRedirectController;
use App\Livewire\Home\Article\Index as ArticleIndex;
use App\Livewire\Home\Article\Show as ArticleShow;
use App\Livewire\Home\Cart\Index as CartComponent;
use App\Livewire\Home\Faq\Index as FaqComponent;
use App\Livewire\Home\Faq as FaqComponent;
use App\Livewire\Home\Index as HomeComponent;
use App\Livewire\Home\Outlet as OutletComponent;
use App\Livewire\Home\Product\Index as ProductIndex;
use App\Livewire\Home\Product\Show as ProductShow;
use App\Livewire\Home\Voucher as VoucherComponent;
use Illuminate\Support\Facades\Route;
@ -13,19 +16,24 @@
Route::get('/outlets', OutletComponent::class)->name('outlet');
Route::get('/products', ProductIndex::class)->name('product.index');
Route::prefix('products')
->name('product.')
->group(function () {
Route::get('/', ProductIndex::class)->name('index');
Route::get('/{slug}', ProductShow::class)->name('show');
});
Route::get('/vouchers', VoucherComponent::class)->name('voucher');
Route::prefix('articles')
->name('article.')
->group(function () {
Route::get('{slug?}', ArticleIndex::class)
->name('index');
Route::get('/', ArticleIndex::class)->name('index');
Route::get('/{article:slug}', ArticleShow::class)->name('show');
});
Route::get('/faq', FaqComponent::class)->name('faq');
Route::get('/faqs', FaqComponent::class)->name('faq');
Route::get('/cart', CartComponent::class)->name('cart');
Route::get('/profile/redirect', \App\Http\Controllers\ProfileRedirectController::class)->name('profile.redirect');
Route::get('/profile/redirect', ProfileRedirectController::class)->name('profile.redirect');