app,server: Allow to add ingredients to existing recipes

This commit is contained in:
traxys 2023-06-25 18:48:49 +02:00
parent 6004520fb9
commit 27f9295aa3
5 changed files with 319 additions and 69 deletions

View file

@ -1,4 +1,7 @@
use api::{RecipeInfo, RecipeIngredientEditRequest, RecipeRenameRequest};
use api::{
AddRecipeIngredientRequest, IngredientInfo, RecipeInfo, RecipeIngredientEditRequest,
RecipeRenameRequest,
};
use itertools::Itertools;
use uuid::Uuid;
use wasm_bindgen::JsCast;
@ -12,6 +15,7 @@ use yew_router::prelude::*;
use crate::{
api,
bootstrap::{bs, ConfirmDangerModal, FormModal, ModalToggleButton},
recipe_creator::IngredientSelectBase,
RegaladeGlobalState, Route,
};
@ -350,6 +354,150 @@ fn EditIngredient(props: &EditIngredientProps) -> Html {
</>}
}
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,
@ -427,7 +575,7 @@ fn RecipeViewerInner(props: &RecipeViewerInnerProps) -> HtmlResult {
<hr />
<div class="text-start">
<h2>{"Ingredients"}</h2>
<ul class="list-group">
<ul class="list-group mb-2">
{for r.ingredients.iter().map(|(id, info, amount)| {
let delete_modal_id = format!("rcpRmIg{id}");
html!{
@ -463,6 +611,12 @@ fn RecipeViewerInner(props: &RecipeViewerInnerProps) -> HtmlResult {
</li>
}})}
</ul>
<AddIngredient
token={props.token.clone()}
household={props.household}
recipe={props.id}
update={update.clone()}
/>
</div>
<hr />
<div class="text-start">

View file

@ -14,10 +14,10 @@ use crate::{
};
#[derive(Clone)]
struct RecipeIngredient {
pub(super) struct RecipeIngredient {
id: i64,
info: IngredientInfo,
amount: u64,
amount: f64,
}
async fn fetch_ingredients(
@ -35,66 +35,57 @@ async fn fetch_ingredients(
}
#[derive(PartialEq, Properties, Clone)]
struct IngredientSelectProps {
token: String,
household: Uuid,
onselect: Callback<RecipeIngredient>,
pub(super) struct IngredientSelectBaseProps {
pub token: String,
pub household: Uuid,
pub on_amount_change: Callback<f64>,
pub on_ig_change: Callback<Option<(i64, IngredientInfo)>>,
#[prop_or_default]
pub children: Children,
pub amount: Option<f64>,
pub ig_select: Option<AttrValue>,
}
#[function_component]
fn IngredientSelect(props: &IngredientSelectProps) -> HtmlResult {
pub(super) fn IngredientSelectBase(props: &IngredientSelectBaseProps) -> HtmlResult {
let ingredients = use_future(|| fetch_ingredients(props.token.clone(), props.household))?;
let ingredient = use_state(|| None::<(i64, IngredientInfo)>);
let unit = use_state(|| None::<String>);
let input_value_h = use_state(String::new);
let input_value_h = use_state(|| {
props
.ig_select
.as_ref()
.map(|v| v.to_string())
.unwrap_or_default()
});
let input_value = input_value_h.to_string();
let error = use_state(|| None::<String>);
let selected_ig = ingredient.clone();
let on_select = props.onselect.clone();
let err = error.clone();
let onclick = Callback::from(move |_| match &*selected_ig {
Some((id, info)) => {
let document = gloo_utils::document();
let amount_change = props.on_amount_change.clone();
let am_change = Callback::from(move |e: Event| {
let Some(target) = e.target() else {
return;
};
let amount: HtmlInputElement = document
.get_element_by_id("igAmount")
.unwrap()
.dyn_into()
.unwrap();
let Ok(target) = target.dyn_into::<HtmlInputElement>() else {
return;
};
if !amount.report_validity() {
err.set(Some("Invalid ingredient amount".into()));
return;
}
let amount = amount.value();
match amount.is_empty() {
false => {
on_select.emit(RecipeIngredient {
id: *id,
info: info.clone(),
amount: amount.parse().unwrap(),
});
err.set(None);
}
true => {
err.set(Some("Amount can't be empty".into()));
}
}
if !target.report_validity() {
return;
}
None => {
err.set(Some("Ingredient does not exist".into()));
if let Ok(value) = target.value().parse() {
amount_change.emit(value);
}
});
match &*ingredients {
Ok(ig) => {
let igc = ig.clone();
let selected_ig = ingredient.clone();
let u = unit.clone();
let on_ig_change = props.on_ig_change.clone();
let onchange = Callback::from(move |e: Event| {
let Some(target) = e.target() else {
return;
@ -109,10 +100,12 @@ fn IngredientSelect(props: &IngredientSelectProps) -> HtmlResult {
match igc.get(&target.value()) {
Some(info) => {
selected_ig.set(Some(info.clone()));
on_ig_change.emit(Some(info.clone()));
u.set(info.1.unit.clone());
}
None => {
selected_ig.set(None);
on_ig_change.emit(None);
u.set(None);
}
}
});
@ -141,14 +134,19 @@ fn IngredientSelect(props: &IngredientSelectProps) -> HtmlResult {
<div class="d-flex mw-50 align-items-center">
<label for="igAmount" class="px-1">{"Amount: "}</label>
<div class="input-group">
<input class="form-control" type="number" id="igAmount" min="0" />
if let Some(ig) = &*ingredient {
if let Some(unit) = &ig.1.unit {
<span class="input-group-text">{unit}</span>
}
<input
class="form-control"
type="number"
id="igAmount"
min="0"
value={props.amount.map(|v| v.to_string())}
onchange={am_change}
/>
if let Some(unit) = &*unit {
<span class="input-group-text">{unit}</span>
}
</div>
<button class="btn btn-primary ms-1" {onclick}>{"Add"}</button>
{props.children.clone()}
</div>
</div>
})
@ -161,6 +159,86 @@ fn IngredientSelect(props: &IngredientSelectProps) -> HtmlResult {
}
}
#[derive(PartialEq, Properties, Clone)]
struct IngredientSelectProps {
token: String,
household: Uuid,
onselect: Callback<RecipeIngredient>,
}
#[function_component]
fn IngredientSelect(props: &IngredientSelectProps) -> Html {
let on_select = props.onselect.clone();
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 onclick = Callback::from(move |_| match &*s_ig {
Some((id, info)) => match &*am {
&Some(amount) => {
on_select.emit(RecipeIngredient {
id: *id,
info: info.clone(),
amount,
});
err.set(None);
}
None => {
err.set(Some("Amount can't be empty".into()));
}
},
None => {
err.set(Some("Ingredient does not exist".into()));
}
});
let fallback = html! {
<div class="spinner-border" role="status">
<span class="visually-hidden">{"Loading ..."}</span>
</div>
};
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));
})
};
html! {<>
if let Some(e) = &*error {
<div class={classes!("alert", "alert-danger")} role="alert">
{e}
</div>
}
<Suspense {fallback}>
<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()))}
>
<button class="btn btn-primary" {onclick}>
{"Add"}
</button>
</IngredientSelectBase>
</Suspense>
</>}
}
async fn do_create_recipe(
token: String,
household: Uuid,
@ -195,8 +273,6 @@ pub fn RecipeCreator() -> Html {
})
};
let fallback = html! {"Loading Ingredients ..."};
let ingredients = use_state(im::Vector::new);
let ig = ingredients.clone();
let onselect = Callback::from(move |rcp_ig: RecipeIngredient| {
@ -298,9 +374,7 @@ pub fn RecipeCreator() -> Html {
rating: (*rtg),
ingredients: ig
.iter()
.map(|rcp_ig: &RecipeIngredient| {
(rcp_ig.id, rcp_ig.amount as f64 / (*pc) as f64)
})
.map(|rcp_ig: &RecipeIngredient| (rcp_ig.id, rcp_ig.amount / (*pc) as f64))
.collect(),
steps: (*s).iter().cloned().collect(),
},
@ -410,13 +484,11 @@ pub fn RecipeCreator() -> Html {
</div>
<div class="d-flex flex-column justify-content-start">
<h2>{"Ingredients"}</h2>
<Suspense {fallback}>
<IngredientSelect
token={global_state.token.token.clone()}
household={global_state.household.id}
{onselect}
/>
</Suspense>
<IngredientSelect
token={global_state.token.token.clone()}
household={global_state.household.id}
{onselect}
/>
<ul class="list-group list-group-flush text-start">
{for (*ingredients).iter().enumerate().map(|(idx, ig)| {
html!{