diff --git a/api/src/lib.rs b/api/src/lib.rs index 1dbc720..e046b1b 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -108,3 +108,8 @@ pub struct RecipeInfo { pub steps: Vec, pub ingredients: Vec<(i64, IngredientInfo, f64)>, } + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct RecipeRenameRequest { + pub name: String, +} diff --git a/app/src/recipe.rs b/app/src/recipe.rs index dd07948..ab87406 100644 --- a/app/src/recipe.rs +++ b/app/src/recipe.rs @@ -1,9 +1,18 @@ -use api::RecipeInfo; +use api::{RecipeInfo, RecipeRenameRequest}; use uuid::Uuid; -use yew::{prelude::*, suspense::use_future}; +use wasm_bindgen::JsCast; +use web_sys::HtmlInputElement; +use yew::{ + prelude::*, + suspense::{use_future, use_future_with_deps}, +}; use yew_router::prelude::*; -use crate::{api, RegaladeGlobalState, Route}; +use crate::{ + api, + bootstrap::{FormModal, ModalToggleButton, bs}, + RegaladeGlobalState, Route, +}; async fn get_all_recipes( token: String, @@ -110,13 +119,139 @@ async fn fetch_recipe(token: String, household: Uuid, id: i64) -> anyhow::Result Ok(rsp.json().await?) } +#[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::() else { + return; + }; + + nm.set(target.value()); + }); + + let error = use_state(|| None::); + + 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! {<> + + {"Edit name"} + + + if let Some(e) = &*error { + + } +
+ + +
+
+ } +} + #[function_component] fn RecipeViewerInner(props: &RecipeViewerInnerProps) -> HtmlResult { - let recipe = use_future(|| fetch_recipe(props.token.clone(), props.household, props.id))?; + 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)); + }); Ok(match &*recipe { Ok(r) => html! {<>

{&r.name}

+

{"Ingredients"}

diff --git a/src/routes/mod.rs b/src/routes/mod.rs index ab5d313..ac10426 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -224,6 +224,8 @@ pub(crate) fn router(api_allowed: Option) -> Router { ) .route( "/household/:house_id/recipe/:recipe_id", - get(recipe::fetch_recipe).layer(mk_service(vec![Method::GET])), + get(recipe::fetch_recipe) + .patch(recipe::edit_name) + .layer(mk_service(vec![Method::GET, Method::PATCH])), ) } diff --git a/src/routes/recipe.rs b/src/routes/recipe.rs index 2fc4a9d..42bbcba 100644 --- a/src/routes/recipe.rs +++ b/src/routes/recipe.rs @@ -1,8 +1,11 @@ use api::{ - CreateRecipeRequest, CreateRecipeResponse, IngredientInfo, ListRecipesResponse, RecipeInfo, + CreateRecipeRequest, CreateRecipeResponse, EmptyResponse, IngredientInfo, ListRecipesResponse, + RecipeInfo, RecipeRenameRequest, }; use axum::{ - extract::{Path, State}, + async_trait, + extract::{FromRef, FromRequestParts, Path, State}, + http::request::Parts, Json, }; use sea_orm::{prelude::*, ActiveValue, TransactionTrait}; @@ -83,23 +86,46 @@ pub(super) async fn list_recipes( .into()) } -#[derive(serde::Deserialize)] -pub(super) struct RecipeId { - recipe_id: i64, +pub(super) struct RecipeExtractor(recipe::Model); + +#[async_trait] +impl FromRequestParts for RecipeExtractor +where + S: Send + Sync, + AppState: FromRef, +{ + type Rejection = RouteError; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + let State(app_state): State = State::from_request_parts(parts, state) + .await + .expect("No state"); + + #[derive(serde::Deserialize)] + pub(super) struct RecipeId { + recipe_id: i64, + } + + let household = AuthorizedHousehold::from_request_parts(parts, state).await?; + let Path(recipe): Path = Path::from_request_parts(parts, state).await?; + + match household + .0 + .find_related(Recipe) + .filter(recipe::Column::Id.eq(recipe.recipe_id)) + .one(&app_state.db) + .await? + { + None => Err(RouteError::RessourceNotFound), + Some(r) => Ok(Self(r)), + } + } } pub(super) async fn fetch_recipe( - AuthorizedHousehold(household): AuthorizedHousehold, State(state): State, - Path(RecipeId { recipe_id }): Path, + RecipeExtractor(recipe): RecipeExtractor, ) -> JsonResult { - let Some(recipe) = household - .find_related(Recipe) - .filter(recipe::Column::Id.eq(recipe_id)) - .one(&state.db).await? else { - return Err(RouteError::RessourceNotFound); - }; - let steps = recipe .find_related(RecipeSteps) .all(&state.db) @@ -134,3 +160,19 @@ pub(super) async fn fetch_recipe( } .into()) } + +pub(super) async fn edit_name( + State(state): State, + RecipeExtractor(recipe): RecipeExtractor, + Json(req): Json, +) -> JsonResult { + let active_model = recipe::ActiveModel { + name: ActiveValue::Set(req.name), + id: ActiveValue::Set(recipe.id), + ..Default::default() + }; + + active_model.update(&state.db).await?; + + Ok(EmptyResponse {}.into()) +}