From f6a3fb9c4d8dd86f78c9f75a23c1ac35bf35d4eb Mon Sep 17 00:00:00 2001 From: Benedikt Peetz Date: Thu, 19 Mar 2026 07:45:14 +0100 Subject: feat(treewide): Commit MVP --- src/pages/recipe.rs | 344 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 344 insertions(+) create mode 100644 src/pages/recipe.rs (limited to 'src/pages/recipe.rs') diff --git a/src/pages/recipe.rs b/src/pages/recipe.rs new file mode 100644 index 0000000..4e56e1d --- /dev/null +++ b/src/pages/recipe.rs @@ -0,0 +1,344 @@ +#![expect( + clippy::needless_pass_by_value, + reason = "It's soo much easier to just pass these values by value" +)] + +use std::sync::Arc; + +use leptos::{ + IntoView, component, + prelude::{ClassAttribute, CollectView, ElementChild, Get, GlobalAttributes, IntoAny}, + view, +}; +use leptos_router::hooks::use_params_map; +use rocie_client::models::{self, Content, Ingredient, Item, Section, UnitAmount}; + +use crate::{ + api::{ + product_by_id_wrapped, recipe_by_id_wrapped, recipe_by_name_wrapped, unit_by_id_wrapped, + }, + components::{ + async_fetch::{AsyncFetch, AsyncResource}, + catch_errors::CatchErrors, + login_wall::LoginWall, + site_header::SiteHeader, + }, +}; + +#[component] +pub fn Recipe() -> impl IntoView { + let name = || { + use_params_map() + .get() + .get("name") + .expect("Should always have a name, because the router would otherwise not match") + }; + + view! { + + + + { + AsyncFetch! { + @map_error_in_producer + from_resource=AsyncResource! { + (name: String = name().clone()) + -> Result { + recipe_by_name_wrapped(&name).await + + } + }, + producer=render_recipe + } + } + + + } +} + +fn render_recipe(recipe: models::recipe::Recipe) -> impl IntoView { + let recipe_content = Arc::new(recipe.content.clone()); + + view! { +
+

{recipe.name}

+ +
+

"Ingredients"

+
    + {recipe + .content + .ingredients + .into_iter() + .map(|ingredient| { + view! {
  • {render_ingredient_ingredient_list(ingredient)}
  • } + }) + .collect_view()} +
+
+ +
+

"Productions steps"

+ { + let local_recipe_content = Arc::clone(&recipe_content); + recipe + .content + .sections + .into_iter() + .map(|section| { + render_section(Arc::clone(&local_recipe_content), section) + }) + .collect_view() + } +
+ +
+ } +} + +fn render_section(recipe: Arc, section: Section) -> impl IntoView { + view! { +
+
    + {section + .content + .into_iter() + .map(|content| { + view! {
  1. {render_content(Arc::clone(&recipe), content)}
  2. } + }) + .collect_view()} +
+
+ } +} + +fn render_content(recipe: Arc, content: Content) -> impl IntoView { + match content { + Content::ContentOneOf(content_one_of) => { + // Step + let step = content_one_of.step; + + step.items + .into_iter() + .map(|item| render_item(Arc::clone(&recipe), item)) + .collect_view() + .into_any() + } + Content::ContentOneOf1(content_one_of1) => { + // Text + view! { {content_one_of1.text} }.into_any() + } + } +} + +fn render_item(recipe: Arc, item: Item) -> impl IntoView { + match item { + Item::ItemOneOf(item_one_of) => { + // text + let text = item_one_of.text; + view! { {text.value} }.into_any() + } + Item::ItemOneOf1(item_one_of1) => { + // Ingredient + let ingr = recipe + .ingredients + .get(item_one_of1.ingredient.index as usize) + .expect("to be valid, as cooklang parser should have varified it") + .to_owned(); + + render_ingredient_text(ingr).into_any() + } + Item::ItemOneOf2(item_one_of2) => { + // Cookware index + let cookware = recipe + .cookware + .get(item_one_of2.cookware.index as usize) + .expect("to be valid, as cooklang parser should have varified it") + .to_owned(); + + view! { + + {if let Some(qty) = cookware.quantity { + format!("{} x {}", qty, cookware.name) + } else { + cookware.name + }} + + } + .into_any() + } + Item::ItemOneOf3(item_one_of3) => { + // Timer index + let timer = recipe + .timers + .get(item_one_of3.timer.index as usize) + .expect("to be valid, as cooklang parser should have varified it") + .to_owned(); + + let amount = timer.quantity.map_or(().into_any(), |amount| { + render_unit_amount_text(amount).into_any() + }); + + let name = timer.name.unwrap_or(String::from("")); + + view! { {name}{amount} }.into_any() + } + Item::ItemOneOf4(item_one_of4) => { + // InlineQuantity + todo!("Inline quantity not yet supported") + } + } +} + +fn render_unit_amount_text(amount: UnitAmount) -> impl IntoView { + AsyncFetch! { + @map_error_in_producer + fetcher = unit_by_id_wrapped(amount.unit), + producer = |unit| { + view! { + { + format!(" ({} {})", amount.value, unit.short_name) + } + } + } + } +} + +fn render_ingredient_text(ingr: Ingredient) -> impl IntoView { + match ingr.clone() { + Ingredient::IngredientOneOf(ingredient_one_of) => { + // Registered product + AsyncFetch! { + @map_error_in_producer + fetcher = product_by_id_wrapped(ingredient_one_of.registered_product.id), + producer =|product| { + let amount = ingredient_one_of + .registered_product + .quantity + .map_or(().into_any(), |amount| render_unit_amount_text(amount.amount).into_any()); + + view! { + + {product.name.clone()}{amount} + + } + } + } + .into_any() + } + Ingredient::IngredientOneOf1(ingredient_one_of1) => { + // Not registered product + let amount = ingredient_one_of1 + .not_registered_product + .quantity + .map_or(().into_any(), |amount| { + render_unit_amount_text(amount).into_any() + }); + + view! { + + {ingredient_one_of1.not_registered_product.name}{amount} + + } + .into_any() + } + Ingredient::IngredientOneOf2(ingredient_one_of2) => { + // Recipe reference + let id = ingredient_one_of2.recipe_reference.id; + + AsyncFetch! { + @map_error_in_producer + fetcher=recipe_by_id_wrapped(id), + producer=|recipe| { + view! { + + {recipe.name.clone()} + + } + }, + } + .into_any() + } + } +} + +fn render_unit_amount_ingredient_list(amount: UnitAmount) -> impl IntoView { + AsyncFetch! { + @map_error_in_producer + fetcher = unit_by_id_wrapped(amount.unit), + producer = |unit| { + view! { + { + format!( + "{} {} of ", + amount.value, + if amount.value == 1 {unit.full_name_singular} else {unit.full_name_plural}, + ) + } + } + } + } +} + +fn render_ingredient_ingredient_list(ingr: Ingredient) -> impl IntoView { + match ingr.clone() { + Ingredient::IngredientOneOf(ingredient_one_of) => { + // Registered product + AsyncFetch! { + @map_error_in_producer + fetcher = product_by_id_wrapped(ingredient_one_of.registered_product.id), + producer = |product| { + let amount = ingredient_one_of + .registered_product + .quantity + .map_or( + ().into_any(), + |amount| render_unit_amount_ingredient_list(amount.amount).into_any()); + + + view! { + + + {amount}{product.name.clone()} + + + } + } + } + .into_any() + } + Ingredient::IngredientOneOf1(ingredient_one_of1) => { + // Not registered product + let amount = ingredient_one_of1 + .not_registered_product + .quantity + .map_or(().into_any(), |amount| { + render_unit_amount_ingredient_list(amount).into_any() + }); + + view! { + + {amount}{ingredient_one_of1.not_registered_product.name} + + } + .into_any() + } + Ingredient::IngredientOneOf2(ingredient_one_of2) => { + // Recipe reference + let id = ingredient_one_of2.recipe_reference.id; + + AsyncFetch! { + @map_error_in_producer + fetcher=recipe_by_id_wrapped(id), + producer=|recipe| { + view! { + + {recipe.name.clone()} + + } + }, + } + .into_any() + } + } +} -- cgit 1.4.1