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 common::ron_api::{self, Ingredient}; use crate::{ request, toast::{self, Level}, utils::{by_id, select, select_and_clone, SelectExt}, }; async fn reload_recipes_list(current_recipe_id: i64) { match Request::get("/fragments/recipes_list") .query([("current_recipe_id", current_recipe_id.to_string())]) .send() .await { Err(error) => { toast::show(Level::Info, &format!("Internal server error: {}", error)); } Ok(response) => { let list = document().get_element_by_id("recipes-list").unwrap(); list.set_outer_html(&response.text().await.unwrap()); } } } pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { // Title. { let title: HtmlInputElement = by_id("input-title"); let mut current_title = title.value(); EventListener::new(&title.clone(), "blur", move |_event| { if title.value() != current_title { current_title = title.value(); let body = ron_api::SetRecipeTitle { recipe_id, title: title.value(), }; spawn_local(async move { let _ = request::put::<(), _>("recipe/set_title", body).await; reload_recipes_list(recipe_id).await; }); } }) .forget(); } // Description. { let description: HtmlTextAreaElement = by_id("text-area-description"); let mut current_description = description.value(); let on_input_description_blur = EventListener::new(&description.clone(), "blur", move |_event| { if description.value() != current_description { current_description = description.value(); let body = ron_api::SetRecipeDescription { recipe_id, description: description.value(), }; spawn_local(async move { let _ = request::put::<(), _>("recipe/set_description", body).await; }); } }); on_input_description_blur.forget(); } // Estimated time. { let estimated_time: HtmlInputElement = by_id("input-estimated-time"); let mut current_time = estimated_time.value_as_number(); let on_input_estimated_time_blur = EventListener::new(&estimated_time.clone(), "blur", move |_event| { let n = estimated_time.value_as_number(); if n.is_nan() { estimated_time.set_value(""); } if n != current_time { 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) }; current_time = n; let body = ron_api::SetRecipeEstimatedTime { recipe_id, estimated_time: time, }; spawn_local(async move { let _ = request::put::<(), _>("recipe/set_estimated_time", body).await; }); } }); on_input_estimated_time_blur.forget(); } // Difficulty. { let difficulty: HtmlSelectElement = by_id("select-difficulty"); let mut current_difficulty = difficulty.value(); let on_select_difficulty_blur = EventListener::new(&difficulty.clone(), "blur", move |_event| { if difficulty.value() != current_difficulty { current_difficulty = difficulty.value(); let body = ron_api::SetRecipeDifficulty { recipe_id, difficulty: ron_api::Difficulty::try_from( current_difficulty.parse::().unwrap(), ) .unwrap(), }; spawn_local(async move { let _ = request::put::<(), _>("recipe/set_difficulty", body).await; }); } }); on_select_difficulty_blur.forget(); } // Language. { let language: HtmlSelectElement = by_id("select-language"); let mut current_language = language.value(); let on_select_language_blur = EventListener::new(&language.clone(), "blur", move |_event| { if language.value() != current_language { current_language = language.value(); let body = ron_api::SetRecipeLanguage { recipe_id, lang: language.value(), }; spawn_local(async move { let _ = request::put::<(), _>("recipe/set_language", body).await; }); } }); on_select_language_blur.forget(); } // Is published. { let is_published: HtmlInputElement = by_id("input-is-published"); let on_input_is_published_blur = EventListener::new(&is_published.clone(), "input", move |_event| { let body = ron_api::SetIsPublished { recipe_id, is_published: is_published.checked(), }; spawn_local(async move { let _ = request::put::<(), _>("recipe/set_is_published", body).await; reload_recipes_list(recipe_id).await; }); }); on_input_is_published_blur.forget(); } fn create_group_element(group: &ron_api::Group) -> Element { let group_id = group.id; let group_element: Element = select_and_clone("#hidden-templates .group"); group_element .set_attribute("id", &format!("group-{}", group.id)) .unwrap(); let groups_container = document().get_element_by_id("groups-container").unwrap(); groups_container.append_child(&group_element).unwrap(); // Group name. let name = group_element.select::(".input-group-name"); name.set_value(&group.name); let mut current_name = group.name.clone(); EventListener::new(&name.clone(), "blur", move |_event| { if name.value() != current_name { current_name = name.value(); let body = ron_api::SetGroupName { group_id, name: name.value(), }; spawn_local(async move { let _ = request::put::<(), _>("recipe/set_group_name", body).await; }) } }) .forget(); // Group comment. let comment: HtmlInputElement = group_element.select(".input-group-comment"); comment.set_value(&group.comment); let mut current_comment = group.comment.clone(); EventListener::new(&comment.clone(), "blur", move |_event| { if comment.value() != current_comment { current_comment = comment.value(); let body = ron_api::SetGroupComment { group_id, comment: comment.value(), }; spawn_local(async move { let _ = request::put::<(), _>("recipe/set_group_comment", body).await; }); } }) .forget(); // Delete button. // TODO: add a user confirmation. let delete_button: HtmlInputElement = group_element.select(".input-group-delete"); EventListener::new(&delete_button, "click", move |_event| { 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(); }); }) .forget(); // Add step button. let add_step_button: HtmlInputElement = group_element.select(".input-add-step"); EventListener::new(&add_step_button, "click", move |_event| { spawn_local(async move { let body = ron_api::AddRecipeStep { group_id }; let response: ron_api::AddRecipeStepResult = request::post("recipe/add_step", body).await.unwrap(); create_step_element( &by_id::(&format!("group-{}", group_id)), &ron_api::Step { id: response.step_id, action: "".to_string(), ingredients: vec![], }, ); }); }) .forget(); group_element } fn create_step_element(group_element: &Element, step: &ron_api::Step) -> Element { let step_id = step.id; let step_element: Element = select_and_clone("#hidden-templates .step"); step_element .set_attribute("id", &format!("step-{}", step.id)) .unwrap(); group_element.append_child(&step_element).unwrap(); // Step action. let action: HtmlTextAreaElement = step_element.select(".text-area-step-action"); action.set_value(&step.action); let mut current_action = step.action.clone(); EventListener::new(&action.clone(), "blur", move |_event| { if action.value() != current_action { current_action = action.value(); let body = ron_api::SetStepAction { step_id, action: action.value(), }; spawn_local(async move { let _ = request::put::<(), _>("recipe/set_step_action", body).await; }); } }) .forget(); // Add ingredient button. let add_ingredient_button: HtmlInputElement = step_element.select(".input-add-ingredient"); EventListener::new(&add_ingredient_button, "click", move |_event| { spawn_local(async move { let body = ron_api::AddRecipeIngredient { step_id }; let response: ron_api::AddRecipeIngredientResult = request::post("recipe/add_ingredient", body).await.unwrap(); create_ingredient_element( &by_id::(&format!("step-{}", step_id)), &ron_api::Ingredient { id: response.ingredient_id, name: "".to_string(), comment: "".to_string(), quantity_value: None, quantity_unit: "".to_string(), }, ); }); }) .forget(); step_element } fn create_ingredient_element( step_element: &Element, ingredient: &ron_api::Ingredient, ) -> Element { let ingredient_id = ingredient.id; let ingredient_element: Element = select_and_clone("#hidden-templates .ingredient"); ingredient_element .set_attribute("id", &format!("ingredient-{}", ingredient.id)) .unwrap(); step_element.append_child(&ingredient_element).unwrap(); // Ingredient name. let name: HtmlInputElement = ingredient_element.select(".input-ingredient-name"); name.set_value(&ingredient.name); let mut current_name = ingredient.name.clone(); EventListener::new(&name.clone(), "blur", move |_event| { if name.value() != current_name { current_name = name.value(); let body = ron_api::SetIngredientName { ingredient_id, name: name.value(), }; spawn_local(async move { let _ = request::put::<(), _>("recipe/set_ingredient_name", body).await; }); } }) .forget(); // Ingredient comment. let comment: HtmlInputElement = ingredient_element.select(".input-ingredient-comment"); comment.set_value(&ingredient.comment); let mut current_comment = ingredient.comment.clone(); EventListener::new(&comment.clone(), "blur", move |_event| { if comment.value() != current_comment { current_comment = comment.value(); let body = ron_api::SetIngredientComment { ingredient_id, comment: comment.value(), }; spawn_local(async move { let _ = request::put::<(), _>("recipe/set_ingredient_comment", body).await; }); } }) .forget(); // Ingredient quantity. let quantity: HtmlInputElement = ingredient_element.select(".input-ingredient-quantity"); quantity.set_value( &ingredient .quantity_value .map_or("".to_string(), |q| q.to_string()), ); let mut current_quantity = quantity.value_as_number(); EventListener::new(&quantity.clone(), "blur", move |_event| { let n = quantity.value_as_number(); if n.is_nan() { quantity.set_value(""); } if n != current_quantity { let q = if n.is_nan() { None } else { Some(n) }; current_quantity = n; let body = ron_api::SetIngredientQuantity { ingredient_id, quantity: q, }; spawn_local(async move { let _ = request::put::<(), _>("recipe/set_ingredient_quantity", body).await; }); } }) .forget(); // Ingredient unit. let unit: HtmlInputElement = ingredient_element.select(".input-ingredient-unit"); unit.set_value(&ingredient.quantity_unit); let mut current_unit = ingredient.quantity_unit.clone(); EventListener::new(&unit.clone(), "blur", move |_event| { if unit.value() != current_unit { current_unit = unit.value(); let body = ron_api::SetIngredientUnit { ingredient_id, unit: unit.value(), }; spawn_local(async move { let _ = request::put::<(), _>("recipe/set_ingredient_unit", body).await; }); } }) .forget(); ingredient_element } // Load initial groups, steps and ingredients. { spawn_local(async move { let groups: Vec = request::get("recipe/get_groups", [("recipe_id", &recipe_id.to_string())]) .await .unwrap(); for group in groups { let group_element = create_group_element(&group); for step in group.steps { let step_element = create_step_element(&group_element, &step); for ingredient in step.ingredients { create_ingredient_element(&step_element, &ingredient); } } } }); } // Add a new group. { let button_add_group: HtmlInputElement = by_id("input-add-group"); let on_click_add_group = EventListener::new(&button_add_group, "click", move |_event| { let body = ron_api::AddRecipeGroup { recipe_id }; spawn_local(async move { let response: ron_api::AddRecipeGroupResult = request::post("recipe/add_group", body).await.unwrap(); create_group_element(&ron_api::Group { id: response.group_id, name: "".to_string(), comment: "".to_string(), steps: vec![], }); }); }); on_click_add_group.forget(); } Ok(()) } // pub fn user_edit(doc: Document) -> Result<(), JsValue> { // log!("user_edit"); // let button = doc // .query_selector("#user-edit input[type='button']")? // .unwrap(); // let on_click_submit = EventListener::new(&button, "click", move |_event| { // log!("Click!"); // let input_name = doc.get_element_by_id("input-name").unwrap(); // let name = input_name.dyn_ref::().unwrap().value(); // let update_data = common::ron_api::UpdateProfile { // name: Some(name), // email: None, // password: None, // }; // let body = common::ron_api::to_string(update_data); // let doc = doc.clone(); // spawn_local(async move { // match Request::put("/ron-api/user/update") // .header("Content-Type", "application/ron") // .body(body) // .unwrap() // .send() // .await // { // Ok(resp) => { // log!("Status code: {}", resp.status()); // if resp.status() == 200 { // toast::show(Level::Info, "Profile saved"); // } else { // toast::show( // Level::Error, // &format!( // "Status code: {} {}", // resp.status(), // resp.text().await.unwrap() // ), // ); // } // } // Err(error) => { // toast::show(Level::Info, &format!("Internal server error: {}", error)); // } // } // }); // }); // on_click_submit.forget(); // Ok(()) // }