app: Allow to create ingredients in the recipe creator

This commit is contained in:
traxys 2023-06-30 18:12:05 +02:00
parent ce29b7b75d
commit 725c2ff3be
2 changed files with 192 additions and 8 deletions

View file

@ -1,4 +1,4 @@
use api::{CreateIngredientRequest, EditIngredientRequest, IngredientInfo}; use api::{CreateIngredientRequest, EditIngredientRequest, IngredientInfo, CreateIngredientResponse};
use itertools::Itertools; use itertools::Itertools;
use uuid::Uuid; use uuid::Uuid;
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
@ -240,12 +240,12 @@ fn IngredientList(props: &IngredientListProps) -> HtmlResult {
}) })
} }
async fn do_add_ingredient( pub async fn do_add_ingredient(
token: String, token: String,
household: Uuid, household: Uuid,
name: String, name: String,
unit: String, unit: String,
) -> anyhow::Result<()> { ) -> anyhow::Result<CreateIngredientResponse> {
let rsp = gloo_net::http::Request::post(api!("household/{household}/ingredients")) let rsp = gloo_net::http::Request::post(api!("household/{household}/ingredients"))
.header("Authorization", &format!("Bearer {token}")) .header("Authorization", &format!("Bearer {token}"))
.json(&CreateIngredientRequest { .json(&CreateIngredientRequest {
@ -263,7 +263,7 @@ async fn do_add_ingredient(
} }
} }
Ok(()) Ok(rsp.json().await?)
} }
#[function_component] #[function_component]

View file

@ -4,12 +4,12 @@ use api::{CreateRecipeRequest, CreateRecipeResponse, IngredientInfo};
use uuid::Uuid; use uuid::Uuid;
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
use web_sys::{HtmlInputElement, HtmlTextAreaElement}; use web_sys::{HtmlInputElement, HtmlTextAreaElement};
use yew::{prelude::*, suspense::use_future}; use yew::{prelude::*, suspense::use_future_with_deps};
use yew_router::prelude::use_navigator; use yew_router::prelude::use_navigator;
use crate::{ use crate::{
api, api,
bootstrap::{ModalBody, ModalFooter, ModalToggleButton, TitledModal}, bootstrap::{bs, FormModal, ModalBody, ModalFooter, ModalToggleButton, TitledModal},
RegaladeGlobalState, Route, RegaladeGlobalState, Route,
}; };
@ -44,11 +44,16 @@ pub(super) struct IngredientSelectBaseProps {
pub children: Children, pub children: Children,
pub amount: Option<f64>, pub amount: Option<f64>,
pub ig_select: Option<AttrValue>, pub ig_select: Option<AttrValue>,
#[prop_or_default]
pub refresh: u64,
} }
#[function_component] #[function_component]
pub(super) fn IngredientSelectBase(props: &IngredientSelectBaseProps) -> HtmlResult { pub(super) fn IngredientSelectBase(props: &IngredientSelectBaseProps) -> HtmlResult {
let ingredients = use_future(|| fetch_ingredients(props.token.clone(), props.household))?; let ingredients = use_future_with_deps(
|_| fetch_ingredients(props.token.clone(), props.household),
props.refresh,
)?;
let unit = use_state(|| None::<String>); let unit = use_state(|| None::<String>);
let input_value_h = use_state(|| { let input_value_h = use_state(|| {
@ -165,6 +170,166 @@ pub(super) fn IngredientSelectBase(props: &IngredientSelectBaseProps) -> HtmlRes
} }
} }
#[derive(PartialEq, Clone, Properties)]
struct RecipeCreateIngredientProps {
token: String,
household: Uuid,
on_ig_add: Callback<RecipeIngredient>,
}
#[function_component]
fn RecipeCreateIngredient(props: &RecipeCreateIngredientProps) -> Html {
let error = use_state(|| None::<String>);
let amount = use_state(|| 1);
let name = use_state(String::new);
let unit = use_state(String::new);
let get_target = |e: Event| match e.target() {
None => None,
Some(e) => e.dyn_into::<HtmlInputElement>().ok(),
};
let on_name_change = {
let name = name.clone();
let error = error.clone();
Callback::from(move |e| {
let Some(tgt) = get_target(e) else {
return;
};
let value = tgt.value();
if value.is_empty() {
error.set(Some("Name can't be empty".into()));
return;
}
name.set(value);
})
};
let on_unit_change = {
let unit = unit.clone();
Callback::from(move |e| {
let Some(tgt) = get_target(e) else {
return;
};
unit.set(tgt.value());
})
};
let on_amount_change = {
let amount = amount.clone();
Callback::from(move |e| {
let Some(tgt) = get_target(e) else {
return;
};
if !tgt.report_validity() {
return;
}
amount.set(tgt.value().parse().expect("amount not a number"));
})
};
let on_submit = {
let name = name.clone();
let unit = unit.clone();
let amount = amount.clone();
let token = props.token.clone();
let household = props.household;
let error = error.clone();
let on_ig_add = props.on_ig_add.clone();
Callback::from(move |_| {
let fut = super::ingredients::do_add_ingredient(
token.clone(),
household,
name.to_string(),
unit.to_string(),
);
let error = error.clone();
let info = IngredientInfo {
name: name.to_string(),
unit: (!unit.is_empty()).then(|| unit.to_string()),
};
let amount = amount.clone();
let on_ig_add = on_ig_add.clone();
wasm_bindgen_futures::spawn_local(async move {
match fut.await {
Ok(rsp) => {
on_ig_add.emit(RecipeIngredient {
id: rsp.id,
info,
amount: *amount as f64,
});
error.set(None);
let modal = bs::Modal::get_instance("#newRcpCreateIg");
modal.hide();
}
Err(e) => {
error.set(Some(format!("Could not add ingredient: {e}")));
}
}
});
})
};
html! {
<FormModal
id="newRcpCreateIg"
fade=true
centered=true
submit_label="Create & Add"
title="Create & Add 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="newRcpCreateIgNameInp"
placeholder={"Name"}
value={name.to_string()}
onchange={on_name_change}
/>
<label for="newRcpCreateIgNameInp">{"Ingredient Name"}</label>
</div>
<div class="form-floating my-1">
<input
class="form-control"
id="newRcpCreateIgUnitInp"
placeholder={"Unit"}
value={unit.to_string()}
onchange={on_unit_change}
/>
<label for="newRcpCreateIgUnitInp">{"Ingredient Unit"}</label>
</div>
<div class="form-floating">
<input
class="form-control"
type="number"
min="1"
id="newRcpCreateIgAmountInp"
placeholder={"Amount"}
value={amount.to_string()}
onchange={on_amount_change}
/>
<label for="newRcpCreateIgAmountInp">{"Ingredient Amount"}</label>
</div>
</FormModal>
}
}
#[derive(PartialEq, Properties, Clone)] #[derive(PartialEq, Properties, Clone)]
struct IngredientSelectProps { struct IngredientSelectProps {
token: String, token: String,
@ -222,12 +387,27 @@ fn IngredientSelect(props: &IngredientSelectProps) -> Html {
}) })
}; };
let ingredient_refresh = use_state(|| 0u64);
let on_ig_add = {
let on_select = props.onselect.clone();
let ingredient_refresh = ingredient_refresh.clone();
Callback::from(move |info| {
on_select.emit(info);
ingredient_refresh.set(ingredient_refresh.wrapping_add(1));
})
};
html! {<> html! {<>
if let Some(e) = &*error { if let Some(e) = &*error {
<div class={classes!("alert", "alert-danger")} role="alert"> <div class={classes!("alert", "alert-danger")} role="alert">
{e} {e}
</div> </div>
} }
<RecipeCreateIngredient
token={props.token.clone()}
household={props.household}
{on_ig_add}
/>
<Suspense {fallback}> <Suspense {fallback}>
<IngredientSelectBase <IngredientSelectBase
token={props.token.clone()} token={props.token.clone()}
@ -236,10 +416,14 @@ fn IngredientSelect(props: &IngredientSelectProps) -> Html {
{on_amount_change} {on_amount_change}
amount={*amount} amount={*amount}
ig_select={(*selected_ig).as_ref().map(|(_, info)| AttrValue::from(info.name.clone()))} ig_select={(*selected_ig).as_ref().map(|(_, info)| AttrValue::from(info.name.clone()))}
refresh={*ingredient_refresh}
> >
<button class="btn btn-primary" {onclick}> <button class="btn btn-primary me-1" {onclick}>
{"Add"} {"Add"}
</button> </button>
<button class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#newRcpCreateIg">
{"Create"}
</button>
</IngredientSelectBase> </IngredientSelectBase>
</Suspense> </Suspense>
</>} </>}