{% endblock %}
\ No newline at end of file
diff --git a/backend/templates/modal_dialog.html b/backend/templates/modal_dialog.html
new file mode 100644
index 0000000..49578a9
--- /dev/null
+++ b/backend/templates/modal_dialog.html
@@ -0,0 +1,6 @@
+{# Needed by the frontend modal_dialog module. #}
+
\ No newline at end of file
diff --git a/backend/templates/toast.html b/backend/templates/toast.html
new file mode 100644
index 0000000..abffc0c
--- /dev/null
+++ b/backend/templates/toast.html
@@ -0,0 +1,8 @@
+{# Needed by the frontend toast module. #}
+
+
+
+
+ ✖
+
+
\ No newline at end of file
diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml
index 3c02071..b00b241 100644
--- a/frontend/Cargo.toml
+++ b/frontend/Cargo.toml
@@ -44,6 +44,7 @@ web-sys = { version = "0.3", features = [
"HtmlDivElement",
"HtmlLabelElement",
"HtmlInputElement",
+ "HtmlImageElement",
"HtmlTextAreaElement",
"HtmlSelectElement",
"HtmlDialogElement",
diff --git a/frontend/src/pages/dev_panel.rs b/frontend/src/pages/dev_panel.rs
index e1fbab8..0b59e75 100644
--- a/frontend/src/pages/dev_panel.rs
+++ b/frontend/src/pages/dev_panel.rs
@@ -1,7 +1,7 @@
-use gloo::{console::log, events::EventListener};
+use gloo::{console::log, events::EventListener, utils::document};
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
-use web_sys::{Element, HtmlInputElement};
+use web_sys::{Element, HtmlElement, HtmlInputElement};
use crate::{
calendar, modal_dialog,
@@ -13,10 +13,56 @@ use crate::{
pub fn setup_page() {
EventListener::new(&by_id("test-toast"), "click", move |_event| {
- toast::show_message(Level::Info, "This is a message");
+ toast::show_message("This is a message");
})
.forget();
+ EventListener::new(&by_id("test-toast-success"), "click", move |_event| {
+ toast::show_message_level(Level::Success, "This is a success message");
+ })
+ .forget();
+
+ EventListener::new(&by_id("test-toast-error"), "click", move |_event| {
+ toast::show_message_level(Level::Error, "This is a error message");
+ })
+ .forget();
+
+ EventListener::new(&by_id("test-toast-info"), "click", move |_event| {
+ toast::show_message_level(Level::Info, "This is an info message");
+ })
+ .forget();
+
+ EventListener::new(&by_id("test-toast-warning"), "click", move |_event| {
+ toast::show_message_level(Level::Warning, "This is a warning message");
+ })
+ .forget();
+
+ EventListener::new(
+ &by_id("test-toast-success-content"),
+ "click",
+ move |_event| {
+ toast::show_element_level(Level::Success, "#hidden-templates .toast-test-content");
+ },
+ )
+ .forget();
+
+ EventListener::new(
+ &by_id("test-toast-success-content-initializer"),
+ "click",
+ move |_event| {
+ toast::show_element_level_and_initialize(
+ Level::Success,
+ "#hidden-templates .toast-test-content",
+ |element| {
+ let new_span = document().create_element("span").unwrap();
+ new_span.set_inner_html("Item 3");
+ element.append_child(&new_span).unwrap();
+ },
+ );
+ },
+ )
+ .forget();
+
EventListener::new(&by_id("test-modal-dialog"), "click", move |_event| {
spawn_local(async move {
modal_dialog::show("#hidden-templates").await;
diff --git a/frontend/src/pages/recipe_edit.rs b/frontend/src/pages/recipe_edit.rs
index 858825b..1bb6f91 100644
--- a/frontend/src/pages/recipe_edit.rs
+++ b/frontend/src/pages/recipe_edit.rs
@@ -76,7 +76,6 @@ pub fn setup_page(recipe_id: i64) {
let servings = if n.is_nan() {
None
} else {
- // TODO: Find a better way to validate integer numbers.
let n = n as u32;
servings.set_value_as_number(n as f64);
Some(n)
@@ -108,7 +107,6 @@ pub fn setup_page(recipe_id: i64) {
let time = if n.is_nan() {
None
} else {
- // TODO: Find a better way to validate integer numbers.
let n = n as u32;
estimated_time.set_value_as_number(n as f64);
Some(n)
@@ -708,7 +706,7 @@ async fn reload_recipes_list(current_recipe_id: i64) {
.await
{
Err(error) => {
- toast::show_message(Level::Info, &format!("Internal server error: {}", error));
+ toast::show_message_level(Level::Error, &format!("Internal server error: {}", error));
}
Ok(response) => {
let list = document().get_element_by_id("recipes-list").unwrap();
diff --git a/frontend/src/pages/recipe_view.rs b/frontend/src/pages/recipe_view.rs
index 6158c64..4eb825c 100644
--- a/frontend/src/pages/recipe_view.rs
+++ b/frontend/src/pages/recipe_view.rs
@@ -52,7 +52,7 @@ pub fn setup_page(recipe_id: i64, is_user_logged: bool, first_day_of_the_week: W
.shedule_recipe(recipe_id, date, servings, add_ingredients_to_shopping_list)
.await
{
- toast::show_element_and_initialize(
+ toast::show_element_level_and_initialize(
match result {
ScheduleRecipeResult::Ok => Level::Success,
ScheduleRecipeResult::RecipeAlreadyScheduledAtThisDate => {
diff --git a/frontend/src/request.rs b/frontend/src/request.rs
index 3579440..707126c 100644
--- a/frontend/src/request.rs
+++ b/frontend/src/request.rs
@@ -64,13 +64,13 @@ where
{
match request.send().await {
Err(error) => {
- toast::show_message(Level::Info, &format!("Internal server error: {}", error));
+ toast::show_message_level(Level::Error, &format!("Internal server error: {}", error));
Err(Error::Gloo(error))
}
Ok(response) => {
if !response.ok() {
- toast::show_message(
- Level::Info,
+ toast::show_message_level(
+ Level::Error,
&format!("HTTP error: {}", response.status_text()),
);
Err(Error::Http(response.status_text()))
diff --git a/frontend/src/toast.rs b/frontend/src/toast.rs
index eda5d85..c3a12fd 100644
--- a/frontend/src/toast.rs
+++ b/frontend/src/toast.rs
@@ -1,5 +1,5 @@
-use gloo::{timers::callback::Timeout, utils::document};
-use web_sys::{Element, HtmlElement};
+use gloo::{events::EventListener, timers::callback::Timeout};
+use web_sys::{Element, HtmlElement, HtmlImageElement};
use crate::utils::{SelectorExt, by_id, selector_and_clone};
@@ -8,55 +8,82 @@ pub enum Level {
Error,
Info,
Warning,
+ Unknown,
}
-/*
-TODO:
- - Stack multiple toast messages (see #toasts) by cloning #toast
- - User can close message by clicking a button
- - Implement level display with icons
-*/
-
const TIME_ANIMATION: u32 = 500; // [ms].
const TIME_DISPLAYED: u32 = 5_000; // [ms].
-pub fn show_message(level: Level, message: &str) {
- let toast_element: HtmlElement = by_id("toast");
- toast_element.set_inner_html(message);
- toast_element.style().set_css_text(&format!(
- "visibility: visible;
- animation:
- fadein {}ms,
- fadeout {}ms {}ms;",
- TIME_ANIMATION,
- TIME_ANIMATION,
- TIME_DISPLAYED - TIME_ANIMATION
- ));
-
- Timeout::new(TIME_DISPLAYED, move || {
- toast_element.style().set_css_text("");
- })
- .forget();
+pub fn show_message(message: &str) {
+ show_message_level(Level::Unknown, message);
}
-pub fn show_element(level: Level, selector: &str) {
- show_element_and_initialize(level, selector, |_| {})
+pub fn show_message_level(level: Level, message: &str) {
+ show_message_content(level, Content::Message(message))
}
-pub fn show_element_and_initialize(level: Level, selector: &str, initializer: T)
+pub fn show_element_level(level: Level, selector: &str) {
+ show_element_level_and_initialize(level, selector, |_| {})
+}
+
+pub fn show_element_level_and_initialize(level: Level, selector: &str, initializer: T)
where
T: Fn(Element),
{
- let toast_element = document().get_element_by_id("toast").unwrap();
+ let content_element: Element = selector_and_clone(selector);
+ initializer(content_element.clone());
- let element: Element = selector_and_clone(selector);
- toast_element.set_inner_html("");
- toast_element.append_child(&element).unwrap();
- initializer(element.clone());
- toast_element.set_class_name("show");
+ show_message_content(level, Content::Element(content_element))
+}
+
+enum Content<'a> {
+ Message(&'a str),
+ Element(Element),
+}
+
+fn show_message_content(level: Level, content: Content) {
+ let toast_element: HtmlElement = selector_and_clone("#toasts .toast");
+
+ let toast_icon: HtmlImageElement = toast_element.selector(".icon");
+ let toast_content: HtmlElement = toast_element.selector(".content");
+
+ match level {
+ Level::Success => toast_icon.set_src("/static/success.svg"),
+ Level::Error => toast_icon.set_src("/static/error.svg"),
+ Level::Info => toast_icon.set_src("/static/info.svg"),
+ Level::Warning => toast_icon.set_src("/static/warning.svg"),
+ Level::Unknown => toast_icon.remove(),
+ }
+
+ match content {
+ Content::Message(message) => toast_content.set_inner_html(message),
+ Content::Element(element) => {
+ let _ = toast_content.append_child(&element);
+ }
+ }
+
+ toast_element.style().set_css_text(&format!(
+ "display: block;
+ animation:
+ fadein {}ms,
+ fadeout {}ms {}ms;",
+ TIME_ANIMATION, TIME_ANIMATION, TIME_DISPLAYED
+ ));
+
+ // FIXME: Here the two events will leak memory. How to fix that?
+ // Save them in a global vec variable and remove them manually?
+ let close_button: HtmlElement = toast_element.selector(".close");
+ let toast_element_cloned = toast_element.clone();
+ EventListener::once(&close_button, "click", move |_event| {
+ toast_element_cloned.remove();
+ })
+ .forget();
+
+ let toasts: HtmlElement = by_id("toasts");
+ toasts.append_child(&toast_element).unwrap();
Timeout::new(TIME_DISPLAYED, move || {
- toast_element.set_class_name("");
+ toast_element.remove();
})
.forget();
}