From aac19831e3f6ea051840c1879405470402f075f0 Mon Sep 17 00:00:00 2001 From: Priec Date: Wed, 29 Jul 2026 20:44:03 +0200 Subject: [PATCH] small fixes --- assets/views/base.html | 19 +++++++++++++++++-- assets/views/shop/_sidebar.html | 12 ++++++++---- assets/views/shop/show.html | 21 +++++++++++++++++++++ src/controllers/shop.rs | 8 ++++++++ src/tasks/import_catalog.rs | 14 ++++++++++---- 5 files changed, 64 insertions(+), 10 deletions(-) diff --git a/assets/views/base.html b/assets/views/base.html index 64700fc..017ab6b 100644 --- a/assets/views/base.html +++ b/assets/views/base.html @@ -29,14 +29,29 @@ if (currentTheme() === 'system') applyTheme('system'); }); // Mark the active top-nav link via aria-current (styled with Tailwind). + // + // Pages that live under a category but not at its URL (a product detail + // page) declare it via
; without that the + // category sidebar would show nothing selected the moment you open a + // product. `window.activeCategory` is read by the sidebar's accordion + // groups, which are hx-preserve'd and so initialise only once — the + // `nav-category` event is what reaches them on later navigations. function markActiveNav() { var path = location.pathname; + var main = document.querySelector('main[data-active-category]'); + var cat = main ? main.getAttribute('data-active-category') : ''; + var catPath = cat ? '/category/' + cat : null; + window.activeCategory = cat; document.querySelectorAll('a[data-nav]').forEach(function (a) { var h = a.getAttribute('data-nav'); - var on = h === path || (h !== '/' && path.indexOf(h) === 0); + // A declared category wins outright: on /shop/{slug} the prefix rule + // would otherwise light up "all products" alongside the category. + var on = h === path || h === catPath + || (!catPath && h !== '/' && path.indexOf(h) === 0); if (on) a.setAttribute('aria-current', 'page'); else a.removeAttribute('aria-current'); }); + if (cat) window.dispatchEvent(new CustomEvent('nav-category', { detail: cat })); } document.addEventListener('DOMContentLoaded', markActiveNav); document.addEventListener('htmx:afterSwap', markActiveNav); @@ -250,7 +265,7 @@ {% endif %} -
+
{% block content %}{% endblock content %}
diff --git a/assets/views/shop/_sidebar.html b/assets/views/shop/_sidebar.html index 7fcb9b2..9d3ccbf 100644 --- a/assets/views/shop/_sidebar.html +++ b/assets/views/shop/_sidebar.html @@ -3,8 +3,10 @@ categories, each `{ name, slug, children: [{ name, slug }] }`. A category 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. + aria-current; groups auto-expand when the current page is the category, one + of its subcategories, or a product inside it (which markActiveNav announces + as window.activeCategory + the `nav-category` event, since this partial is + hx-preserve'd and therefore only ever initialises once). Adapted from the vendored Penguin UI component penguinui-components/sidebar/sidebar-with-collapsible-menus.html: Penguin's @@ -27,8 +29,10 @@ {% for group in category_groups %} {% if group.children | length > 0 %} -
+
diff --git a/assets/views/shop/show.html b/assets/views/shop/show.html index 7f9cdff..9284ac5 100644 --- a/assets/views/shop/show.html +++ b/assets/views/shop/show.html @@ -3,6 +3,27 @@ {% block title %}{{ product.name }}{% endblock title %} +{# Keeps this product's category selected and expanded in the sidebar, which + otherwise matches on the URL alone and so goes blank on /shop/{slug}. #} +{% block nav_category %}{% if category %}{{ category.slug }}{% endif %}{% endblock nav_category %} + +{# Same trail as the category listing, extended with the product itself, so the + path back up the tree survives the click into a product. #} +{% block breadcrumbs %} +{% set L = lang | default(value='sk') %} + +{% endblock breadcrumbs %} + {% block content %}
diff --git a/src/controllers/shop.rs b/src/controllers/shop.rs index 9f31440..29e86ed 100644 --- a/src/controllers/shop.rs +++ b/src/controllers/shop.rs @@ -443,6 +443,13 @@ async fn show( Some(id) => categories::Entity::find_by_id(id).one(&ctx.db).await?, None => None, }; + // Ancestors of the product's category, so the detail page carries the same + // trail as the listing the customer arrived from. Empty for an + // uncategorized product, which then shows only Home › Shop › name. + let breadcrumbs = match category.as_ref() { + Some(c) => categories::ancestors(&categories::published(&ctx).await?, c.parent_id), + None => Vec::new(), + }; let user = guard::current_user(&ctx, &jar).await; let cur = currency::resolve(&ctx, &jar).await; @@ -487,6 +494,7 @@ async fn show( "variants": options, "images": images.iter().map(|i| i.image_id.clone()).collect::>(), "category": category, + "breadcrumbs": breadcrumbs, "logged_in_admin": c.logged_in_admin, "logged_in_customer": c.logged_in_customer, "customer_name": c.customer_name, diff --git a/src/tasks/import_catalog.rs b/src/tasks/import_catalog.rs index 09fed94..f86f9d2 100644 --- a/src/tasks/import_catalog.rs +++ b/src/tasks/import_catalog.rs @@ -85,6 +85,10 @@ struct ProductJson { #[derive(Deserialize)] struct CrumbJson { id: i32, + /// Ancestor names ending in this category's own — its depth in the tree. + /// Breadcrumb nodes carry a `url` instead of a path, hence the default. + #[serde(default)] + path: Vec, } #[derive(Deserialize)] @@ -244,13 +248,15 @@ async fn import_products( .await?; // The breadcrumb's last node is the product's primary category. A few - // products have no breadcrumb; fall back to the first category they are - // listed in. Extra memberships are dropped — a product holds one - // category here. + // products have no breadcrumb; fall back to the deepest category they + // are listed in, not the first — the old shop lists the broad parent + // first, so taking `first()` filed all three glove products under + // "Zdravotnícke" and left "Rukavice" empty. Extra memberships are + // dropped either way: a product holds one category here. let legacy_category = item .breadcrumb .last() - .or_else(|| item.categories.first()) + .or_else(|| item.categories.iter().max_by_key(|c| c.path.len())) .map(|c| c.id); let category_id = legacy_category.and_then(|old| category_ids.get(&old).copied()); if item.categories.len() > 1 {