page is better in shop now

This commit is contained in:
Priec
2026-06-25 15:38:18 +02:00
parent ee8ec5c85b
commit 848042c304
4 changed files with 44 additions and 4 deletions

View File

@@ -315,6 +315,7 @@ order-search-placeholder = Search orders…
search-empty = Nothing matched your search: search-empty = Nothing matched your search:
results-count = { $count } products results-count = { $count } products
sort-label = Sort sort-label = Sort
per-page-label = Per page
sort-relevance = Relevance sort-relevance = Relevance
sort-newest = Newest sort-newest = Newest
sort-price_asc = Price: low to high sort-price_asc = Price: low to high

View File

@@ -315,6 +315,7 @@ order-search-placeholder = Hľadať objednávky…
search-empty = Pre váš výraz sme nič nenašli: search-empty = Pre váš výraz sme nič nenašli:
results-count = { $count } produktov results-count = { $count } produktov
sort-label = Zoradiť sort-label = Zoradiť
per-page-label = Na stránku
sort-relevance = Relevancia sort-relevance = Relevancia
sort-newest = Najnovšie sort-newest = Najnovšie
sort-price_asc = Cena: od najnižšej sort-price_asc = Cena: od najnižšej

View File

@@ -52,6 +52,24 @@
</select> </select>
</label> </label>
<!-- per-page count -->
<label class="flex items-center gap-2 text-xs font-medium text-on-surface/70 dark:text-on-surface-dark/70">
{{ t(key="per-page-label", lang=L) }}
<select name="per_page"
class="rounded-radius border border-outline bg-surface px-2 py-1.5 text-sm text-on-surface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark">
{% for opt in per_page_options %}
<option value="{{ opt }}"{% if per_page == opt %} selected{% endif %}>{{ opt }}</option>
{% endfor %}
</select>
</label>
<!-- in stock only -->
<label class="flex items-center gap-2 text-sm text-on-surface dark:text-on-surface-dark">
<input type="checkbox" name="in_stock" value="1"{% if in_stock %} checked{% endif %}
class="size-4 rounded border-outline text-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary dark:border-outline-dark dark:text-primary-dark" />
{{ t(key="filter-in-stock", lang=L) }}
</label>
<!-- grid / list view toggle --> <!-- grid / list view toggle -->
<div class="inline-flex gap-0.5 rounded-radius border border-outline p-0.5 dark:border-outline-dark" role="group" <div class="inline-flex gap-0.5 rounded-radius border border-outline p-0.5 dark:border-outline-dark" role="group"
aria-label="{{ t(key='view-grid', lang=L) }} / {{ t(key='view-list', lang=L) }}"> aria-label="{{ t(key='view-grid', lang=L) }} / {{ t(key='view-list', lang=L) }}">

View File

@@ -22,8 +22,21 @@ use crate::{
views::shop as view, 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; 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 /// Hard cap on candidates a single text search considers before faceting; well
/// above any realistic page of results for this catalog. /// above any realistic page of results for this catalog.
const SEARCH_CAP: u64 = 1000; const SEARCH_CAP: u64 = 1000;
@@ -40,6 +53,7 @@ struct SearchParams {
in_stock: Option<String>, in_stock: Option<String>,
sort: Option<String>, sort: Option<String>,
page: Option<u32>, page: Option<u32>,
per_page: Option<u32>,
} }
/// A candidate product with everything the listing needs to filter, sort and /// 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()) { if let Some(s) = params.sort.as_deref().filter(|s| !s.is_empty()) {
ser.append_pair("sort", s); 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() ser.finish()
} }
@@ -198,14 +215,15 @@ async fn run_search(
} }
// 7. Paginate. // 7. Paginate.
let per_page = resolve_per_page(params);
let total = items.len(); 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 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). // 8. Render only the current page's cards (images fetched per row).
let mut rows = Vec::new(); 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 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()); let cat_name = item.product.category_id.and_then(|id| category_name.get(&id).cloned());
rows.push(view::product_card( rows.push(view::product_card(
@@ -229,6 +247,8 @@ async fn run_search(
"selected_category_id": selected_category.parse::<i32>().unwrap_or(-1), "selected_category_id": selected_category.parse::<i32>().unwrap_or(-1),
"uncategorized_count": uncategorized_count, "uncategorized_count": uncategorized_count,
"sort": sort, "sort": sort,
"per_page": per_page,
"per_page_options": PER_PAGE_OPTIONS,
"in_stock": in_stock_only, "in_stock": in_stock_only,
"min_price": params.min_price.clone().unwrap_or_default(), "min_price": params.min_price.clone().unwrap_or_default(),
"max_price": params.max_price.clone().unwrap_or_default(), "max_price": params.max_price.clone().unwrap_or_default(),