summaryrefslogtreecommitdiffstats
path: root/src/pages/recipe.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/pages/recipe.rs')
-rw-r--r--src/pages/recipe.rs344
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()
+ }
+ }
+}