app: Isolate RecipeInfoView in a separate component

This commit is contained in:
traxys 2023-06-30 12:46:31 +02:00
parent 5b44cc7dde
commit 8cf5803c1e

View file

@ -1,3 +1,5 @@
use std::rc::Rc;
use api::{ use api::{
AddRecipeIngredientRequest, IngredientInfo, RecipeEditRating, RecipeEditStepsRequest, AddRecipeIngredientRequest, IngredientInfo, RecipeEditRating, RecipeEditStepsRequest,
RecipeInfo, RecipeIngredientEditRequest, RecipeRenameRequest, RecipeInfo, RecipeIngredientEditRequest, RecipeRenameRequest,
@ -128,7 +130,7 @@ struct RecipeViewerInnerProps {
household: Uuid, household: Uuid,
} }
async fn fetch_recipe(token: String, household: Uuid, id: i64) -> anyhow::Result<RecipeInfo> { 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}")) let rsp = gloo_net::http::Request::get(api!("household/{household}/recipe/{id}"))
.header("Authorization", &format!("Bearer {token}")) .header("Authorization", &format!("Bearer {token}"))
.send() .send()
@ -139,7 +141,7 @@ async fn fetch_recipe(token: String, household: Uuid, id: i64) -> anyhow::Result
anyhow::bail!("Could not get recipe (code={}): {body}", rsp.status()); anyhow::bail!("Could not get recipe (code={}): {body}", rsp.status());
} }
Ok(rsp.json().await?) Ok(Rc::new(rsp.json().await?))
} }
#[derive(Clone, PartialEq, Properties)] #[derive(Clone, PartialEq, Properties)]
@ -756,25 +758,23 @@ fn EditRating(props: &EditRatingProps) -> Html {
</>} </>}
} }
#[derive(Properties, PartialEq, Clone)]
struct RecipeInfoProps {
token: String,
household: Uuid,
update: Callback<()>,
recipe_id: i64,
info: Rc<RecipeInfo>,
}
#[function_component] #[function_component]
fn RecipeViewerInner(props: &RecipeViewerInnerProps) -> HtmlResult { fn RecipeInfoView(props: &RecipeInfoProps) -> Html {
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));
});
let error = use_state(|| None::<String>); let error = use_state(|| None::<String>);
let mk_del_ig = |&id| { let mk_del_ig = |&id| {
let update = update.clone(); let update = props.update.clone();
let token = props.token.clone(); let token = props.token.clone();
let household = props.household; let household = props.household;
let recipe = props.id; let recipe = props.recipe_id;
let err = error.clone(); let err = error.clone();
Callback::from(move |_| { Callback::from(move |_| {
let update = update.clone(); let update = update.clone();
@ -794,94 +794,119 @@ fn RecipeViewerInner(props: &RecipeViewerInnerProps) -> HtmlResult {
}) })
}; };
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>
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.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(match &*recipe {
Ok(r) => html! {<> Ok(r) => {
<h1>{&r.name} <RecipeRating rating={r.rating} /> </h1> html! {
<div class="mt-2"> <RecipeInfoView
<EditName
token={props.token.clone()} token={props.token.clone()}
id={props.id} recipe_id={props.id}
info={r.clone()}
household={props.household} household={props.household}
name={r.name.clone()} {update}
update={update.clone()}
/> />
<EditRating
token={props.token.clone()}
recipe={props.id}
household={props.household}
rating={r.rating + 1}
update={update.clone()}
/>
</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 r.ingredients.iter().map(|(id, info, amount)| {
let delete_modal_id = format!("rcpRmIg{id}");
let amount_rounded = amount.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.id}
ingredient={*id}
household={props.household}
amount={*amount}
update={update.clone()}
/>
</div>
</li>
}})}
</ul>
<AddIngredient
token={props.token.clone()}
household={props.household}
recipe={props.id}
update={update.clone()}
/>
</div>
<hr />
<div class="text-start">
<h2>{"Steps"}</h2>
<ul class="list-group list-group-flush">
{for r.steps.split('\n').map(|text| html!{
<li class="list-group-item">{text}</li>
})}
</ul>
<EditSteps
token={props.token.clone()}
household={props.household}
recipe={props.id}
steps={r.steps.clone()}
update={update}
/>
</div>
</>},
Err(e) => html! { Err(e) => html! {
<div class={classes!("alert", "alert-danger")} role="alert"> <div class={classes!("alert", "alert-danger")} role="alert">
{format!("Error fetching recipe: {e}")} {format!("Error fetching recipe: {e}")}