2023-06-25 16:11:26 +02:00
|
|
|
use api::{RecipeInfo, RecipeRenameRequest};
|
2023-06-25 14:12:09 +02:00
|
|
|
use uuid::Uuid;
|
2023-06-25 16:11:26 +02:00
|
|
|
use wasm_bindgen::JsCast;
|
|
|
|
|
use web_sys::HtmlInputElement;
|
|
|
|
|
use yew::{
|
|
|
|
|
prelude::*,
|
|
|
|
|
suspense::{use_future, use_future_with_deps},
|
|
|
|
|
};
|
2023-06-25 14:12:09 +02:00
|
|
|
use yew_router::prelude::*;
|
|
|
|
|
|
2023-06-25 16:11:26 +02:00
|
|
|
use crate::{
|
|
|
|
|
api,
|
|
|
|
|
bootstrap::{FormModal, ModalToggleButton, bs},
|
|
|
|
|
RegaladeGlobalState, Route,
|
|
|
|
|
};
|
2023-06-25 14:12:09 +02:00
|
|
|
|
|
|
|
|
async fn get_all_recipes(
|
|
|
|
|
token: String,
|
|
|
|
|
household: Uuid,
|
|
|
|
|
) -> anyhow::Result<api::ListRecipesResponse> {
|
|
|
|
|
let rsp = gloo_net::http::Request::get(api!("household/{household}/recipe"))
|
|
|
|
|
.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(rsp.json().await?)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Properties)]
|
|
|
|
|
struct RecipeListProps {
|
|
|
|
|
token: String,
|
|
|
|
|
household: Uuid,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[function_component]
|
|
|
|
|
fn RecipeListInner(props: &RecipeListProps) -> HtmlResult {
|
|
|
|
|
let state = use_future(|| get_all_recipes(props.token.clone(), props.household))?;
|
|
|
|
|
|
|
|
|
|
Ok(match &*state {
|
|
|
|
|
Ok(l) => html! {
|
|
|
|
|
<div class="container text-center">
|
2023-06-25 15:13:15 +02:00
|
|
|
<div class="row row-cols-2 row-cols-sm-2 row-cols-md-4 g-2 mb-2">
|
2023-06-25 14:12:09 +02:00
|
|
|
{for l.recipes.iter().map(|(id, name)| html!{
|
|
|
|
|
<div class="col" key={*id}>
|
|
|
|
|
<div class="p-3 border rounded border-light-subtle h-100">
|
|
|
|
|
<Link<Route>
|
|
|
|
|
classes={classes!(
|
|
|
|
|
"link-light",
|
|
|
|
|
"link-offset-2",
|
|
|
|
|
"link-underline-opacity-25",
|
|
|
|
|
"link-underline-opacity-100-hover",
|
|
|
|
|
)}
|
|
|
|
|
to={Route::Recipe { id: *id }}
|
|
|
|
|
>
|
|
|
|
|
{name}
|
|
|
|
|
</Link<Route>>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
},
|
|
|
|
|
Err(e) => html! {
|
|
|
|
|
<div class={classes!("alert", "alert-danger")} role="alert">
|
|
|
|
|
{format!("Error fetching recipes: {e}")}
|
|
|
|
|
</div>
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[function_component]
|
|
|
|
|
pub fn RecipeList() -> Html {
|
|
|
|
|
let fallback = html! {"Loading ..."};
|
|
|
|
|
let global_state = use_state(RegaladeGlobalState::get);
|
|
|
|
|
|
|
|
|
|
html! {
|
|
|
|
|
<div class="d-flex align-items-center justify-content-center w-100">
|
|
|
|
|
<div class={classes!("container", "text-center", "rounded", "border", "pt-2", "m-2")}>
|
|
|
|
|
<h2>{"Recipes"}</h2>
|
|
|
|
|
<Suspense {fallback}>
|
|
|
|
|
<RecipeListInner
|
|
|
|
|
token={global_state.token.token.clone()}
|
|
|
|
|
household={global_state.household.id}
|
|
|
|
|
/>
|
|
|
|
|
</Suspense>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-06-25 15:13:15 +02:00
|
|
|
|
|
|
|
|
#[derive(PartialEq, Eq, Clone, Properties)]
|
|
|
|
|
pub struct RecipeViewerProps {
|
|
|
|
|
pub id: i64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(PartialEq, Eq, Clone, Properties)]
|
|
|
|
|
struct RecipeViewerInnerProps {
|
|
|
|
|
id: i64,
|
|
|
|
|
token: String,
|
|
|
|
|
household: Uuid,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn fetch_recipe(token: String, household: Uuid, id: i64) -> anyhow::Result<RecipeInfo> {
|
|
|
|
|
let rsp = gloo_net::http::Request::get(api!("household/{household}/recipe/{id}"))
|
|
|
|
|
.header("Authorization", &format!("Bearer {token}"))
|
|
|
|
|
.send()
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
if !rsp.ok() {
|
|
|
|
|
let body = rsp.text().await.unwrap_or_default();
|
|
|
|
|
anyhow::bail!("Could not get recipe (code={}): {body}", rsp.status());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(rsp.json().await?)
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-25 16:11:26 +02:00
|
|
|
#[derive(Clone, PartialEq, Properties)]
|
|
|
|
|
struct EditNameProps {
|
|
|
|
|
token: String,
|
|
|
|
|
household: Uuid,
|
|
|
|
|
id: i64,
|
|
|
|
|
name: String,
|
|
|
|
|
update: Callback<()>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn do_rename_recipe(
|
|
|
|
|
token: String,
|
|
|
|
|
household: Uuid,
|
|
|
|
|
recipe: i64,
|
|
|
|
|
name: String,
|
|
|
|
|
) -> anyhow::Result<()> {
|
|
|
|
|
let rsp = gloo_net::http::Request::patch(api!("household/{household}/recipe/{recipe}"))
|
|
|
|
|
.json(&RecipeRenameRequest { name })?
|
|
|
|
|
.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(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[function_component]
|
|
|
|
|
fn EditName(props: &EditNameProps) -> Html {
|
|
|
|
|
let name = use_state(|| props.name.clone());
|
|
|
|
|
|
|
|
|
|
let nm = name.clone();
|
|
|
|
|
let onchange = 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(|| None::<String>);
|
|
|
|
|
|
|
|
|
|
let nm = name.clone();
|
|
|
|
|
let err = error.clone();
|
|
|
|
|
let token = props.token.clone();
|
|
|
|
|
let household = props.household;
|
|
|
|
|
let recipe = props.id;
|
|
|
|
|
let update = props.update.clone();
|
|
|
|
|
let on_submit = Callback::from(move |_| {
|
|
|
|
|
if nm.is_empty() {
|
|
|
|
|
err.set(Some("Name can't be empty".into()));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let future = do_rename_recipe(token.clone(), household, recipe, (*nm).clone());
|
|
|
|
|
|
|
|
|
|
let err = err.clone();
|
|
|
|
|
let update = update.clone();
|
|
|
|
|
wasm_bindgen_futures::spawn_local(async move {
|
|
|
|
|
match future.await {
|
|
|
|
|
Ok(_) => {
|
|
|
|
|
let modal = bs::Modal::get_instance("#rcpEditName");
|
|
|
|
|
modal.hide();
|
|
|
|
|
|
|
|
|
|
err.set(None);
|
|
|
|
|
update.emit(());
|
|
|
|
|
},
|
|
|
|
|
Err(e) => {
|
|
|
|
|
err.set(Some(format!("Could not edit name: {e}")));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
html! {<>
|
|
|
|
|
<ModalToggleButton classes={classes!("btn", "btn-secondary")} modal_id="rcpEditName">
|
|
|
|
|
{"Edit name"}
|
|
|
|
|
</ModalToggleButton>
|
|
|
|
|
<FormModal
|
|
|
|
|
id="rcpEditName"
|
|
|
|
|
fade=true
|
|
|
|
|
centered=true
|
|
|
|
|
submit_label="Edit"
|
|
|
|
|
title="Edit Name"
|
|
|
|
|
{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="rcpEditNameInp"
|
|
|
|
|
placeholder={(*name).clone()}
|
|
|
|
|
value={(*name).clone()}
|
|
|
|
|
{onchange}
|
|
|
|
|
/>
|
|
|
|
|
<label for="rcpEditNameInp">{"Recipe Name"}</label>
|
|
|
|
|
</div>
|
|
|
|
|
</FormModal>
|
|
|
|
|
</>}
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-25 15:13:15 +02:00
|
|
|
#[function_component]
|
|
|
|
|
fn RecipeViewerInner(props: &RecipeViewerInnerProps) -> HtmlResult {
|
2023-06-25 16:11:26 +02:00
|
|
|
let recipe_render = use_state(|| 0u64);
|
|
|
|
|
let recipe = use_future_with_deps(
|
|
|
|
|
|_| fetch_recipe(props.token.clone(), props.household, props.id),
|
|
|
|
|
*recipe_render,
|
|
|
|
|
)?;
|
|
|
|
|
|
|
|
|
|
let update = Callback::from(move |_| {
|
|
|
|
|
recipe_render.set((*recipe_render).wrapping_add(1));
|
|
|
|
|
});
|
2023-06-25 15:13:15 +02:00
|
|
|
|
|
|
|
|
Ok(match &*recipe {
|
|
|
|
|
Ok(r) => html! {<>
|
|
|
|
|
<h1>{&r.name}</h1>
|
2023-06-25 16:11:26 +02:00
|
|
|
<EditName
|
|
|
|
|
token={props.token.clone()}
|
|
|
|
|
id={props.id}
|
|
|
|
|
household={props.household}
|
|
|
|
|
name={r.name.clone()}
|
|
|
|
|
{update}
|
|
|
|
|
/>
|
2023-06-25 15:13:15 +02:00
|
|
|
<hr />
|
|
|
|
|
<div class="text-start">
|
|
|
|
|
<h2>{"Ingredients"}</h2>
|
|
|
|
|
<ul class="list-group">
|
|
|
|
|
{for r.ingredients.iter().map(|(id, info, amount)| html!{
|
|
|
|
|
<li key={*id} class="list-group-item">
|
|
|
|
|
{format!("{amount}{} {}", info.unit.as_deref().unwrap_or(""), info.name)}
|
|
|
|
|
</li>
|
|
|
|
|
})}
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
<hr />
|
|
|
|
|
<div class="text-start">
|
|
|
|
|
<h2>{"Steps"}</h2>
|
|
|
|
|
<ul class="list-group list-group-flush">
|
|
|
|
|
{for r.steps.iter().map(|text| html!{
|
|
|
|
|
<li class="list-group-item">{text}</li>
|
|
|
|
|
})}
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
</>},
|
|
|
|
|
Err(e) => html! {
|
|
|
|
|
<div class={classes!("alert", "alert-danger")} role="alert">
|
|
|
|
|
{format!("Error fetching recipe: {e}")}
|
|
|
|
|
</div>
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[function_component]
|
|
|
|
|
pub fn RecipeViewer(props: &RecipeViewerProps) -> Html {
|
|
|
|
|
let global_state = use_state(RegaladeGlobalState::get);
|
|
|
|
|
let fallback = html! {
|
|
|
|
|
<div class="spinner-border" role="status">
|
|
|
|
|
<span class="visually-hidden">{"Loading ..."}</span>
|
|
|
|
|
</div>
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
html! {
|
|
|
|
|
<div class="d-flex align-items-center justify-content-center w-100">
|
|
|
|
|
<div class={classes!("container", "text-center", "rounded", "border", "pt-2", "m-2")}>
|
|
|
|
|
<Suspense {fallback}>
|
|
|
|
|
<RecipeViewerInner
|
|
|
|
|
id={props.id}
|
|
|
|
|
token={global_state.token.token.clone()}
|
|
|
|
|
household={global_state.household.id}
|
|
|
|
|
/>
|
|
|
|
|
</Suspense>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
}
|