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 uuid::Uuid;
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,
household: Uuid,
name: String,
unit: String,
) -> anyhow::Result<()> {
) -> anyhow::Result<CreateIngredientResponse> {
let rsp = gloo_net::http::Request::post(api!("household/{household}/ingredients"))
.header("Authorization", &format!("Bearer {token}"))
.json(&CreateIngredientRequest {
@ -263,7 +263,7 @@ async fn do_add_ingredient(
}
}
Ok(())
Ok(rsp.json().await?)
}
#[function_component]

View file

@ -4,12 +4,12 @@ use api::{CreateRecipeRequest, CreateRecipeResponse, IngredientInfo};
use uuid::Uuid;
use wasm_bindgen::JsCast;
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 crate::{
api,
bootstrap::{ModalBody, ModalFooter, ModalToggleButton, TitledModal},
bootstrap::{bs, FormModal, ModalBody, ModalFooter, ModalToggleButton, TitledModal},
RegaladeGlobalState, Route,
};
@ -44,11 +44,16 @@ pub(super) struct IngredientSelectBaseProps {
pub children: Children,
pub amount: Option<f64>,
pub ig_select: Option<AttrValue>,
#[prop_or_default]
pub refresh: u64,
}
#[function_component]
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 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)]
struct IngredientSelectProps {
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! {<>
if let Some(e) = &*error {
<div class={classes!("alert", "alert-danger")} role="alert">
{e}
</div>
}
<RecipeCreateIngredient
token={props.token.clone()}
household={props.household}
{on_ig_add}
/>
<Suspense {fallback}>
<IngredientSelectBase
token={props.token.clone()}
@ -236,10 +416,14 @@ fn IngredientSelect(props: &IngredientSelectProps) -> Html {
{on_amount_change}
amount={*amount}
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"}
</button>
<button class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#newRcpCreateIg">
{"Create"}
</button>
</IngredientSelectBase>
</Suspense>
</>}