saerch query in the shop now works well
This commit is contained in:
102
assets/views/shop/_search.html
Normal file
102
assets/views/shop/_search.html
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
{# Shared storefront search + filter toolbar and results region, used by the shop
|
||||||
|
index and every category page. One form drives the whole 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 any
|
||||||
|
select/checkbox change, and submit (Enter / Apply) for the price band. Degrades
|
||||||
|
to a plain GET form without JS.
|
||||||
|
Expects: query, category_groups, selected_category, selected_category_id,
|
||||||
|
uncategorized_count, sort, min_price, max_price, price_floor, price_ceil,
|
||||||
|
in_stock, plus the result vars consumed by _results.html. #}
|
||||||
|
{% set L = lang | default(value='sk') %}
|
||||||
|
<div 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">
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- filter toolbar -->
|
||||||
|
<div class="flex flex-wrap items-end gap-3 rounded-radius border border-outline bg-surface-alt p-3 dark:border-outline-dark dark:bg-surface-dark-alt">
|
||||||
|
<!-- category -->
|
||||||
|
<label class="flex flex-col gap-1 text-xs font-medium text-on-surface/70 dark:text-on-surface-dark/70">
|
||||||
|
{{ t(key="filter-category", lang=L) }}
|
||||||
|
<select name="category"
|
||||||
|
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">
|
||||||
|
<option value="all"{% if selected_category == "all" %} selected{% endif %}>{{ t(key="filter-all-categories", lang=L) }}</option>
|
||||||
|
{% for g in category_groups %}
|
||||||
|
<option value="{{ g.id }}"{% if selected_category_id == g.id %} selected{% endif %}>{{ g.name }} ({{ g.count }})</option>
|
||||||
|
{% for ch in g.children %}
|
||||||
|
<option value="{{ ch.id }}"{% if selected_category_id == ch.id %} selected{% endif %}> — {{ ch.name }} ({{ ch.count }})</option>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if uncategorized_count > 0 %}
|
||||||
|
<option value="none"{% if selected_category == "none" %} selected{% endif %}>{{ t(key="filter-uncategorized", lang=L) }} ({{ uncategorized_count }})</option>
|
||||||
|
{% endif %}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!-- sort -->
|
||||||
|
<label class="flex flex-col gap-1 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 ["relevance", "newest", "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>
|
||||||
|
|
||||||
|
<!-- price band -->
|
||||||
|
<label class="flex flex-col gap-1 text-xs font-medium text-on-surface/70 dark:text-on-surface-dark/70">
|
||||||
|
{{ t(key="filter-price", lang=L) }}
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<input type="number" name="min_price" min="0" step="0.01" inputmode="decimal"
|
||||||
|
value="{{ min_price | default(value='') }}" placeholder="{{ price_floor }}"
|
||||||
|
aria-label="{{ t(key='filter-price-from', lang=L) }}"
|
||||||
|
class="w-24 rounded-radius border border-outline bg-surface px-2 py-1.5 text-sm text-on-surface dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark" />
|
||||||
|
<span class="text-on-surface/50 dark:text-on-surface-dark/50">–</span>
|
||||||
|
<input type="number" name="max_price" min="0" step="0.01" inputmode="decimal"
|
||||||
|
value="{{ max_price | default(value='') }}" placeholder="{{ price_ceil }}"
|
||||||
|
aria-label="{{ t(key='filter-price-to', lang=L) }}"
|
||||||
|
class="w-24 rounded-radius border border-outline bg-surface px-2 py-1.5 text-sm text-on-surface dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark" />
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!-- in stock -->
|
||||||
|
<label class="flex items-center gap-2 pb-1.5 text-sm text-on-surface dark:text-on-surface-dark">
|
||||||
|
<input type="checkbox" name="in_stock" value="1"{% if in_stock %} checked{% endif %}
|
||||||
|
class="size-4 rounded border-outline text-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary dark:border-outline-dark dark:text-primary-dark" />
|
||||||
|
{{ t(key="filter-in-stock", lang=L) }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="ml-auto flex items-end gap-2">
|
||||||
|
{{ ui::button(label=t(key="filter-apply", lang=L), type="submit", variant="secondary") }}
|
||||||
|
<a href="/shop" hx-get="/search" hx-target="#shop-results" hx-push-url="true"
|
||||||
|
class="self-end pb-1.5 text-sm font-medium text-on-surface/70 underline-offset-2 transition hover:text-primary hover:underline dark:text-on-surface-dark/70 dark:hover:text-primary-dark">
|
||||||
|
{{ t(key="filter-clear", lang=L) }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="shop-results">
|
||||||
|
{% include "shop/_results.html" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -4,10 +4,11 @@
|
|||||||
{% block title %}{{ category.name }}{% endblock title %}
|
{% block title %}{{ category.name }}{% endblock title %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="space-y-8">
|
{% set L = lang | default(value='sk') %}
|
||||||
|
<div class="space-y-6">
|
||||||
<header class="space-y-2">
|
<header class="space-y-2">
|
||||||
<nav class="text-sm text-on-surface/60 dark:text-on-surface-dark/60">
|
<nav class="text-sm text-on-surface/60 dark:text-on-surface-dark/60">
|
||||||
<a href="/shop" class="hover:text-primary dark:hover:text-primary-dark">{{ t(key="nav-shop", lang=lang | default(value='sk')) }}</a>
|
<a href="/shop" class="hover:text-primary dark:hover:text-primary-dark">{{ t(key="nav-shop", lang=L) }}</a>
|
||||||
{% for crumb in breadcrumbs %}
|
{% for crumb in breadcrumbs %}
|
||||||
<span class="px-1">/</span>
|
<span class="px-1">/</span>
|
||||||
<a href="/category/{{ crumb.slug }}" class="hover:text-primary dark:hover:text-primary-dark">{{ crumb.name }}</a>
|
<a href="/category/{{ crumb.slug }}" class="hover:text-primary dark:hover:text-primary-dark">{{ crumb.name }}</a>
|
||||||
@@ -28,12 +29,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{% if products | length > 0 %}
|
{# Same search + filters as the shop, with this category preselected. #}
|
||||||
{% include "shop/_product_grid.html" %}
|
{% include "shop/_search.html" %}
|
||||||
{% else %}
|
|
||||||
<div class="rounded-radius border border-outline px-6 py-16 text-center text-on-surface/70 dark:border-outline-dark dark:text-on-surface-dark/70">
|
|
||||||
{{ t(key="shop-empty", lang=lang | default(value='sk')) }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|||||||
@@ -11,99 +11,6 @@
|
|||||||
<p class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="shop-subtitle", lang=L) }}</p>
|
<p class="text-on-surface/70 dark:text-on-surface-dark/70">{{ t(key="shop-subtitle", lang=L) }}</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{# One form drives the whole listing. htmx re-runs /search and swaps only the
|
{% include "shop/_search.html" %}
|
||||||
results region; the toolbar keeps its own DOM state. Triggers: live (debounced)
|
|
||||||
typing in the search box, immediate on any select/checkbox change, and submit
|
|
||||||
(Enter / Apply) for the price band. Degrades to a plain GET form without JS. #}
|
|
||||||
<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">
|
|
||||||
|
|
||||||
<!-- 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>
|
|
||||||
|
|
||||||
<!-- filter toolbar -->
|
|
||||||
<div class="flex flex-wrap items-end gap-3 rounded-radius border border-outline bg-surface-alt p-3 dark:border-outline-dark dark:bg-surface-dark-alt">
|
|
||||||
<!-- category -->
|
|
||||||
<label class="flex flex-col gap-1 text-xs font-medium text-on-surface/70 dark:text-on-surface-dark/70">
|
|
||||||
{{ t(key="filter-category", lang=L) }}
|
|
||||||
<select name="category"
|
|
||||||
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">
|
|
||||||
<option value="all"{% if selected_category == "all" %} selected{% endif %}>{{ t(key="filter-all-categories", lang=L) }}</option>
|
|
||||||
{% for g in category_groups %}
|
|
||||||
<option value="{{ g.id }}"{% if selected_category_id == g.id %} selected{% endif %}>{{ g.name }} ({{ g.count }})</option>
|
|
||||||
{% for ch in g.children %}
|
|
||||||
<option value="{{ ch.id }}"{% if selected_category_id == ch.id %} selected{% endif %}> — {{ ch.name }} ({{ ch.count }})</option>
|
|
||||||
{% endfor %}
|
|
||||||
{% endfor %}
|
|
||||||
{% if uncategorized_count > 0 %}
|
|
||||||
<option value="none"{% if selected_category == "none" %} selected{% endif %}>{{ t(key="filter-uncategorized", lang=L) }} ({{ uncategorized_count }})</option>
|
|
||||||
{% endif %}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<!-- sort -->
|
|
||||||
<label class="flex flex-col gap-1 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 ["relevance", "newest", "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>
|
|
||||||
|
|
||||||
<!-- price band -->
|
|
||||||
<label class="flex flex-col gap-1 text-xs font-medium text-on-surface/70 dark:text-on-surface-dark/70">
|
|
||||||
{{ t(key="filter-price", lang=L) }}
|
|
||||||
<span class="flex items-center gap-1">
|
|
||||||
<input type="number" name="min_price" min="0" step="0.01" inputmode="decimal"
|
|
||||||
value="{{ min_price | default(value='') }}" placeholder="{{ price_floor }}"
|
|
||||||
aria-label="{{ t(key='filter-price-from', lang=L) }}"
|
|
||||||
class="w-24 rounded-radius border border-outline bg-surface px-2 py-1.5 text-sm text-on-surface dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark" />
|
|
||||||
<span class="text-on-surface/50 dark:text-on-surface-dark/50">–</span>
|
|
||||||
<input type="number" name="max_price" min="0" step="0.01" inputmode="decimal"
|
|
||||||
value="{{ max_price | default(value='') }}" placeholder="{{ price_ceil }}"
|
|
||||||
aria-label="{{ t(key='filter-price-to', lang=L) }}"
|
|
||||||
class="w-24 rounded-radius border border-outline bg-surface px-2 py-1.5 text-sm text-on-surface dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark" />
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<!-- in stock -->
|
|
||||||
<label class="flex items-center gap-2 pb-1.5 text-sm text-on-surface dark:text-on-surface-dark">
|
|
||||||
<input type="checkbox" name="in_stock" value="1"{% if in_stock %} checked{% endif %}
|
|
||||||
class="size-4 rounded border-outline text-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary dark:border-outline-dark dark:text-primary-dark" />
|
|
||||||
{{ t(key="filter-in-stock", lang=L) }}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="ml-auto flex items-end gap-2">
|
|
||||||
{{ ui::button(label=t(key="filter-apply", lang=L), type="submit", variant="secondary") }}
|
|
||||||
<a href="/shop" hx-get="/search" hx-target="#shop-results" hx-push-url="true"
|
|
||||||
class="self-end pb-1.5 text-sm font-medium text-on-surface/70 underline-offset-2 transition hover:text-primary hover:underline dark:text-on-surface-dark/70 dark:hover:text-primary-dark">
|
|
||||||
{{ t(key="filter-clear", lang=L) }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div id="shop-results">
|
|
||||||
{% include "shop/_results.html" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|||||||
@@ -164,13 +164,13 @@ async fn run_search(
|
|||||||
let filter = view::category_filter_ids(&all_categories, &selected_category);
|
let filter = view::category_filter_ids(&all_categories, &selected_category);
|
||||||
items.retain(|i| view::category_filter_keep(&filter, i.product.category_id));
|
items.retain(|i| view::category_filter_keep(&filter, i.product.category_id));
|
||||||
|
|
||||||
// 6. Sort. Relevance keeps the search rank (newest when there is no query).
|
// 6. Sort. Relevance is the default; with no text query it keeps the base
|
||||||
let default_sort = if q_trim.is_empty() { "newest" } else { "relevance" };
|
// order (newest-first), and with a query it keeps the ranked search order.
|
||||||
let sort = params
|
let sort = params
|
||||||
.sort
|
.sort
|
||||||
.clone()
|
.clone()
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty())
|
||||||
.unwrap_or_else(|| default_sort.to_string());
|
.unwrap_or_else(|| "relevance".to_string());
|
||||||
match sort.as_str() {
|
match sort.as_str() {
|
||||||
"price_asc" => items.sort_by(|a, b| a.priced.price_cents.cmp(&b.priced.price_cents)),
|
"price_asc" => items.sort_by(|a, b| a.priced.price_cents.cmp(&b.priced.price_cents)),
|
||||||
"price_desc" => items.sort_by(|a, b| b.priced.price_cents.cmp(&a.priced.price_cents)),
|
"price_desc" => items.sort_by(|a, b| b.priced.price_cents.cmp(&a.priced.price_cents)),
|
||||||
@@ -434,11 +434,17 @@ async fn show(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Category page: the same faceted search as the shop, but with this category
|
||||||
|
/// preselected as the default filter (plus breadcrumbs and subcategory chips).
|
||||||
|
/// Any other filters/sort/query on the URL are honoured; the category itself is
|
||||||
|
/// always forced to this page's category. Interacting with the toolbar navigates
|
||||||
|
/// to `/search` (the category stays selected there too).
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
async fn category(
|
async fn category(
|
||||||
jar: CookieJar,
|
jar: CookieJar,
|
||||||
ViewEngine(v): ViewEngine<TeraView>,
|
ViewEngine(v): ViewEngine<TeraView>,
|
||||||
Path(slug): Path<String>,
|
Path(slug): Path<String>,
|
||||||
|
Query(params): Query<SearchParams>,
|
||||||
State(ctx): State<AppContext>,
|
State(ctx): State<AppContext>,
|
||||||
) -> Result<Response> {
|
) -> Result<Response> {
|
||||||
let published = categories::published(&ctx).await?;
|
let published = categories::published(&ctx).await?;
|
||||||
@@ -451,36 +457,22 @@ async fn category(
|
|||||||
let breadcrumbs = categories::ancestors(&published, category.parent_id);
|
let breadcrumbs = categories::ancestors(&published, category.parent_id);
|
||||||
let children = categories::children_of(&published, category.id);
|
let children = categories::children_of(&published, category.id);
|
||||||
|
|
||||||
// Products listed here span this category and all of its descendants, so a
|
// Force the category filter to this page's category, keeping any other params.
|
||||||
// parent category is never empty just because its products live in leaves.
|
let params = SearchParams {
|
||||||
let mut category_ids: Vec<i32> = categories::descendant_ids(&published, category.id)
|
category: Some(category.id.to_string()),
|
||||||
.into_iter()
|
..params
|
||||||
.collect();
|
};
|
||||||
category_ids.push(category.id);
|
|
||||||
let list = products::Entity::find()
|
|
||||||
.filter(products::Column::CategoryId.is_in(category_ids))
|
|
||||||
.filter(products::Column::Published.eq(true))
|
|
||||||
.order_by_desc(products::Column::PublishedAt)
|
|
||||||
.all(&ctx.db)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let user = guard::current_user(&ctx, &jar).await;
|
let user = guard::current_user(&ctx, &jar).await;
|
||||||
|
let mut context = run_search(&ctx, user.as_ref(), ¶ms).await?;
|
||||||
|
if let Some(map) = context.as_object_mut() {
|
||||||
|
map.insert("category".into(), serde_json::to_value(&category)?);
|
||||||
|
map.insert("breadcrumbs".into(), serde_json::to_value(&breadcrumbs)?);
|
||||||
|
map.insert("children".into(), serde_json::to_value(&children)?);
|
||||||
|
}
|
||||||
let c = guard::chrome_from(&ctx, user.as_ref());
|
let c = guard::chrome_from(&ctx, user.as_ref());
|
||||||
format::view(
|
add_chrome(&mut context, &c, ¤t_lang(&jar));
|
||||||
&v,
|
format::view(&v, "shop/category.html", context)
|
||||||
"shop/category.html",
|
|
||||||
json!({
|
|
||||||
"category": category,
|
|
||||||
"breadcrumbs": breadcrumbs,
|
|
||||||
"children": children,
|
|
||||||
"products": product_rows(&ctx, user.as_ref(), list).await?,
|
|
||||||
"logged_in_admin": c.logged_in_admin,
|
|
||||||
"logged_in_customer": c.logged_in_customer,
|
|
||||||
"customer_name": c.customer_name,
|
|
||||||
"customer_account_type": c.customer_account_type,
|
|
||||||
"lang": current_lang(&jar),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn routes() -> Routes {
|
pub fn routes() -> Routes {
|
||||||
|
|||||||
Reference in New Issue
Block a user