From 5ce3391466d0815c815a6444bfdd6fb0b61131f3 Mon Sep 17 00:00:00 2001 From: Greg Burri Date: Sat, 28 Dec 2024 22:52:07 +0100 Subject: [PATCH] Recipe edit, we can now delete groups, steps and ingredients --- Cargo.lock | 14 +++--- backend/scss/modal-dialog.scss | 19 ++++++++ backend/scss/style.scss | 1 + backend/src/services/ron.rs | 48 -------------------- backend/templates/base.html | 6 +++ backend/templates/recipe_edit.html | 6 +-- frontend/Cargo.toml | 3 ++ frontend/src/handles.rs | 71 +++++++++++++++++++++++++----- frontend/src/lib.rs | 2 + frontend/src/modal_dialog.rs | 30 +++++++++++++ frontend/src/on_click.rs | 51 +++++++++++++++++++++ 11 files changed, 183 insertions(+), 68 deletions(-) create mode 100644 backend/scss/modal-dialog.scss create mode 100644 frontend/src/modal_dialog.rs create mode 100644 frontend/src/on_click.rs diff --git a/Cargo.lock b/Cargo.lock index de94fa4..df74748 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -791,6 +791,7 @@ version = "0.1.0" dependencies = [ "common", "console_error_panic_hook", + "futures", "gloo", "ron", "serde", @@ -808,6 +809,7 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -2196,9 +2198,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.216" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] @@ -2216,9 +2218,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.216" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", @@ -2624,9 +2626,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.92" +version = "2.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ae51629bf965c5c098cc9e87908a3df5301051a9e087d6f9bef5c9771ed126" +checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058" dependencies = [ "proc-macro2", "quote", diff --git a/backend/scss/modal-dialog.scss b/backend/scss/modal-dialog.scss new file mode 100644 index 0000000..7ea2fb1 --- /dev/null +++ b/backend/scss/modal-dialog.scss @@ -0,0 +1,19 @@ +#modal-dialog { + // visibility: hidden; + color: white; + max-width: 300px; + margin-left: -125px; + background-color: black; + text-align: center; + border-radius: 2px; + padding: 16px; // TODO: 'rem' better? + position: fixed; + z-index: 1; + left: 50%; + bottom: 30px; + box-shadow: -1px 1px 10px rgba(0, 0, 0, 0.3); +} + +#modal-dialog.show { + visibility: visible; +} \ No newline at end of file diff --git a/backend/scss/style.scss b/backend/scss/style.scss index f5e0cdf..fa47f09 100644 --- a/backend/scss/style.scss +++ b/backend/scss/style.scss @@ -1,4 +1,5 @@ @use 'toast.scss'; +@use 'modal-dialog.scss'; @font-face { font-family: Fira Code; diff --git a/backend/src/services/ron.rs b/backend/src/services/ron.rs index 8f8ea96..8be7ab7 100644 --- a/backend/src/services/ron.rs +++ b/backend/src/services/ron.rs @@ -1,51 +1,3 @@ -// use actix_web::{ -// http::{header, header::ContentType, StatusCode}, -// post, put, web, HttpMessage, HttpRequest, HttpResponse, Responder, -// }; -// use log::{debug, error, info, log_enabled, Level}; -// use ron::de::from_bytes; - -// use super::Result; -// use crate::data::db; - -// #[put("/ron-api/recipe/set-title")] -// pub async fn set_recipe_title( -// req: HttpRequest, -// body: web::Bytes, -// connection: web::Data, -// ) -> Result { -// let ron_req: common::ron_api::SetRecipeTitle = from_bytes(&body)?; -// connection -// .set_recipe_title_async(ron_req.recipe_id, &ron_req.title) -// .await?; -// Ok(HttpResponse::Ok().finish()) -// } - -// #[put("/ron-api/recipe/set-description")] -// pub async fn set_recipe_description( -// req: HttpRequest, -// body: web::Bytes, -// connection: web::Data, -// ) -> Result { -// let ron_req: common::ron_api::SetRecipeDescription = from_bytes(&body)?; -// connection -// .set_recipe_description_async(ron_req.recipe_id, &ron_req.description) -// .await?; -// Ok(HttpResponse::Ok().finish()) -// } - -// #[put("/ron-api/recipe/add-image)] -// #[put("/ron-api/recipe/rm-photo")] -// #[put("/ron-api/recipe/add-ingredient")] -// #[put("/ron-api/recipe/rm-ingredient")] -// #[put("/ron-api/recipe/set-ingredients-order")] -// #[put("/ron-api/recipe/add-group")] -// #[put("/ron-api/recipe/rm-group")] -// #[put("/ron-api/recipe/set-groups-order")] -// #[put("/ron-api/recipe/add-step")] -// #[put("/ron-api/recipe/rm-step")] -// #[put("/ron-api/recipe/set-steps-order")] - use axum::{ debug_handler, extract::{Extension, Query, State}, diff --git a/backend/templates/base.html b/backend/templates/base.html index 0f61352..8da3d5d 100644 --- a/backend/templates/base.html +++ b/backend/templates/base.html @@ -18,6 +18,12 @@
+ +
+ + +
+ {% block body_container %}{% endblock %}
gburri - 2025
diff --git a/backend/templates/recipe_edit.html b/backend/templates/recipe_edit.html index 5438d3c..7ef799f 100644 --- a/backend/templates/recipe_edit.html +++ b/backend/templates/recipe_edit.html @@ -93,15 +93,15 @@
+ + + - - - diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml index 8c388f9..5495232 100644 --- a/frontend/Cargo.toml +++ b/frontend/Cargo.toml @@ -17,6 +17,8 @@ ron = "0.8" serde = { version = "1.0", features = ["derive"] } thiserror = "2" +futures = "0.3" + wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" web-sys = { version = "0.3", features = [ @@ -32,6 +34,7 @@ web-sys = { version = "0.3", features = [ "HtmlInputElement", "HtmlTextAreaElement", "HtmlSelectElement", + "HtmlDialogElement", ] } gloo = "0.11" diff --git a/frontend/src/handles.rs b/frontend/src/handles.rs index 20e70ae..1512b0c 100644 --- a/frontend/src/handles.rs +++ b/frontend/src/handles.rs @@ -1,12 +1,14 @@ use gloo::{console::log, events::EventListener, net::http::Request, utils::document}; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::spawn_local; -use web_sys::{Element, Event, HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement}; +use web_sys::{ + Element, Event, HtmlDialogElement, HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement, +}; use common::ron_api::{self, Ingredient}; use crate::{ - request, + modal_dialog, request, toast::{self, Level}, utils::{by_id, select, select_and_clone, SelectExt}, }; @@ -169,7 +171,7 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { .set_attribute("id", &format!("group-{}", group.id)) .unwrap(); - let groups_container = document().get_element_by_id("groups-container").unwrap(); + let groups_container: Element = by_id("groups-container"); groups_container.append_child(&group_element).unwrap(); // Group name. @@ -209,13 +211,19 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { .forget(); // Delete button. - // TODO: add a user confirmation. + let group_element_cloned = group_element.clone(); let delete_button: HtmlInputElement = group_element.select(".input-group-delete"); EventListener::new(&delete_button, "click", move |_event| { + let name = group_element_cloned + .select::(".input-group-name") + .value(); spawn_local(async move { - let body = ron_api::RemoveRecipeGroup { group_id }; - let _ = request::delete::<(), _>("recipe/remove_group", body).await; - by_id::(&format!("group-{}", group_id)).remove(); + if modal_dialog::show(&format!("Are you sure to delete the group '{}'", name)).await + { + let body = ron_api::RemoveRecipeGroup { group_id }; + let _ = request::delete::<(), _>("recipe/remove_group", body).await; + by_id::(&format!("group-{}", group_id)).remove(); + } }); }) .forget(); @@ -228,7 +236,7 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { let response: ron_api::AddRecipeStepResult = request::post("recipe/add_step", body).await.unwrap(); create_step_element( - &by_id::(&format!("group-{}", group_id)), + &select::(&format!("#group-{} .steps", group_id)), &ron_api::Step { id: response.step_id, action: "".to_string(), @@ -268,6 +276,25 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { }) .forget(); + // Delete button. + let step_element_cloned = step_element.clone(); + let delete_button: HtmlInputElement = step_element.select(".input-step-delete"); + EventListener::new(&delete_button, "click", move |_event| { + let action = step_element_cloned + .select::(".text-area-step-action") + .value(); + spawn_local(async move { + if modal_dialog::show(&format!("Are you sure to delete the step '{}'", action)) + .await + { + let body = ron_api::RemoveRecipeStep { step_id }; + let _ = request::delete::<(), _>("recipe/remove_step", body).await; + by_id::(&format!("step-{}", step_id)).remove(); + } + }); + }) + .forget(); + // Add ingredient button. let add_ingredient_button: HtmlInputElement = step_element.select(".input-add-ingredient"); EventListener::new(&add_ingredient_button, "click", move |_event| { @@ -276,7 +303,7 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { let response: ron_api::AddRecipeIngredientResult = request::post("recipe/add_ingredient", body).await.unwrap(); create_ingredient_element( - &by_id::(&format!("step-{}", step_id)), + &select::(&format!("#step-{} .ingredients", step_id)), &ron_api::Ingredient { id: response.ingredient_id, name: "".to_string(), @@ -384,6 +411,25 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { }) .forget(); + // Delete button. + let ingredient_element_cloned = ingredient_element.clone(); + let delete_button: HtmlInputElement = ingredient_element.select(".input-ingredient-delete"); + EventListener::new(&delete_button, "click", move |_event| { + let name = ingredient_element_cloned + .select::(".input-ingredient-name") + .value(); + spawn_local(async move { + if modal_dialog::show(&format!("Are you sure to delete the ingredient '{}'", name)) + .await + { + let body = ron_api::RemoveRecipeIngredient { ingredient_id }; + let _ = request::delete::<(), _>("recipe/remove_ingredient", body).await; + by_id::(&format!("ingredient-{}", ingredient_id)).remove(); + } + }); + }) + .forget(); + ingredient_element } @@ -399,10 +445,13 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { let group_element = create_group_element(&group); for step in group.steps { - let step_element = create_step_element(&group_element, &step); + let step_element = create_step_element(&group_element.select(".steps"), &step); for ingredient in step.ingredients { - create_ingredient_element(&step_element, &ingredient); + create_ingredient_element( + &step_element.select(".ingredients"), + &ingredient, + ); } } } diff --git a/frontend/src/lib.rs b/frontend/src/lib.rs index 073eaef..1a2bde4 100644 --- a/frontend/src/lib.rs +++ b/frontend/src/lib.rs @@ -1,4 +1,6 @@ mod handles; +mod modal_dialog; +mod on_click; mod request; mod toast; mod utils; diff --git a/frontend/src/modal_dialog.rs b/frontend/src/modal_dialog.rs new file mode 100644 index 0000000..7d3cd02 --- /dev/null +++ b/frontend/src/modal_dialog.rs @@ -0,0 +1,30 @@ +use futures::{future::FutureExt, pin_mut, select}; +use web_sys::{Element, HtmlDialogElement}; + +use crate::utils::{by_id, SelectExt}; + +use crate::on_click; + +pub async fn show(message: &str) -> bool { + let dialog: HtmlDialogElement = by_id("modal-dialog"); + let input_ok: Element = dialog.select(".ok"); + let input_cancel: Element = dialog.select(".cancel"); + + dialog.select::(".content").set_inner_html(message); + + dialog.show_modal().unwrap(); + + let click_ok = on_click::OnClick::new(&input_ok).fuse(); + let click_cancel = on_click::OnClick::new(&input_cancel).fuse(); + + pin_mut!(click_ok, click_cancel); + + let result = select! { + () = click_ok => true, + () = click_cancel => false, + }; + + dialog.close(); + + result +} diff --git a/frontend/src/on_click.rs b/frontend/src/on_click.rs new file mode 100644 index 0000000..a634e30 --- /dev/null +++ b/frontend/src/on_click.rs @@ -0,0 +1,51 @@ +use futures::channel::mpsc; +use futures::stream::Stream; +use gloo::{console::log, events::EventListener, net::http::Request, utils::document}; +use std::{ + future::Future, + pin::Pin, + task::{Context, Poll}, +}; +use wasm_bindgen::prelude::*; +use web_sys::EventTarget; + +// From: https://docs.rs/gloo-events/latest/gloo_events/struct.EventListener.html + +pub struct OnClick { + receiver: mpsc::UnboundedReceiver<()>, + // Automatically removed from the DOM on drop. + listener: EventListener, +} + +impl OnClick { + pub fn new(target: &EventTarget) -> Self { + let (sender, receiver) = mpsc::unbounded(); + + // Attach an event listener. + let listener = EventListener::new(target, "click", move |_event| { + sender.unbounded_send(()).unwrap_throw(); + }); + + Self { receiver, listener } + } +} + +// Multiple clicks. +impl Stream for OnClick { + type Item = (); + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + Pin::new(&mut self.receiver).poll_next(cx) + } +} + +// Just one click. +impl Future for OnClick { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + Pin::new(&mut self.receiver) + .poll_next(cx) + .map(Option::unwrap) + } +} -- 2.49.0