app: Allow to create ingredients in the recipe creator
This commit is contained in:
parent
ce29b7b75d
commit
725c2ff3be
2 changed files with 192 additions and 8 deletions
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
</>}
|
</>}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue