dependencies = [
"common",
"console_error_panic_hook",
+ "futures",
"gloo",
"ron",
"serde",
dependencies = [
"futures-channel",
"futures-core",
+ "futures-executor",
"futures-io",
"futures-sink",
"futures-task",
[[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",
]
[[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",
[[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",
--- /dev/null
+#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
@use 'toast.scss';
+@use 'modal-dialog.scss';
@font-face {
font-family: Fira Code;
-// 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<db::Connection>,
-// ) -> Result<HttpResponse> {
-// 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<db::Connection>,
-// ) -> Result<HttpResponse> {
-// 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},
<div id="toast"></div>
+ <dialog id="modal-dialog">
+ <div class="content"></div>
+ <input type="button" class="ok" value="OK" />
+ <input type="button" class="cancel" value="Cancel" />
+ </dialog>
+
{% block body_container %}{% endblock %}
<footer class="footer-container">gburri - 2025</footer>
</div>
<div class="ingredient">
+ <label for="input-ingredient-name">Name</label>
+ <input class="input-ingredient-name" type="text" />
+
<label for="input-ingredient-quantity">Quantity</label>
<input class="input-ingredient-quantity" type="number" step="0.1" min="0" max="10000" />
<label for="input-ingredient-unit">Unit</label>
<input class="input-ingredient-unit" type="text" />
- <label for="input-ingredient-name">Name</label>
- <input class="input-ingredient-name" type="text" />
-
<label for="input-ingredient-comment">Comment</label>
<input class="input-ingredient-comment" type="text" />
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 = [
"HtmlInputElement",
"HtmlTextAreaElement",
"HtmlSelectElement",
+ "HtmlDialogElement",
] }
gloo = "0.11"
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},
};
.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.
.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::<HtmlInputElement>(".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::<Element>(&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::<Element>(&format!("group-{}", group_id)).remove();
+ }
});
})
.forget();
let response: ron_api::AddRecipeStepResult =
request::post("recipe/add_step", body).await.unwrap();
create_step_element(
- &by_id::<Element>(&format!("group-{}", group_id)),
+ &select::<Element>(&format!("#group-{} .steps", group_id)),
&ron_api::Step {
id: response.step_id,
action: "".to_string(),
})
.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::<HtmlTextAreaElement>(".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::<Element>(&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| {
let response: ron_api::AddRecipeIngredientResult =
request::post("recipe/add_ingredient", body).await.unwrap();
create_ingredient_element(
- &by_id::<Element>(&format!("step-{}", step_id)),
+ &select::<Element>(&format!("#step-{} .ingredients", step_id)),
&ron_api::Ingredient {
id: response.ingredient_id,
name: "".to_string(),
})
.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::<HtmlInputElement>(".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::<Element>(&format!("ingredient-{}", ingredient_id)).remove();
+ }
+ });
+ })
+ .forget();
+
ingredient_element
}
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,
+ );
}
}
}
mod handles;
+mod modal_dialog;
+mod on_click;
mod request;
mod toast;
mod utils;
--- /dev/null
+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::<Element>(".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
+}
--- /dev/null
+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<Option<Self::Item>> {
+ 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<Self::Output> {
+ Pin::new(&mut self.receiver)
+ .poll_next(cx)
+ .map(Option::unwrap)
+ }
+}