diff --git a/assets/i18n/en/main.ftl b/assets/i18n/en/main.ftl
index 31c1a74..82c97e4 100644
--- a/assets/i18n/en/main.ftl
+++ b/assets/i18n/en/main.ftl
@@ -315,6 +315,7 @@ order-search-placeholder = Search orders…
search-empty = Nothing matched your search:
results-count = { $count } products
sort-label = Sort
+per-page-label = Per page
sort-relevance = Relevance
sort-newest = Newest
sort-price_asc = Price: low to high
diff --git a/assets/i18n/sk/main.ftl b/assets/i18n/sk/main.ftl
index 379c31f..8fbfcdc 100644
--- a/assets/i18n/sk/main.ftl
+++ b/assets/i18n/sk/main.ftl
@@ -315,6 +315,7 @@ order-search-placeholder = Hľadať objednávky…
search-empty = Pre váš výraz sme nič nenašli:
results-count = { $count } produktov
sort-label = Zoradiť
+per-page-label = Na stránku
sort-relevance = Relevancia
sort-newest = Najnovšie
sort-price_asc = Cena: od najnižšej
diff --git a/assets/views/shop/_search.html b/assets/views/shop/_search.html
index 6aa38e8..48d6b45 100644
--- a/assets/views/shop/_search.html
+++ b/assets/views/shop/_search.html
@@ -52,6 +52,24 @@
+
+
+
+
+
+
diff --git a/src/controllers/shop.rs b/src/controllers/shop.rs
index f9f35cf..89429d7 100644
--- a/src/controllers/shop.rs
+++ b/src/controllers/shop.rs
@@ -22,8 +22,21 @@ use crate::{
views::shop as view,
};
-/// Results per page in the storefront listing/search.
+/// Default results per page in the storefront listing/search.
const PER_PAGE: usize = 24;
+/// Allowed per-page choices offered in the toolbar; any other value falls back
+/// to [`PER_PAGE`].
+const PER_PAGE_OPTIONS: [usize; 3] = [24, 48, 96];
+
+/// Resolve the requested per-page count to one of [`PER_PAGE_OPTIONS`],
+/// defaulting to [`PER_PAGE`].
+fn resolve_per_page(params: &SearchParams) -> usize {
+ params
+ .per_page
+ .map(|p| p as usize)
+ .filter(|p| PER_PAGE_OPTIONS.contains(p))
+ .unwrap_or(PER_PAGE)
+}
/// Hard cap on candidates a single text search considers before faceting; well
/// above any realistic page of results for this catalog.
const SEARCH_CAP: u64 = 1000;
@@ -40,6 +53,7 @@ struct SearchParams {
in_stock: Option,
sort: Option,
page: Option,
+ per_page: Option,
}
/// A candidate product with everything the listing needs to filter, sort and
@@ -81,6 +95,9 @@ fn query_base(params: &SearchParams) -> String {
if let Some(s) = params.sort.as_deref().filter(|s| !s.is_empty()) {
ser.append_pair("sort", s);
}
+ if let Some(p) = params.per_page.filter(|p| *p as usize != PER_PAGE) {
+ ser.append_pair("per_page", &p.to_string());
+ }
ser.finish()
}
@@ -198,14 +215,15 @@ async fn run_search(
}
// 7. Paginate.
+ let per_page = resolve_per_page(params);
let total = items.len();
- let pages = total.div_ceil(PER_PAGE).max(1);
+ let pages = total.div_ceil(per_page).max(1);
let page = params.page.unwrap_or(1).clamp(1, pages as u32);
- let start = (page as usize - 1) * PER_PAGE;
+ let start = (page as usize - 1) * per_page;
// 8. Render only the current page's cards (images fetched per row).
let mut rows = Vec::new();
- for item in items.iter().skip(start).take(PER_PAGE) {
+ for item in items.iter().skip(start).take(per_page) {
let image = product_images::first_for(ctx, item.product.id).await?;
let cat_name = item.product.category_id.and_then(|id| category_name.get(&id).cloned());
rows.push(view::product_card(
@@ -229,6 +247,8 @@ async fn run_search(
"selected_category_id": selected_category.parse::().unwrap_or(-1),
"uncategorized_count": uncategorized_count,
"sort": sort,
+ "per_page": per_page,
+ "per_page_options": PER_PAGE_OPTIONS,
"in_stock": in_stock_only,
"min_price": params.min_price.clone().unwrap_or_default(),
"max_price": params.max_price.clone().unwrap_or_default(),