diff --git a/assets/views/shop/_search.html b/assets/views/shop/_search.html new file mode 100644 index 0000000..2046bc5 --- /dev/null +++ b/assets/views/shop/_search.html @@ -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') %} +
+ + +
+ {% include "shop/_results.html" %} +
+
diff --git a/assets/views/shop/category.html b/assets/views/shop/category.html index 7265c4e..06f43a4 100644 --- a/assets/views/shop/category.html +++ b/assets/views/shop/category.html @@ -4,10 +4,11 @@ {% block title %}{{ category.name }}{% endblock title %} {% block content %} -
+{% set L = lang | default(value='sk') %} +
- {% if products | length > 0 %} - {% include "shop/_product_grid.html" %} - {% else %} -
- {{ t(key="shop-empty", lang=lang | default(value='sk')) }} -
- {% endif %} + {# Same search + filters as the shop, with this category preselected. #} + {% include "shop/_search.html" %}
{% endblock content %} diff --git a/assets/views/shop/index.html b/assets/views/shop/index.html index f85d4b1..a063934 100644 --- a/assets/views/shop/index.html +++ b/assets/views/shop/index.html @@ -11,99 +11,6 @@

{{ t(key="shop-subtitle", lang=L) }}

- {# One form drives the whole listing. htmx re-runs /search and swaps only the - 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. #} - - -
- {% include "shop/_results.html" %} -
+ {% include "shop/_search.html" %}
{% endblock content %} diff --git a/src/controllers/shop.rs b/src/controllers/shop.rs index dd82ebd..b0932e6 100644 --- a/src/controllers/shop.rs +++ b/src/controllers/shop.rs @@ -164,13 +164,13 @@ async fn run_search( let filter = view::category_filter_ids(&all_categories, &selected_category); 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). - let default_sort = if q_trim.is_empty() { "newest" } else { "relevance" }; + // 6. Sort. Relevance is the default; with no text query it keeps the base + // order (newest-first), and with a query it keeps the ranked search order. let sort = params .sort .clone() .filter(|s| !s.is_empty()) - .unwrap_or_else(|| default_sort.to_string()); + .unwrap_or_else(|| "relevance".to_string()); match sort.as_str() { "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)), @@ -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] async fn category( jar: CookieJar, ViewEngine(v): ViewEngine, Path(slug): Path, + Query(params): Query, State(ctx): State, ) -> Result { let published = categories::published(&ctx).await?; @@ -451,36 +457,22 @@ async fn category( let breadcrumbs = categories::ancestors(&published, category.parent_id); let children = categories::children_of(&published, category.id); - // Products listed here span this category and all of its descendants, so a - // parent category is never empty just because its products live in leaves. - let mut category_ids: Vec = categories::descendant_ids(&published, category.id) - .into_iter() - .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?; + // Force the category filter to this page's category, keeping any other params. + let params = SearchParams { + category: Some(category.id.to_string()), + ..params + }; 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()); - format::view( - &v, - "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), - }), - ) + add_chrome(&mut context, &c, ¤t_lang(&jar)); + format::view(&v, "shop/category.html", context) } pub fn routes() -> Routes {