summary refs log tree commit diff stats
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()
+        }
+    }
+}