regalade/app/src/recipe.rs

1101 lines
33 KiB
Rust

use std::rc::Rc;
use api::{
AddRecipeIngredientRequest, IngredientInfo, RecipeEditPersonCount, RecipeEditRating,
RecipeEditStepsRequest, RecipeInfo, RecipeIngredientEditRequest, RecipeRenameRequest,
};
use itertools::Itertools;
use uuid::Uuid;
use wasm_bindgen::JsCast;
use web_sys::{HtmlInputElement, HtmlTextAreaElement};
use yew::{
prelude::*,
suspense::{use_future, use_future_with_deps},
};
use yew_router::prelude::*;
use crate::{
api,
bootstrap::{bs, ConfirmDangerModal, FormModal, ModalToggleButton},
recipe_creator::IngredientSelectBase,
RegaladeGlobalState, Route,
};
#[derive(Debug, Clone, PartialEq, Properties)]
struct RecipeRatingProps {
rating: u8,
}
#[function_component]
fn RecipeRating(props: &RecipeRatingProps) -> Html {
let rating = (props.rating + 1).min(3);
html! {
<span aria-label={format!("Rating: {rating}")} class="ms-1">
{ for (0..rating).map(|_| html!{
<i aria-hidden="true" class="bi-star-fill ms-1"/>
}) }
</span>
}
}
async fn get_all_recipes(
token: String,
household: Uuid,
) -> anyhow::Result<api::ListRecipesResponse> {
let rsp = gloo_net::http::Request::get(api!("household/{household}/recipe"))
.header("Authorization", &format!("Bearer {token}"))
.send()
.await?;
if !rsp.ok() {
let body = rsp.text().await.unwrap_or_default();
anyhow::bail!("Could not get recipes (code={}): {body}", rsp.status());
}
Ok(rsp.json().await?)
}
#[derive(Debug, Clone, PartialEq, Eq, Properties)]
struct RecipeListProps {
token: String,
household: Uuid,
}
#[function_component]
fn RecipeListInner(props: &RecipeListProps) -> HtmlResult {
let state = use_future(|| get_all_recipes(props.token.clone(), props.household))?;
Ok(match &*state {
Ok(l) => html! {
<div class="container text-center">
<div class="row row-cols-2 row-cols-sm-2 row-cols-md-4 g-2 mb-2">
{for l.recipes.iter().sorted_by_key(|(_, name, _)| name).map(|(id, name, rating)| html!{
<div class="col" key={*id}>
<div class="p-3 border rounded border-light-subtle h-100">
<Link<Route>
classes={classes!(
"link-light",
"link-offset-2",
"link-underline-opacity-25",
"link-underline-opacity-100-hover",
)}
to={Route::Recipe { id: *id }}
>
{name}
</Link<Route>>
<RecipeRating {rating} />
</div>
</div>
})}
</div>
</div>
},
Err(e) => html! {
<div class={classes!("alert", "alert-danger")} role="alert">
{format!("Error fetching recipes: {e}")}
</div>
},
})
}
#[function_component]
pub fn RecipeList() -> Html {
let fallback = html! {"Loading ..."};
let global_state = use_state(RegaladeGlobalState::get);
html! {
<div class="d-flex align-items-center justify-content-center w-100">
<div class={classes!("container", "text-center", "rounded", "border", "pt-2", "m-2")}>
<h2>{"Recipes"}</h2>
<Suspense {fallback}>
<RecipeListInner
token={global_state.token.token.clone()}
household={global_state.household.id}
/>
</Suspense>
</div>
</div>
}
}
#[derive(PartialEq, Eq, Clone, Properties)]
pub struct RecipeViewerProps {
pub id: i64,
}
#[derive(PartialEq, Eq, Clone, Properties)]
struct RecipeViewerInnerProps {
id: i64,
token: String,
household: Uuid,
}
async fn fetch_recipe(token: String, household: Uuid, id: i64) -> anyhow::Result<Rc<RecipeInfo>> {
let rsp = gloo_net::http::Request::get(api!("household/{household}/recipe/{id}"))
.header("Authorization", &format!("Bearer {token}"))
.send()
.await?;
if !rsp.ok() {
let body = rsp.text().await.unwrap_or_default();
anyhow::bail!("Could not get recipe (code={}): {body}", rsp.status());
}
Ok(Rc::new(rsp.json().await?))
}
#[derive(Clone, PartialEq, Properties)]
struct EditNameProps {
token: String,
household: Uuid,
id: i64,
name: String,
update: Callback<()>,
}
async fn do_rename_recipe(
token: String,
household: Uuid,
recipe: i64,
name: String,
) -> anyhow::Result<()> {
let rsp = gloo_net::http::Request::patch(api!("household/{household}/recipe/{recipe}"))
.json(&RecipeRenameRequest { name })?
.header("Authorization", &format!("Bearer {token}"))
.send()
.await?;
if !rsp.ok() {
let body = rsp.text().await.unwrap_or_default();
anyhow::bail!("Could not get recipes (code={}): {body}", rsp.status());
}
Ok(())
}
#[function_component]
fn EditName(props: &EditNameProps) -> Html {
let name = use_state(|| props.name.clone());
let nm = name.clone();
let onchange = Callback::from(move |e: Event| {
let Some(target) = e.target() else {
return;
};
let Ok(target) = target.dyn_into::<HtmlInputElement>() else {
return;
};
nm.set(target.value());
});
let error = use_state(|| None::<String>);
let nm = name.clone();
let err = error.clone();
let token = props.token.clone();
let household = props.household;
let recipe = props.id;
let update = props.update.clone();
let on_submit = Callback::from(move |_| {
if nm.is_empty() {
err.set(Some("Name can't be empty".into()));
return;
}
let future = do_rename_recipe(token.clone(), household, recipe, (*nm).clone());
let err = err.clone();
let update = update.clone();
wasm_bindgen_futures::spawn_local(async move {
match future.await {
Ok(_) => {
let modal = bs::Modal::get_instance("#rcpEditName");
modal.hide();
err.set(None);
update.emit(());
}
Err(e) => {
err.set(Some(format!("Could not edit name: {e}")));
}
}
})
});
html! {<>
<ModalToggleButton classes={classes!("btn", "btn-secondary")} modal_id="rcpEditName">
{"Edit name"}
</ModalToggleButton>
<FormModal
id="rcpEditName"
fade=true
centered=true
submit_label="Edit"
title="Edit Name"
{on_submit}
>
if let Some(e) = &*error {
<div class={classes!("alert", "alert-danger")} role="alert">
{e}
</div>
}
<div class="form-floating">
<input
class="form-control"
id="rcpEditNameInp"
placeholder={(*name).clone()}
value={(*name).clone()}
{onchange}
/>
<label for="rcpEditNameInp">{"Recipe Name"}</label>
</div>
</FormModal>
</>}
}
#[derive(Clone, PartialEq, Properties)]
struct EditIngredientProps {
token: String,
household: Uuid,
recipe: i64,
ingredient: i64,
amount: f64,
update: Callback<()>,
}
async fn do_edit_ingredient_recipe(
token: String,
household: Uuid,
recipe: i64,
ingredient: i64,
amount: f64,
) -> anyhow::Result<()> {
let rsp = gloo_net::http::Request::patch(api!(
"household/{household}/recipe/{recipe}/ingredients/{ingredient}"
))
.json(&RecipeIngredientEditRequest { amount })?
.header("Authorization", &format!("Bearer {token}"))
.send()
.await?;
if !rsp.ok() {
let body = rsp.text().await.unwrap_or_default();
anyhow::bail!("Could not get recipes (code={}): {body}", rsp.status());
}
Ok(())
}
#[function_component]
fn EditIngredient(props: &EditIngredientProps) -> Html {
let amount = use_state(|| props.amount);
let am = amount.clone();
let onchange = Callback::from(move |e: Event| {
let Some(target) = e.target() else {
return;
};
let Ok(target) = target.dyn_into::<HtmlInputElement>() else {
return;
};
am.set(target.value().parse().unwrap());
});
let error = use_state(|| None::<String>);
let modal_id = format!("rcpEditIg{}", props.ingredient);
let am = amount.clone();
let err = error.clone();
let token = props.token.clone();
let household = props.household;
let recipe = props.recipe;
let ingredient = props.ingredient;
let update = props.update.clone();
let mid = modal_id.clone();
let on_submit = Callback::from(move |_| {
let future = do_edit_ingredient_recipe(token.clone(), household, recipe, ingredient, *am);
let err = err.clone();
let update = update.clone();
let modal_selector = format!("#{mid}");
wasm_bindgen_futures::spawn_local(async move {
match future.await {
Ok(_) => {
let modal = bs::Modal::get_instance(&modal_selector);
modal.hide();
err.set(None);
update.emit(());
}
Err(e) => {
err.set(Some(format!("Could not edit ingredient: {e}")));
}
}
})
});
let input_id = format!("{modal_id}Inp");
html! {<>
<FormModal
id={modal_id.clone()}
fade=true
centered=true
submit_label="Edit"
title="Edit Ingredient"
{on_submit}
>
if let Some(e) = &*error {
<div class={classes!("alert", "alert-danger")} role="alert">
{e}
</div>
}
<div class="form-floating">
<input
class="form-control"
id={input_id.clone()}
placeholder="Ingredient Amount"
value={amount.to_string()}
type="number"
step="any"
{onchange}
/>
<label for={input_id}>{"Ingredient Amount"}</label>
</div>
</FormModal>
<ModalToggleButton {modal_id}>
<i class="bi-pencil-fill" />
</ModalToggleButton>
</>}
}
async fn do_add_ingredient_recipe(
token: String,
household: Uuid,
recipe: i64,
ingredient: i64,
amount: f64,
) -> anyhow::Result<()> {
let rsp = gloo_net::http::Request::put(api!(
"household/{household}/recipe/{recipe}/ingredients/{ingredient}"
))
.json(&AddRecipeIngredientRequest { amount })?
.header("Authorization", &format!("Bearer {token}"))
.send()
.await?;
if !rsp.ok() {
let body = rsp.text().await.unwrap_or_default();
anyhow::bail!("Could not get recipes (code={}): {body}", rsp.status());
}
Ok(())
}
#[derive(Clone, PartialEq, Properties)]
struct AddIngredientProps {
token: String,
household: Uuid,
recipe: i64,
update: Callback<()>,
}
#[function_component]
fn AddIngredientInner(props: &AddIngredientProps) -> HtmlResult {
let error = use_state(|| None::<String>);
let amount = use_state(|| None::<f64>);
let selected_ig = use_state(|| None::<(i64, IngredientInfo)>);
let s_ig = selected_ig.clone();
let am = amount.clone();
let err = error.clone();
let token = props.token.clone();
let household = props.household;
let recipe = props.recipe;
let update = props.update.clone();
let on_submit = Callback::from(move |_| match &*s_ig {
&Some((id, _)) => match &*am {
&Some(amount) => {
let fut = do_add_ingredient_recipe(token.clone(), household, recipe, id, amount);
let am = am.clone();
let s_ig = s_ig.clone();
let err = err.clone();
let update = update.clone();
wasm_bindgen_futures::spawn_local(async move {
match fut.await {
Ok(_) => {
err.set(None);
am.set(None);
s_ig.set(None);
update.emit(());
let modal = bs::Modal::get_instance("#rcpEditNewIg");
modal.hide();
}
Err(e) => {
err.set(Some(format!("Could not add ingredient: {e}")));
}
}
});
}
None => {
err.set(Some("Amount can't be empty".into()));
}
},
None => {
err.set(Some("Ingredient does not exist".into()));
}
});
let on_ig_change = {
let selected_ig = selected_ig.clone();
Callback::from(move |v| {
selected_ig.set(v);
})
};
let on_amount_change = {
let amount = amount.clone();
Callback::from(move |v| {
amount.set(Some(v));
})
};
Ok({
html! {<>
<FormModal
id="rcpEditNewIg"
fade=true
centered=true
submit_label="Add"
{on_submit}
title="Add ingredient"
>
if let Some(e) = &*error {
<div class={classes!("alert", "alert-danger")} role="alert">
{e}
</div>
}
<IngredientSelectBase
token={props.token.clone()}
household={props.household}
{on_ig_change}
{on_amount_change}
amount={*amount}
ig_select={(*selected_ig).as_ref().map(|(_, info)| AttrValue::from(info.name.clone()))}
/>
</FormModal>
<ModalToggleButton modal_id="rcpEditNewIg" classes={classes!("btn", "btn-secondary")}>
{"Add Ingredient"}
</ModalToggleButton>
</>}
})
}
#[function_component]
fn AddIngredient(props: &AddIngredientProps) -> Html {
let fallback = html! {
<div class="spinner-border" role="status">
<span class="visually-hidden">{"Loading ..."}</span>
</div>
};
html! {<>
<Suspense {fallback}>
<AddIngredientInner
token={props.token.clone()}
household={props.household}
recipe={props.recipe}
update={props.update.clone()}
/>
</Suspense>
</>}
}
async fn do_delete_ig(
token: String,
household: Uuid,
recipe: i64,
ingredient: i64,
) -> anyhow::Result<()> {
let rsp = gloo_net::http::Request::delete(api!(
"household/{household}/recipe/{recipe}/ingredients/{ingredient}"
))
.header("Authorization", &format!("Bearer {token}"))
.send()
.await?;
if !rsp.ok() {
let body = rsp.text().await.unwrap_or_default();
anyhow::bail!("Could not get recipes (code={}): {body}", rsp.status());
}
Ok(())
}
async fn do_edit_steps(
token: String,
household: Uuid,
recipe: i64,
steps: String,
) -> anyhow::Result<()> {
let rsp = gloo_net::http::Request::patch(api!("household/{household}/recipe/{recipe}/steps"))
.json(&RecipeEditStepsRequest { steps })?
.header("Authorization", &format!("Bearer {token}"))
.send()
.await?;
if !rsp.ok() {
let body = rsp.text().await.unwrap_or_default();
anyhow::bail!("Could not get recipes (code={}): {body}", rsp.status());
}
Ok(())
}
#[derive(Properties, PartialEq, Clone)]
struct EditStepsProps {
token: String,
household: Uuid,
recipe: i64,
steps: String,
update: Callback<()>,
}
#[function_component]
fn EditSteps(props: &EditStepsProps) -> Html {
let steps = use_state(|| props.steps.clone());
let error = use_state(|| None::<String>);
let onchange = {
let steps = steps.clone();
Callback::from(move |e: Event| {
let Some(target) = e.target() else {
return;
};
let Ok(target) = target.dyn_into::<HtmlTextAreaElement>() else {
return;
};
steps.set(target.value());
})
};
let on_submit = {
let steps = steps.clone();
let token = props.token.clone();
let household = props.household;
let recipe = props.recipe;
let error = error.clone();
let update = props.update.clone();
Callback::from(move |_| {
let token = token.clone();
let steps = steps.clone();
let error = error.clone();
let update = update.clone();
wasm_bindgen_futures::spawn_local(async move {
match do_edit_steps(token.clone(), household, recipe, (*steps).clone()).await {
Ok(_) => {
let modal = bs::Modal::get_instance("#rcpEditSteps");
modal.hide();
error.set(None);
update.emit(());
}
Err(e) => {
error.set(Some(format!("Could not edit steps: {e}")));
}
}
});
})
};
html! {<>
<ModalToggleButton modal_id="rcpEditSteps" classes={classes!("btn", "btn-secondary", "mb-2")}>
{"Edit Steps"}
</ModalToggleButton>
<FormModal
id="rcpEditSteps"
fade=true
centered=true
submit_label="Edit"
title="Edit steps"
{on_submit}
>
if let Some(e) = &*error {
<div class={classes!("alert", "alert-danger")} role="alert">
{e}
</div>
}
<textarea
class="form-control"
value={(*steps).clone()}
{onchange}
/>
</FormModal>
</>}
}
async fn do_edit_rating(
token: String,
household: Uuid,
recipe: i64,
rating: u8,
) -> anyhow::Result<()> {
let rsp = gloo_net::http::Request::patch(api!("household/{household}/recipe/{recipe}/rating"))
.json(&RecipeEditRating { rating })?
.header("Authorization", &format!("Bearer {token}"))
.send()
.await?;
if !rsp.ok() {
let body = rsp.text().await.unwrap_or_default();
anyhow::bail!("Could not edit rating (code={}): {body}", rsp.status());
}
Ok(())
}
#[derive(Properties, PartialEq, Clone)]
struct EditRatingProps {
token: String,
household: Uuid,
recipe: i64,
rating: u8,
update: Callback<()>,
}
#[function_component]
fn EditRating(props: &EditRatingProps) -> Html {
let rating = use_state(|| props.rating);
let error = use_state(|| None::<String>);
let onchange = {
let rating = rating.clone();
Callback::from(move |e: Event| {
let Some(target) = e.target() else {
return;
};
let Ok(target) = target.dyn_into::<HtmlInputElement>() else {
return;
};
if !target.report_validity() {
return;
}
rating.set(target.value().parse().expect("invalid number"));
})
};
let on_submit = {
let rating = rating.clone();
let token = props.token.clone();
let household = props.household;
let recipe = props.recipe;
let error = error.clone();
let update = props.update.clone();
Callback::from(move |_| {
let token = token.clone();
let rating = rating.clone();
let error = error.clone();
let update = update.clone();
wasm_bindgen_futures::spawn_local(async move {
match do_edit_rating(token.clone(), household, recipe, *rating - 1).await {
Ok(_) => {
let modal = bs::Modal::get_instance("#rcpEditRating");
modal.hide();
error.set(None);
update.emit(());
}
Err(e) => {
error.set(Some(format!("Could not edit rating: {e}")));
}
}
});
})
};
html! {<>
<ModalToggleButton modal_id="rcpEditRating" classes={classes!("btn", "btn-secondary", "ms-2")}>
{"Edit Rating"}
</ModalToggleButton>
<FormModal
id="rcpEditRating"
fade=true
centered=true
submit_label="Edit"
title="Edit rating"
{on_submit}
>
if let Some(e) = &*error {
<div class={classes!("alert", "alert-danger")} role="alert">
{e}
</div>
}
<div class="form-floating">
<input
type="number"
max="3"
min="1"
class="form-control"
id="rcpEditRatingInp"
value={(*rating).to_string()}
{onchange}
/>
<label for="rcpEditRatingInp">{"Rating"}</label>
</div>
</FormModal>
</>}
}
async fn do_edit_person_count(
token: String,
household: Uuid,
recipe: i64,
person_count: u32,
) -> anyhow::Result<()> {
let rsp =
gloo_net::http::Request::patch(api!("household/{household}/recipe/{recipe}/person_count"))
.json(&RecipeEditPersonCount { person_count })?
.header("Authorization", &format!("Bearer {token}"))
.send()
.await?;
if !rsp.ok() {
let body = rsp.text().await.unwrap_or_default();
anyhow::bail!(
"Could not edit person_count (code={}): {body}",
rsp.status()
);
}
Ok(())
}
#[derive(Properties, PartialEq, Clone)]
struct EditPersonCountProps {
token: String,
household: Uuid,
recipe: i64,
person_count: u32,
}
#[function_component]
fn EditPersonCount(props: &EditPersonCountProps) -> Html {
let person_count = use_state(|| props.person_count);
let error = use_state(|| None::<String>);
let onchange = {
let person_count = person_count.clone();
Callback::from(move |e: Event| {
let Some(target) = e.target() else {
return;
};
let Ok(target) = target.dyn_into::<HtmlInputElement>() else {
return;
};
if !target.report_validity() {
return;
}
person_count.set(target.value().parse().expect("invalid number"));
})
};
let on_submit = {
let person_count = person_count.clone();
let token = props.token.clone();
let household = props.household;
let recipe = props.recipe;
let error = error.clone();
Callback::from(move |_| {
let token = token.clone();
let person_count = person_count.clone();
let error = error.clone();
wasm_bindgen_futures::spawn_local(async move {
match do_edit_person_count(token.clone(), household, recipe, *person_count)
.await
{
Ok(_) => {
let modal = bs::Modal::get_instance("#rcpEditPersonCount");
modal.hide();
error.set(None);
}
Err(e) => {
error.set(Some(format!("Could not edit person count: {e}")));
}
}
});
})
};
html! {<>
<ModalToggleButton modal_id="rcpEditPersonCount" classes={classes!("btn", "btn-secondary")}>
{"Edit"}
</ModalToggleButton>
<FormModal
id="rcpEditPersonCount"
fade=true
centered=true
submit_label="Edit"
title="Edit default person count"
{on_submit}
>
if let Some(e) = &*error {
<div class={classes!("alert", "alert-danger")} role="alert">
{e}
</div>
}
<div class="form-floating">
<input
type="number"
min="1"
class="form-control"
id="rcpEditPersonCountInp"
value={(*person_count).to_string()}
{onchange}
/>
<label for="rcpEditPersonCountInp">{"Default person count"}</label>
</div>
</FormModal>
</>}
}
#[derive(Properties, PartialEq, Clone)]
struct RecipeInfoProps {
token: String,
household: Uuid,
update: Callback<()>,
recipe_id: i64,
info: Rc<RecipeInfo>,
}
#[function_component]
fn RecipeInfoView(props: &RecipeInfoProps) -> Html {
let error = use_state(|| None::<String>);
let mk_del_ig = |&id| {
let update = props.update.clone();
let token = props.token.clone();
let household = props.household;
let recipe = props.recipe_id;
let err = error.clone();
Callback::from(move |_| {
let update = update.clone();
let future = do_delete_ig(token.clone(), household, recipe, id);
let err = err.clone();
wasm_bindgen_futures::spawn_local(async move {
match future.await {
Err(e) => {
err.set(Some(format!("Could not delete ingredient: {e}")));
}
Ok(_) => {
update.emit(());
err.set(None);
}
}
})
})
};
let person_count = use_state(|| props.info.person_count);
let onchange = {
let person_count = person_count.clone();
Callback::from(move |e: Event| {
let Some(target) = e.target() else {
return;
};
let Ok(target) = target.dyn_into::<HtmlInputElement>() else {
return;
};
let Ok(pc) = target.value().parse() else {
return;
};
person_count.set(pc);
})
};
html! {<>
<h1>{&props.info.name} <RecipeRating rating={props.info.rating} /> </h1>
<div class="mt-2">
<EditName
token={props.token.clone()}
id={props.recipe_id}
household={props.household}
name={props.info.name.clone()}
update={props.update.clone()}
/>
<EditRating
token={props.token.clone()}
recipe={props.recipe_id}
household={props.household}
rating={props.info.rating + 1}
update={props.update.clone()}
/>
</div>
<div class="mt-2 container text-start">
<div class="row">
<div class="col-8">
<div class="input-group">
<input
class="form-control"
type="number"
id="rcpPeopleCount"
min="1"
value={person_count.to_string()}
{onchange}
/>
<span class="input-group-text">{"people"}</span>
</div>
</div>
<div class="col">
<EditPersonCount
token={props.token.clone()}
recipe={props.recipe_id}
household={props.household}
person_count={props.info.person_count}
/>
</div>
</div>
</div>
if let Some(e) = &*error {
<div class={classes!("alert", "alert-danger")} role="alert">
{e}
</div>
}
<hr />
<div class="text-start">
<h2>{"Ingredients"}</h2>
<ul class="list-group mb-2">
{for props.info.ingredients.iter().map(|(id, info, amount)| {
let delete_modal_id = format!("rcpRmIg{id}");
let amount_rounded = (amount * (*person_count) as f64).round();
html!{
<li
key={*id}
class="list-group-item d-flex justify-content-between align-items-center"
>
{format!("{amount_rounded}{} {}", info.unit.as_deref().unwrap_or(""), info.name)}
<div>
<ConfirmDangerModal
id={delete_modal_id.clone()}
title="Remove ingredient"
centered=true
on_confirm={mk_del_ig(id)}
>
{format!("Are you sure you to delete '{}'", info.name)}
</ConfirmDangerModal>
<ModalToggleButton
modal_id={delete_modal_id}
classes={classes!("btn", "btn-danger", "me-1")}
>
<i class="bi-trash3" />
</ModalToggleButton>
<EditIngredient
token={props.token.clone()}
recipe={props.recipe_id}
ingredient={*id}
household={props.household}
amount={*amount}
update={props.update.clone()}
/>
</div>
</li>
}})}
</ul>
<AddIngredient
token={props.token.clone()}
household={props.household}
recipe={props.recipe_id}
update={props.update.clone()}
/>
</div>
<hr />
<div class="text-start">
<h2>{"Steps"}</h2>
<ul class="list-group list-group-flush">
{for props.info.steps.split('\n').map(|text| html!{
<li class="list-group-item">{text}</li>
})}
</ul>
<EditSteps
token={props.token.clone()}
household={props.household}
recipe={props.recipe_id}
steps={props.info.steps.clone()}
update={props.update.clone()}
/>
</div>
</>}
}
#[function_component]
fn RecipeViewerInner(props: &RecipeViewerInnerProps) -> HtmlResult {
let recipe_render = use_state(|| 0u64);
let recipe = use_future_with_deps(
|_| fetch_recipe(props.token.clone(), props.household, props.id),
*recipe_render,
)?;
let update = Callback::from(move |_| {
recipe_render.set((*recipe_render).wrapping_add(1));
});
Ok(match &*recipe {
Ok(r) => {
html! {
<RecipeInfoView
token={props.token.clone()}
recipe_id={props.id}
info={r.clone()}
household={props.household}
{update}
/>
}
}
Err(e) => html! {
<div class={classes!("alert", "alert-danger")} role="alert">
{format!("Error fetching recipe: {e}")}
</div>
},
})
}
#[function_component]
pub fn RecipeViewer(props: &RecipeViewerProps) -> Html {
let global_state = use_state(RegaladeGlobalState::get);
let fallback = html! {
<div class="spinner-border" role="status">
<span class="visually-hidden">{"Loading ..."}</span>
</div>
};
html! {
<div class="d-flex align-items-center justify-content-center w-100">
<div class={classes!("container", "text-center", "rounded", "border", "pt-2", "m-2")}>
<Suspense {fallback}>
<RecipeViewerInner
id={props.id}
token={global_state.token.token.clone()}
household={global_state.household.id}
/>
</Suspense>
</div>
</div>
}
}