now products have different options, like different parameters
Some checks failed
CI / Check Style (push) Has been cancelled
CI / Run Clippy (push) Has been cancelled
CI / Run Tests (push) Has been cancelled

This commit is contained in:
Priec
2026-06-22 15:44:02 +02:00
parent 29854a972b
commit 3f798432a0
52 changed files with 1281 additions and 628 deletions

View File

@@ -9,22 +9,40 @@ use serde_json::json;
use crate::{
controllers::i18n::current_lang,
shared::{guard, pricing},
models::{categories, product_images, products, users},
models::{categories, product_images, product_variants, products, users},
views::shop as view,
};
/// Shape a list of products into card rows for `user` (None = public), pricing
/// each via [`pricing::price_many`] and loading its primary image.
/// Shape a list of products into card rows for `user` (None = public). Each card
/// shows the resolved price of the product's representative (first) variant; the
/// `variant_count` lets the template render "from {price}" for multi-variant
/// products. Products with no variants are skipped (not purchasable).
async fn product_rows(
ctx: &AppContext,
user: Option<&users::Model>,
list: Vec<products::Model>,
) -> Result<Vec<serde_json::Value>> {
let priced = pricing::price_many(ctx, &list, user).await?;
let mut rows = Vec::with_capacity(list.len());
for (product, priced) in list.iter().zip(priced.iter()) {
let ids: Vec<i32> = list.iter().map(|p| p.id).collect();
let grouped = product_variants::Entity::grouped_for_products(&ctx.db, &ids).await?;
// Representative (first) variant per product, in list order, dropping any
// product that has no variants.
let mut entries: Vec<(&products::Model, product_variants::Model, usize)> = Vec::new();
for product in &list {
if let Some(variants) = grouped.get(&product.id) {
if let Some(first) = variants.first() {
entries.push((product, first.clone(), variants.len()));
}
}
}
let reps: Vec<product_variants::Model> = entries.iter().map(|(_, v, _)| v.clone()).collect();
let priced = pricing::price_variants(ctx, &reps, user).await?;
let mut rows = Vec::with_capacity(entries.len());
for ((product, rep, count), priced) in entries.iter().zip(priced.iter()) {
let image = product_images::first_for(ctx, product.id).await?;
rows.push(view::product_card(product, priced, image, None));
rows.push(view::product_card(product, rep, priced, *count, image, None));
}
Ok(rows)
}
@@ -121,13 +139,44 @@ async fn show(
};
let user = guard::current_user(&ctx, &jar).await;
let priced = pricing::price_for(&ctx, &product, user.as_ref()).await?;
let variants = product_variants::Entity::for_product(&ctx.db, product.id).await?;
let variant_prices = pricing::price_variants(&ctx, &variants, user.as_ref()).await?;
let options: Vec<serde_json::Value> = variants
.iter()
.zip(variant_prices.iter())
.map(|(variant, priced)| view::variant_option(variant, priced))
.collect();
// The card header uses the representative (first) variant for its headline
// price; the picker below lets the customer switch.
let representative = variants.first();
let priced = variant_prices.first().copied();
let card = match (representative, priced) {
(Some(rep), Some(priced)) => view::product_card(
&product,
rep,
&priced,
variants.len(),
None,
category.as_ref().map(|c| c.name.clone()),
),
// A product with no variants isn't purchasable; show it without a price.
_ => serde_json::json!({
"id": product.id,
"name": product.name,
"slug": product.slug,
"description": product.description,
"currency": product.currency,
"variant_count": 0,
"has_options": false,
}),
};
let c = guard::chrome_from(&ctx, user.as_ref());
format::view(
&v,
"shop/show.html",
json!({
"product": view::product_card(&product, &priced, None, category.as_ref().map(|c| c.name.clone())),
"product": card,
"variants": options,
"images": images.iter().map(|i| i.image_id.clone()).collect::<Vec<_>>(),
"category": category,
"logged_in_admin": c.logged_in_admin,