84 lines
5.5 KiB
HTML
84 lines
5.5 KiB
HTML
{# Shared storefront search box + results region, used by the shop index and
|
|
every category page. One form drives the listing: htmx re-runs /search and
|
|
swaps only #shop-results; the toolbar keeps its own DOM state. Triggers: live
|
|
(debounced) typing in the search box, immediate on a sort change, and submit
|
|
(Enter). Degrades to a plain GET form without JS.
|
|
Category is chosen from the sidebar (carried here as a hidden field so it
|
|
survives a search / re-sort). The grid/list view toggle lives next to sort;
|
|
its `view` state is held in Alpine on this wrapper so both the toggle and the
|
|
swapped-in product grid (and `_card.html`) share it.
|
|
Expects: query, selected_category, sort, plus the result vars consumed by
|
|
_results.html. #}
|
|
{% set L = lang | default(value='sk') %}
|
|
<div x-data="{ view: localStorage.getItem('shopView') === 'list' ? 'list' : 'grid' }"
|
|
x-init="$watch('view', v => localStorage.setItem('shopView', v))"
|
|
class="space-y-6">
|
|
<form action="/search" method="get" role="search"
|
|
hx-get="/search" hx-target="#shop-results" hx-swap="innerHTML"
|
|
hx-push-url="true" hx-indicator="#search-spinner"
|
|
hx-trigger="submit, change, keyup changed delay:350ms from:input[name='q']"
|
|
class="space-y-3">
|
|
|
|
{# Category comes from the sidebar; keep it on the query so searching /
|
|
re-sorting stays within the active category. #}
|
|
<input type="hidden" name="category" value="{{ selected_category | default(value='all') }}" />
|
|
|
|
<!-- search box -->
|
|
<div class="relative max-w-xl">
|
|
<span class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-on-surface/50 dark:text-on-surface-dark/50">
|
|
{{ ui::icon(name="search", size="size-5") }}
|
|
</span>
|
|
<input type="search" name="q" value="{{ query | default(value='') }}" autocomplete="off"
|
|
placeholder="{{ t(key='search-placeholder', lang=L) }}"
|
|
aria-label="{{ t(key='search-placeholder', lang=L) }}"
|
|
class="w-full rounded-radius border border-outline bg-surface py-2 pl-10 pr-10 text-sm text-on-surface placeholder:text-on-surface/50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark dark:placeholder:text-on-surface-dark/50 dark:focus-visible:outline-primary-dark" />
|
|
<span id="search-spinner" class="htmx-indicator pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 text-on-surface/50 dark:text-on-surface-dark/50">
|
|
<svg class="size-4 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 0 1 8-8V0C5.4 0 0 5.4 0 12h4Z"></path>
|
|
</svg>
|
|
</span>
|
|
</div>
|
|
|
|
<!-- sort + product card style switch -->
|
|
<div class="flex flex-wrap items-center justify-end gap-3">
|
|
<label class="flex items-center gap-2 text-xs font-medium text-on-surface/70 dark:text-on-surface-dark/70">
|
|
{{ t(key="sort-label", lang=L) }}
|
|
<select name="sort"
|
|
class="rounded-radius border border-outline bg-surface px-2 py-1.5 text-sm text-on-surface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
|
|
{% for opt in ["newest", "relevance", "price_asc", "price_desc", "name_asc", "name_desc"] %}
|
|
<option value="{{ opt }}"{% if sort == opt %} selected{% endif %}>{{ t(key="sort-" ~ opt, lang=L) }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</label>
|
|
|
|
<!-- grid / list view toggle -->
|
|
<div class="inline-flex gap-0.5 rounded-radius border border-outline p-0.5 dark:border-outline-dark" role="group"
|
|
aria-label="{{ t(key='view-grid', lang=L) }} / {{ t(key='view-list', lang=L) }}">
|
|
<button type="button" @click="view = 'grid'" :aria-pressed="view === 'grid'"
|
|
class="inline-flex size-8 items-center justify-center rounded-radius transition"
|
|
:class="view === 'grid' ? 'bg-primary text-on-primary dark:bg-primary-dark dark:text-on-primary-dark' : 'text-on-surface hover:text-primary dark:text-on-surface-dark dark:hover:text-primary-dark'"
|
|
aria-label="{{ t(key='view-grid', lang=L) }}"
|
|
title="{{ t(key='view-grid', lang=L) }}">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="size-4">
|
|
<path d="M3 3h6v6H3V3Zm8 0h6v6h-6V3ZM3 11h6v6H3v-6Zm8 0h6v6h-6v-6Z" />
|
|
</svg>
|
|
</button>
|
|
<button type="button" @click="view = 'list'" :aria-pressed="view === 'list'"
|
|
class="inline-flex size-8 items-center justify-center rounded-radius transition"
|
|
:class="view === 'list' ? 'bg-primary text-on-primary dark:bg-primary-dark dark:text-on-primary-dark' : 'text-on-surface hover:text-primary dark:text-on-surface-dark dark:hover:text-primary-dark'"
|
|
aria-label="{{ t(key='view-list', lang=L) }}"
|
|
title="{{ t(key='view-list', lang=L) }}">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="size-4">
|
|
<path d="M3 4h14v2.5H3V4Zm0 4.75h14v2.5H3v-2.5ZM3 13.5h14V16H3v-2.5Z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
<div id="shop-results">
|
|
{% include "shop/_results.html" %}
|
|
</div>
|
|
</div>
|