subcategories implemented and working now
This commit is contained in:
@@ -1,7 +1,10 @@
|
|||||||
{# Site-wide category sidebar contents, served as an htmx partial and swapped
|
{# Site-wide category menu, served as an htmx partial and swapped into the
|
||||||
into the <aside> in base.html. `category_tree` is a depth-ordered flat list
|
<aside> in base.html. `category_groups` is a two-level list of top-level
|
||||||
of { name, slug, depth }; nesting is shown via left indentation. Active state
|
categories, each `{ name, slug, children: [{ name, slug }] }`. A category
|
||||||
is set client-side by markActiveNav() via data-nav + aria-current. #}
|
with children is expandable (accordion); one without is a plain link.
|
||||||
|
Active state is set client-side by markActiveNav() via data-nav +
|
||||||
|
aria-current; groups auto-expand when the current page is the category or
|
||||||
|
one of its subcategories. #}
|
||||||
<p class="px-3 pb-2 text-xs font-semibold uppercase tracking-wide text-on-surface/60 dark:text-on-surface-dark/60">
|
<p class="px-3 pb-2 text-xs font-semibold uppercase tracking-wide text-on-surface/60 dark:text-on-surface-dark/60">
|
||||||
{{ t(key="categories", lang=lang | default(value='sk')) }}
|
{{ t(key="categories", lang=lang | default(value='sk')) }}
|
||||||
</p>
|
</p>
|
||||||
@@ -12,16 +15,46 @@
|
|||||||
{{ t(key="all-products", lang=lang | default(value='sk')) }}
|
{{ t(key="all-products", lang=lang | default(value='sk')) }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% for item in category_tree %}
|
{% for group in category_groups %}
|
||||||
|
{% if group.children | length > 0 %}
|
||||||
|
<li x-data="{ open: false }"
|
||||||
|
x-init="open = ['{{ group.slug }}'{% for child in group.children %}, '{{ child.slug }}'{% endfor %}].some(s => location.pathname === '/category/' + s)">
|
||||||
|
<div class="flex items-stretch">
|
||||||
|
<a href="/category/{{ group.slug }}" data-nav="/category/{{ group.slug }}"
|
||||||
|
class="flex-1 truncate rounded-l-radius px-3 py-1.5 text-sm font-medium text-on-surface transition hover:bg-surface hover:text-primary aria-[current=page]:bg-primary aria-[current=page]:text-on-primary dark:text-on-surface-dark dark:hover:bg-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:bg-primary-dark dark:aria-[current=page]:text-on-primary-dark">
|
||||||
|
{{ group.name }}
|
||||||
|
</a>
|
||||||
|
<button type="button" @click="open = !open" :aria-expanded="open"
|
||||||
|
aria-label="{{ group.name }}"
|
||||||
|
class="inline-flex w-8 shrink-0 items-center justify-center rounded-r-radius text-on-surface/60 transition hover:bg-surface hover:text-primary dark:text-on-surface-dark/60 dark:hover:bg-surface-dark dark:hover:text-primary-dark">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"
|
||||||
|
class="size-4 transition-transform" :class="open && 'rotate-90'">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ul x-show="open" x-cloak x-transition class="mt-0.5 flex flex-col gap-0.5">
|
||||||
|
{% for child in group.children %}
|
||||||
<li>
|
<li>
|
||||||
<a href="/category/{{ item.slug }}" data-nav="/category/{{ item.slug }}" style="padding-left: {{ 12 + item.depth * 16 }}px"
|
<a href="/category/{{ child.slug }}" data-nav="/category/{{ child.slug }}" style="padding-left: 28px"
|
||||||
class="flex items-center gap-1.5 rounded-radius py-1.5 pr-3 text-sm font-medium text-on-surface transition hover:bg-surface hover:text-primary aria-[current=page]:bg-primary aria-[current=page]:text-on-primary dark:text-on-surface-dark dark:hover:bg-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:bg-primary-dark dark:aria-[current=page]:text-on-primary-dark">
|
class="flex items-center gap-1.5 rounded-radius py-1.5 pr-3 text-sm text-on-surface transition hover:bg-surface hover:text-primary aria-[current=page]:bg-primary aria-[current=page]:text-on-primary dark:text-on-surface-dark dark:hover:bg-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:bg-primary-dark dark:aria-[current=page]:text-on-primary-dark">
|
||||||
{% if item.depth > 0 %}<span class="text-on-surface/40 dark:text-on-surface-dark/40">↳</span>{% endif %}
|
<span class="text-on-surface/40 dark:text-on-surface-dark/40">↳</span>
|
||||||
<span class="truncate">{{ item.name }}</span>
|
<span class="truncate">{{ child.name }}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% if category_tree | length == 0 %}
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li>
|
||||||
|
<a href="/category/{{ group.slug }}" data-nav="/category/{{ group.slug }}"
|
||||||
|
class="block truncate rounded-radius px-3 py-1.5 text-sm font-medium text-on-surface transition hover:bg-surface hover:text-primary aria-[current=page]:bg-primary aria-[current=page]:text-on-primary dark:text-on-surface-dark dark:hover:bg-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:bg-primary-dark dark:aria-[current=page]:text-on-primary-dark">
|
||||||
|
{{ group.name }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% if category_groups | length == 0 %}
|
||||||
<p class="px-3 py-2 text-sm text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="shop-empty", lang=lang | default(value='sk')) }}</p>
|
<p class="px-3 py-2 text-sm text-on-surface/60 dark:text-on-surface-dark/60">{{ t(key="shop-empty", lang=lang | default(value='sk')) }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ async fn category_sidebar(
|
|||||||
&v,
|
&v,
|
||||||
"shop/_sidebar.html",
|
"shop/_sidebar.html",
|
||||||
json!({
|
json!({
|
||||||
"category_tree": view::sidebar_rows(&categories::tree(&published)),
|
"category_groups": view::sidebar_groups(&published),
|
||||||
"lang": current_lang(&jar),
|
"lang": current_lang(&jar),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -45,12 +45,24 @@ pub fn product_form(product: &products::Model, image: Option<String>) -> Value {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Depth-ordered `{ name, slug, depth }` rows for the storefront sidebar,
|
/// Two-level grouping for the storefront sidebar menu: each top-level category
|
||||||
/// rendered as an indented flat list.
|
/// as `{ name, slug, children: [{ name, slug }] }`, with its direct
|
||||||
pub fn sidebar_rows(tree: &[(categories::Model, usize)]) -> Vec<Value> {
|
/// subcategories nested under `children` (empty when the category has none).
|
||||||
tree.iter()
|
/// Siblings are ordered by position then name on both levels.
|
||||||
.map(|(category, depth)| {
|
pub fn sidebar_groups(categories: &[categories::Model]) -> Vec<Value> {
|
||||||
json!({ "name": category.name, "slug": category.slug, "depth": depth })
|
let mut top: Vec<&categories::Model> = categories
|
||||||
|
.iter()
|
||||||
|
.filter(|c| c.parent_id.is_none())
|
||||||
|
.collect();
|
||||||
|
top.sort_by(|a, b| a.position.cmp(&b.position).then_with(|| a.name.cmp(&b.name)));
|
||||||
|
|
||||||
|
top.into_iter()
|
||||||
|
.map(|category| {
|
||||||
|
let children: Vec<Value> = crate::models::categories::children_of(categories, category.id)
|
||||||
|
.into_iter()
|
||||||
|
.map(|child| json!({ "name": child.name, "slug": child.slug }))
|
||||||
|
.collect();
|
||||||
|
json!({ "name": category.name, "slug": category.slug, "children": children })
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user