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 {