app: Add a UI to create recipes
This commit is contained in:
parent
58cadd37d2
commit
cec3ef214e
6 changed files with 564 additions and 2 deletions
43
Cargo.lock
generated
43
Cargo.lock
generated
|
|
@ -75,6 +75,7 @@ dependencies = [
|
||||||
"gloo-net",
|
"gloo-net",
|
||||||
"gloo-storage",
|
"gloo-storage",
|
||||||
"gloo-utils",
|
"gloo-utils",
|
||||||
|
"im",
|
||||||
"itertools",
|
"itertools",
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -379,6 +380,15 @@ version = "1.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitmaps"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitvec"
|
name = "bitvec"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
|
@ -1446,6 +1456,20 @@ dependencies = [
|
||||||
"unicode-normalization",
|
"unicode-normalization",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "im"
|
||||||
|
version = "15.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9"
|
||||||
|
dependencies = [
|
||||||
|
"bitmaps",
|
||||||
|
"rand_core",
|
||||||
|
"rand_xoshiro",
|
||||||
|
"sized-chunks",
|
||||||
|
"typenum",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "implicit-clone"
|
name = "implicit-clone"
|
||||||
version = "0.3.5"
|
version = "0.3.5"
|
||||||
|
|
@ -2226,6 +2250,15 @@ dependencies = [
|
||||||
"getrandom",
|
"getrandom",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_xoshiro"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa"
|
||||||
|
dependencies = [
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
|
|
@ -2793,6 +2826,16 @@ version = "0.1.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a"
|
checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sized-chunks"
|
||||||
|
version = "0.6.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e"
|
||||||
|
dependencies = [
|
||||||
|
"bitmaps",
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.8"
|
version = "0.4.8"
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ console_log = { version = "1.0.0", features = ["color"] }
|
||||||
gloo-net = "0.2.6"
|
gloo-net = "0.2.6"
|
||||||
gloo-storage = "0.2.2"
|
gloo-storage = "0.2.2"
|
||||||
gloo-utils = "0.1.6"
|
gloo-utils = "0.1.6"
|
||||||
|
im = "15.1.0"
|
||||||
itertools = "0.10.5"
|
itertools = "0.10.5"
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
serde = { version = "1.0.163", features = ["derive"] }
|
serde = { version = "1.0.163", features = ["derive"] }
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use crate::{
|
||||||
RegaladeGlobalState,
|
RegaladeGlobalState,
|
||||||
};
|
};
|
||||||
|
|
||||||
async fn fetch_ingredients(token: String, household: Uuid) -> anyhow::Result<api::IngredientList> {
|
pub async fn fetch_ingredients(token: String, household: Uuid) -> anyhow::Result<api::IngredientList> {
|
||||||
let rsp = gloo_net::http::Request::get(api!("household/{household}/ingredients"))
|
let rsp = gloo_net::http::Request::get(api!("household/{household}/ingredients"))
|
||||||
.header("Authorization", &format!("Bearer {token}"))
|
.header("Authorization", &format!("Bearer {token}"))
|
||||||
.send()
|
.send()
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,9 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
mod bootstrap;
|
mod bootstrap;
|
||||||
mod sidebar;
|
|
||||||
mod ingredients;
|
mod ingredients;
|
||||||
|
mod recipe_creator;
|
||||||
|
mod sidebar;
|
||||||
|
|
||||||
const API_ROUTE: &str = match option_env!("REGALADE_API_SERVER_BASE") {
|
const API_ROUTE: &str = match option_env!("REGALADE_API_SERVER_BASE") {
|
||||||
None => "http://localhost:8085",
|
None => "http://localhost:8085",
|
||||||
|
|
@ -45,6 +46,10 @@ enum Route {
|
||||||
Ingredients,
|
Ingredients,
|
||||||
#[at("/household_select")]
|
#[at("/household_select")]
|
||||||
HouseholdSelect,
|
HouseholdSelect,
|
||||||
|
#[at("/new_recipe")]
|
||||||
|
NewRecipe,
|
||||||
|
#[at("/recipe/:id")]
|
||||||
|
Recipe { id: i64 },
|
||||||
#[at("/404")]
|
#[at("/404")]
|
||||||
#[not_found]
|
#[not_found]
|
||||||
NotFound,
|
NotFound,
|
||||||
|
|
@ -468,6 +473,16 @@ fn switch(route: Route) -> Html {
|
||||||
Route::HouseholdSelect => html! {
|
Route::HouseholdSelect => html! {
|
||||||
<HouseholdSelection />
|
<HouseholdSelection />
|
||||||
},
|
},
|
||||||
|
Route::NewRecipe => html! {
|
||||||
|
<GlobalStateRedirector {route}>
|
||||||
|
<recipe_creator::RecipeCreator />
|
||||||
|
</GlobalStateRedirector>
|
||||||
|
},
|
||||||
|
Route::Recipe { id } => html! {
|
||||||
|
<GlobalStateRedirector {route}>
|
||||||
|
{format!("RECIPE {id}")}
|
||||||
|
</GlobalStateRedirector>
|
||||||
|
},
|
||||||
Route::NotFound => html! {
|
Route::NotFound => html! {
|
||||||
"Page not found"
|
"Page not found"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
498
app/src/recipe_creator.rs
Normal file
498
app/src/recipe_creator.rs
Normal file
|
|
@ -0,0 +1,498 @@
|
||||||
|
use std::{collections::BTreeMap, rc::Rc};
|
||||||
|
|
||||||
|
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_router::prelude::use_navigator;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
api,
|
||||||
|
bootstrap::{ModalBody, ModalFooter, ModalToggleButton, TitledModal},
|
||||||
|
RegaladeGlobalState, Route,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct RecipeIngredient {
|
||||||
|
id: i64,
|
||||||
|
info: IngredientInfo,
|
||||||
|
amount: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_ingredients(
|
||||||
|
token: String,
|
||||||
|
household: Uuid,
|
||||||
|
) -> anyhow::Result<Rc<BTreeMap<String, (i64, IngredientInfo)>>> {
|
||||||
|
let list = crate::ingredients::fetch_ingredients(token, household).await?;
|
||||||
|
|
||||||
|
Ok(Rc::new(
|
||||||
|
list.ingredients
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| (v.name.clone(), (k, v)))
|
||||||
|
.collect(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Properties, Clone)]
|
||||||
|
struct IngredientSelectProps {
|
||||||
|
token: String,
|
||||||
|
household: Uuid,
|
||||||
|
onselect: Callback<RecipeIngredient>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
fn IngredientSelect(props: &IngredientSelectProps) -> HtmlResult {
|
||||||
|
let ingredients = use_future(|| fetch_ingredients(props.token.clone(), props.household))?;
|
||||||
|
let ingredient = use_state(|| None::<(i64, IngredientInfo)>);
|
||||||
|
|
||||||
|
let input_value_h = use_state(String::new);
|
||||||
|
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: HtmlInputElement = document
|
||||||
|
.get_element_by_id("igAmount")
|
||||||
|
.unwrap()
|
||||||
|
.dyn_into()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
err.set(Some("Ingredient does not exist".into()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
match &*ingredients {
|
||||||
|
Ok(ig) => {
|
||||||
|
let igc = ig.clone();
|
||||||
|
let selected_ig = ingredient.clone();
|
||||||
|
|
||||||
|
let onchange = Callback::from(move |e: Event| {
|
||||||
|
let Some(target) = e.target() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let ip = input_value_h.clone();
|
||||||
|
let Ok(target) = target.dyn_into::<HtmlInputElement>() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
ip.set(target.value());
|
||||||
|
|
||||||
|
match igc.get(&target.value()) {
|
||||||
|
Some(info) => {
|
||||||
|
selected_ig.set(Some(info.clone()));
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
selected_ig.set(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(html! {
|
||||||
|
<div class="d-flex flex-column align-items-start">
|
||||||
|
if let Some(e) = &*error {
|
||||||
|
<div class={classes!("alert", "alert-danger")} role="alert">
|
||||||
|
{e}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<script src="/awesomplete.min.js" async=true></script>
|
||||||
|
<div class="d-flex align-items-center mb-1">
|
||||||
|
<label for="igSelect" class="pe-1">{"Name:"}</label>
|
||||||
|
<input
|
||||||
|
class="awesomplete form-control"
|
||||||
|
list="igList"
|
||||||
|
value={input_value}
|
||||||
|
{onchange}
|
||||||
|
id="igSelect"
|
||||||
|
/>
|
||||||
|
<datalist id="igList">
|
||||||
|
{ for ig.keys().map(|k| html!{<option key={k.clone()}>{k}</option>}) }
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary ms-1" {onclick}>{"Add"}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(e) => Ok(html! {
|
||||||
|
<div class={classes!("alert", "alert-danger")} role="alert">
|
||||||
|
{format!("Could not load ingredients: {e:?}")}
|
||||||
|
</div>
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn do_create_recipe(
|
||||||
|
token: String,
|
||||||
|
household: Uuid,
|
||||||
|
request: CreateRecipeRequest,
|
||||||
|
) -> anyhow::Result<i64> {
|
||||||
|
let rsp = gloo_net::http::Request::post(api!("household/{household}/recipe"))
|
||||||
|
.header("Authorization", &format!("Bearer {token}"))
|
||||||
|
.json(&request)?
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !rsp.ok() {
|
||||||
|
let body = rsp.text().await.unwrap_or_default();
|
||||||
|
anyhow::bail!("Could not post recipe (code={}): {body}", rsp.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
let rsp: CreateRecipeResponse = rsp.json().await?;
|
||||||
|
|
||||||
|
Ok(rsp.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn RecipeCreator() -> Html {
|
||||||
|
let current_rating = use_state(|| 0u8);
|
||||||
|
let global_state = use_state(RegaladeGlobalState::get);
|
||||||
|
let person_count = use_state(|| 1u8);
|
||||||
|
|
||||||
|
let mk_rating_oninput = |id| {
|
||||||
|
let current_rating = current_rating.clone();
|
||||||
|
Callback::from(move |_| {
|
||||||
|
current_rating.set(id);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let fallback = html! {"Loading Ingredients ..."};
|
||||||
|
|
||||||
|
let ingredients = use_state(im::Vector::new);
|
||||||
|
let ig = ingredients.clone();
|
||||||
|
let onselect = Callback::from(move |rcp_ig: RecipeIngredient| {
|
||||||
|
let mut ingredients = (*ig).clone();
|
||||||
|
ingredients.push_back(rcp_ig);
|
||||||
|
ig.set(ingredients);
|
||||||
|
});
|
||||||
|
|
||||||
|
let mk_ig_delete = |idx| {
|
||||||
|
let ig = ingredients.clone();
|
||||||
|
Callback::from(move |_| {
|
||||||
|
let mut ingredients = (*ig).clone();
|
||||||
|
ingredients.remove(idx);
|
||||||
|
ig.set(ingredients);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let steps = use_state(im::Vector::new);
|
||||||
|
|
||||||
|
let s = steps.clone();
|
||||||
|
let on_add_step = Callback::from(move |_| {
|
||||||
|
let mut steps = (*s).clone();
|
||||||
|
steps.push_back(String::new());
|
||||||
|
s.set(steps);
|
||||||
|
});
|
||||||
|
|
||||||
|
let mk_step_del = |idx| {
|
||||||
|
let s = steps.clone();
|
||||||
|
Callback::from(move |_| {
|
||||||
|
let mut steps = (*s).clone();
|
||||||
|
steps.remove(idx);
|
||||||
|
s.set(steps);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let mk_step_change = |idx| {
|
||||||
|
let s = steps.clone();
|
||||||
|
Callback::from(move |e: Event| {
|
||||||
|
let Some(target) = e.target() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(target) = target.dyn_into::<HtmlTextAreaElement>() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut steps = (*s).clone();
|
||||||
|
steps[idx] = target.value();
|
||||||
|
s.set(steps);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let name = use_state(String::new);
|
||||||
|
|
||||||
|
let nm = name.clone();
|
||||||
|
let on_name_change = Callback::from(move |e: Event| {
|
||||||
|
let Some(target) = e.target() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(target) = target.dyn_into::<HtmlInputElement>() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
nm.set(target.value());
|
||||||
|
});
|
||||||
|
|
||||||
|
let error = use_state(|| Option::None::<String>);
|
||||||
|
|
||||||
|
let s = steps.clone();
|
||||||
|
let ig = ingredients.clone();
|
||||||
|
let rtg = current_rating.clone();
|
||||||
|
let nm = name.clone();
|
||||||
|
let err = error.clone();
|
||||||
|
let token = global_state.token.token.clone();
|
||||||
|
let household = global_state.household.id;
|
||||||
|
let pc = person_count.clone();
|
||||||
|
let nav = use_navigator().unwrap();
|
||||||
|
let new_rcp_submit = Callback::from(move |_| {
|
||||||
|
if nm.is_empty() {
|
||||||
|
err.set(Some("Name can't be empty".into()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let s = s.clone();
|
||||||
|
let ig = ig.clone();
|
||||||
|
let rtg = rtg.clone();
|
||||||
|
let nm = nm.clone();
|
||||||
|
let err = err.clone();
|
||||||
|
let token = token.clone();
|
||||||
|
let pc = pc.clone();
|
||||||
|
let nav = nav.clone();
|
||||||
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
|
match do_create_recipe(
|
||||||
|
token,
|
||||||
|
household,
|
||||||
|
CreateRecipeRequest {
|
||||||
|
name: (*nm).clone(),
|
||||||
|
rating: (*rtg),
|
||||||
|
ingredients: ig
|
||||||
|
.iter()
|
||||||
|
.map(|rcp_ig: &RecipeIngredient| {
|
||||||
|
(rcp_ig.id, rcp_ig.amount as f64 / (*pc) as f64)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
steps: (*s).iter().cloned().collect(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(id) => {
|
||||||
|
s.set(Default::default());
|
||||||
|
ig.set(Default::default());
|
||||||
|
rtg.set(Default::default());
|
||||||
|
nm.set(Default::default());
|
||||||
|
err.set(None);
|
||||||
|
|
||||||
|
nav.push(&Route::Recipe { id });
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
err.set(Some(format!("Error creating recipe: {e:?}")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let pc = person_count.clone();
|
||||||
|
let on_person_count_change = Callback::from(move |e: Event| {
|
||||||
|
let Some(target) = e.target() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(target) = target.dyn_into::<HtmlInputElement>() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !target.report_validity() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pc.set(target.value().parse().unwrap());
|
||||||
|
});
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div class="d-flex align-items-center justify-content-center w-100">
|
||||||
|
<div class="container rounded border py-2 m-2 text-center">
|
||||||
|
<h1>{"Create a new recipe"}</h1>
|
||||||
|
if let Some(e) = &*error {
|
||||||
|
<div class={classes!("alert", "alert-danger")} role="alert">
|
||||||
|
{e}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<hr />
|
||||||
|
<div class="form-floating">
|
||||||
|
<input
|
||||||
|
id="newRcpName"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Name"
|
||||||
|
value={name.to_string()}
|
||||||
|
onchange={on_name_change}
|
||||||
|
/>
|
||||||
|
<label for="newRcpName">{"Name"}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating">
|
||||||
|
<input
|
||||||
|
id="newRcpPersonCount"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Person Count"
|
||||||
|
type="number"
|
||||||
|
min=0
|
||||||
|
onchange={on_person_count_change}
|
||||||
|
value={person_count.to_string()}
|
||||||
|
/>
|
||||||
|
<label for="newRcpPersonCount">{"Person Count"}</label>
|
||||||
|
</div>
|
||||||
|
<div class="pt-2">
|
||||||
|
{"Rating: "}
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="ratingOptions"
|
||||||
|
id="likeRating"
|
||||||
|
checked={*current_rating == 0}
|
||||||
|
oninput={mk_rating_oninput(0)}
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="likeRating">{"Like"}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="ratingOptions"
|
||||||
|
id="likeLotRating"
|
||||||
|
checked={*current_rating == 1}
|
||||||
|
oninput={mk_rating_oninput(1)}
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="likeLotRating">{"Like a lot"}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
name="ratingOptions"
|
||||||
|
id="loveRating"
|
||||||
|
checked={*current_rating == 2}
|
||||||
|
oninput={mk_rating_oninput(2)}
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="loveRating">{"Love"}</label>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
<ul class="list-group list-group-flush text-start">
|
||||||
|
{for (*ingredients).iter().enumerate().map(|(idx, ig)| {
|
||||||
|
html!{
|
||||||
|
<li
|
||||||
|
class={classes!(
|
||||||
|
"list-group-item",
|
||||||
|
"d-flex",
|
||||||
|
"justify-content-between",
|
||||||
|
"align-items-center",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{format!("{}{} {}",
|
||||||
|
ig.amount,
|
||||||
|
ig.info.unit.as_deref().unwrap_or(""),
|
||||||
|
ig.info.name,
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
class="btn btn-danger"
|
||||||
|
onclick={mk_ig_delete(idx)}
|
||||||
|
>
|
||||||
|
{"Remove"}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>{"Steps"}</h2>
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
{for (*steps).iter().enumerate().map(|(idx, step)| {
|
||||||
|
html!{
|
||||||
|
<li class="list-group-item">
|
||||||
|
<textarea
|
||||||
|
class="form-control"
|
||||||
|
value={step.clone()}
|
||||||
|
onchange={mk_step_change(idx)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="btn btn-danger mt-1"
|
||||||
|
onclick={mk_step_del(idx)}
|
||||||
|
>
|
||||||
|
{"Remove"}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
<button class="btn btn-primary" onclick={on_add_step}>{"Add Step"}</button>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<ModalToggleButton classes={classes!("btn", "btn-lg", "btn-primary")} modal_id="newRcpModal">
|
||||||
|
{"Create Recipe"}
|
||||||
|
</ModalToggleButton>
|
||||||
|
<TitledModal id="newRcpModal" fade=true centered=true title="Create Recipe">
|
||||||
|
<ModalBody>
|
||||||
|
{"Do you confirm this recipe ?"}
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<button type="button" class={classes!("btn", "btn-secondary")} data-bs-dismiss="modal">
|
||||||
|
{"Cancel"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={classes!("btn", "btn-primary")}
|
||||||
|
data-bs-dismiss="modal"
|
||||||
|
onclick={new_rcp_submit}
|
||||||
|
>
|
||||||
|
{"Confirm"}
|
||||||
|
</button>
|
||||||
|
</ModalFooter>
|
||||||
|
</TitledModal>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -440,6 +440,11 @@ pub(crate) fn RegaladeSidebar(props: &RegaladeSidebarProps) -> Html {
|
||||||
icon: "bi-egg-fill",
|
icon: "bi-egg-fill",
|
||||||
page: Route::Ingredients,
|
page: Route::Ingredients,
|
||||||
},
|
},
|
||||||
|
MenuEntry {
|
||||||
|
label: "New Recipe",
|
||||||
|
icon: "bi-clipboard2-plus-fill",
|
||||||
|
page: Route::NewRecipe,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue