diff options
Diffstat (limited to 'src/pages/recipe.rs')
| -rw-r--r-- | src/pages/recipe.rs | 344 |
1 files changed, 344 insertions, 0 deletions
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! { + <CatchErrors> + <LoginWall back=move || format!("/recipe/{}", name())> + <SiteHeader logo=icondata_io::IoArrowBack back_location="/" name="Recipies" /> + { + AsyncFetch! { + @map_error_in_producer + from_resource=AsyncResource! { + (name: String = name().clone()) + -> Result<models::recipe::Recipe, leptos::error::Error> { + recipe_by_name_wrapped(&name).await + + } + }, + producer=render_recipe + } + } + </LoginWall> + </CatchErrors> + } +} + +fn render_recipe(recipe: models::recipe::Recipe) -> impl IntoView { + let recipe_content = Arc::new(recipe.content.clone()); + + view! { + <div class="flex flex-col contents-start"> + <h1>{recipe.name}</h1> + + <div> + <h3 class="text-lg font-bold">"Ingredients"</h3> + <ul> + {recipe + .content + .ingredients + .into_iter() + .map(|ingredient| { + view! { <li>{render_ingredient_ingredient_list(ingredient)}</li> } + }) + .collect_view()} + </ul> + </div> + + <div> + <h3>"Productions steps"</h3> + { + 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() + } + </div> + + </div> + } +} + +fn render_section(recipe: Arc<models::CooklangRecipe>, section: Section) -> impl IntoView { + view! { + <div> + <ol class="list-inside list-decimal"> + {section + .content + .into_iter() + .map(|content| { + view! { <li>{render_content(Arc::clone(&recipe), content)}</li> } + }) + .collect_view()} + </ol> + </div> + } +} + +fn render_content(recipe: Arc<models::CooklangRecipe>, 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<models::CooklangRecipe>, 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! { + <span class="bg-green-400/50 rounded"> + {if let Some(qty) = cookware.quantity { + format!("{} x {}", qty, cookware.name) + } else { + cookware.name + }} + </span> + } + .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("<Unnamed timer>")); + + view! { <span class="bg-blue-400/50 rounded">{name}{amount}</span> }.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! { + <span class="rounded bg-gray-300/70"> + <a href=format!("/product/{}", &product.name)>{product.name.clone()}</a>{amount} + </span> + } + } + } + .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! { + <span class="rounded-lg bg-red-400/70"> + {ingredient_one_of1.not_registered_product.name}{amount} + </span> + } + .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! { + <span class="bg-blue-300/50 rounded-lg"> + <a href=format!("/recipe/{}", recipe.name)>{recipe.name.clone()}</a> + </span> + } + }, + } + .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! { + <span id={product.name}> + <a href={format!("/product/{}", &product.name)}> + {amount}{product.name.clone()} + </a> + </span> + } + } + } + .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! { + <span class="rounded-lg bg-red-400/70"> + {amount}{ingredient_one_of1.not_registered_product.name} + </span> + } + .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! { + <span class="bg-blue-300/50 rounded-lg"> + <a href=format!("/recipe/{}", recipe.name)>{recipe.name.clone()}</a> + </span> + } + }, + } + .into_any() + } + } +} |
