product category page: front-end sorting and filtering using turboframes

This commit is contained in:
elvira
2026-08-31 22:56:40 +03:00
parent b070a7d1e6
commit 8a07c772f8
20 changed files with 428 additions and 148 deletions
+20 -102
View File
@@ -3,6 +3,16 @@
@section('title', $collection['name'] . ' — ' . config('app.name'))
@section('description', strip_tags($collection['description'] ?? ''))
@push('seo')
{{-- Filtered / sorted / paged variants all consolidate onto the bare
category URL; the noindex keeps the near-duplicate variants out of the
index while still letting crawlers follow through to the products. --}}
<link rel="canonical" href="{{ route('category.show', ['id' => $collection['id']]) }}">
@if($listing->isRefined())
<meta name="robots" content="noindex,follow">
@endif
@endpush
@section('content')
<div class="max-w-5xl mx-auto pt-26 pb-12">
@@ -15,108 +25,16 @@
]" />
</div>
<div class="grid grid-cols-1 lg:grid-cols-[1fr_230px] gap-14">
<div class="flex items-center justify-between gap-6 flex-wrap mb-7">
{{-- <p class="text-neutral-600"> --}}
<p>
{{ trans_choice('storefront.shop.showing_results', $products->total(), [
'first' => $products->firstItem() ?? 0,
'last' => $products->lastItem() ?? 0,
'total' => $products->total(),
]) }}
</p>
{{-- Dummy — not wired to real sorting yet. Options carry data-action
for a future `category` Stimulus controller; no-op until that
controller exists and a [data-controller="category"] wraps this. --}}
<x-ui.dropdown
id="category-sort"
:label="__('storefront.shop.sort_popularity')"
:ariaLabel="__('storefront.shop.sort_label')"
triggerClass="min-w-48"
>
@foreach ([
'popularity' => __('storefront.shop.sort_popularity'),
'price-asc' => __('storefront.shop.sort_price_asc'),
'price-desc' => __('storefront.shop.sort_price_desc'),
'newest' => __('storefront.shop.sort_newest'),
] as $value => $label)
<x-ui.dropdown.item
data-action="category#sort"
data-category-sort-param="{{ $value }}"
:current="$value === 'popularity'"
>{{ $label }}</x-ui.dropdown.item>
@endforeach
</x-ui.dropdown>
</div>
<div></div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-[1fr_230px] gap-14">
{{-- Products --}}
<div>
@if($products->isEmpty())
<p class="text-neutral-500">{{ __('storefront.shop.no_products') }}</p>
@else
<x-product-grid :products="$products->items()" cols="3" />
<div class="mt-12">
<x-ui.pagination :paginator="$products" />
</div>
@endif
</div>
{{-- Sidebar — search, price and availability filters are dummy for now --}}
<aside class="flex flex-col gap-10">
<div class="-mt-2">
<label for="shop-search" class="sr-only">{{ __('storefront.shop.search_label') }}</label>
<div class="relative">
<x-ui.input
type="search"
id="shop-search"
:placeholder="__('storefront.shop.search_placeholder')"
class="pr-10"
/>
<button
type="button"
class="absolute right-0 top-1/2 -translate-y-1/2"
aria-label="{{ __('storefront.shop.search_label') }}"
>
<x-ui.icon name="search" :size="20" />
</button>
</div>
</div>
<div class="flex flex-col gap-4">
<p class="font-extrabold text-h4">{{ __('storefront.shop.filter_price') }}</p>
{{-- Dummy — not wired to real price bounds or filtering yet. --}}
<x-ui.range-slider
:min="10"
:max="50"
prefix="€"
separator=" - "
:legend="__('storefront.shop.filter_price')"
:min-label="__('storefront.shop.price_min')"
:max-label="__('storefront.shop.price_max')"
:reset-label="__('storefront.shop.reset')"
/>
</div>
<div class="flex flex-col gap-4 [&_label]:text-base">
<p class="font-extrabold text-h4">{{ __('storefront.shop.availability') }}</p>
<x-ui.checkbox id="shop-in-stock" name="in_stock">
{{ __('storefront.shop.in_stock_only') }}
</x-ui.checkbox>
</div>
</aside>
</div>
{{-- Everything that reflects sort / filter / page state lives in this
frame. A sort link or filter submit inside it navigates the frame;
the controller re-renders it straight from the query string, and
data-turbo-action="advance" keeps the address bar in sync so a
refresh or bookmark reproduces the exact same view. With no JS the
frame is just a block and the links do full-page navigations to the
same URLs. --}}
<turbo-frame id="category-listing" data-turbo-action="advance">
@include('category.partials.listing')
</turbo-frame>
</div>
@endsection