diff --git a/api/src/lib.rs b/api/src/lib.rs index 02fc42c..8958dd7 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -118,3 +118,8 @@ pub struct RecipeRenameRequest { pub struct RecipeIngredientEditRequest { pub amount: f64, } + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct AddRecipeIngredientRequest { + pub amount: f64, +} diff --git a/app/src/recipe.rs b/app/src/recipe.rs index f0b3f43..e828a76 100644 --- a/app/src/recipe.rs +++ b/app/src/recipe.rs @@ -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::); + + let amount = use_state(|| None::); + 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! {<> + + if let Some(e) = &*error { + + } + + + + {"Add Ingredient"} + + } + }) +} + +#[function_component] +fn AddIngredient(props: &AddIngredientProps) -> Html { + let fallback = html! { +
+ {"Loading ..."} +
+ }; + + html! {<> + + + + } +} + async fn do_delete_ig( token: String, household: Uuid, @@ -427,7 +575,7 @@ fn RecipeViewerInner(props: &RecipeViewerInnerProps) -> HtmlResult {

{"Ingredients"}

-
    +
      {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 { }})}
    +

diff --git a/app/src/recipe_creator.rs b/app/src/recipe_creator.rs index af17b59..fffbdad 100644 --- a/app/src/recipe_creator.rs +++ b/app/src/recipe_creator.rs @@ -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, +pub(super) struct IngredientSelectBaseProps { + pub token: String, + pub household: Uuid, + pub on_amount_change: Callback, + pub on_ig_change: Callback>, + #[prop_or_default] + pub children: Children, + pub amount: Option, + pub ig_select: Option, } #[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::); - 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::); - 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::() 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 {
- - if let Some(ig) = &*ingredient { - if let Some(unit) = &ig.1.unit { - {unit} - } + + if let Some(unit) = &*unit { + {unit} }
- + {props.children.clone()}
}) @@ -161,6 +159,86 @@ fn IngredientSelect(props: &IngredientSelectProps) -> HtmlResult { } } +#[derive(PartialEq, Properties, Clone)] +struct IngredientSelectProps { + token: String, + household: Uuid, + onselect: Callback, +} + +#[function_component] +fn IngredientSelect(props: &IngredientSelectProps) -> Html { + let on_select = props.onselect.clone(); + let error = use_state(|| None::); + + let amount = use_state(|| None::); + 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! { +
+ {"Loading ..."} +
+ }; + + 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 { + + } + + + + + + } +} + 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 {

{"Ingredients"}

- - - +
    {for (*ingredients).iter().enumerate().map(|(idx, ig)| { html!{ diff --git a/src/routes/mod.rs b/src/routes/mod.rs index a359519..d458891 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -232,6 +232,7 @@ pub(crate) fn router(api_allowed: Option) -> Router { "/household/:house_id/recipe/:recipe_id/ingredients/:iid", patch(recipe::edit_ig_amount) .delete(recipe::delete_ig) - .layer(mk_service(vec![Method::PATCH, Method::DELETE])), + .put(recipe::add_ig_request) + .layer(mk_service(vec![Method::PATCH, Method::DELETE, Method::PUT])), ) } diff --git a/src/routes/recipe.rs b/src/routes/recipe.rs index f15abe8..6ea3a70 100644 --- a/src/routes/recipe.rs +++ b/src/routes/recipe.rs @@ -1,6 +1,7 @@ use api::{ - CreateRecipeRequest, CreateRecipeResponse, EmptyResponse, IngredientInfo, ListRecipesResponse, - RecipeInfo, RecipeIngredientEditRequest, RecipeRenameRequest, + AddRecipeIngredientRequest, CreateRecipeRequest, CreateRecipeResponse, EmptyResponse, + IngredientInfo, ListRecipesResponse, RecipeInfo, RecipeIngredientEditRequest, + RecipeRenameRequest, }; use axum::{ async_trait, @@ -208,3 +209,20 @@ pub(super) async fn delete_ig( Ok(EmptyResponse {}.into()) } + +pub(super) async fn add_ig_request( + State(state): State, + RecipeExtractor(recipe): RecipeExtractor, + IngredientExtractor(ingredient): IngredientExtractor, + Json(req): Json, +) -> JsonResult { + let model = recipe_ingerdients::ActiveModel { + recipe_id: ActiveValue::Set(recipe.id), + ingredient_id: ActiveValue::Set(ingredient.id), + amount: ActiveValue::Set(req.amount), + }; + + model.insert(&state.db).await?; + + Ok(EmptyResponse {}.into()) +}