From: Greg Burri Date: Thu, 26 Dec 2024 00:39:07 +0000 (+0100) Subject: Recipe edit (WIP): forms to edit groups, steps and ingredients X-Git-Url: https://git.euphorik.ch/?a=commitdiff_plain;h=07b7ff425ee54922b2c7a4495fb2d1bed430dd2b;p=recipes.git Recipe edit (WIP): forms to edit groups, steps and ingredients --- diff --git a/Cargo.lock b/Cargo.lock index 22fc2c7..2ae3f71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -793,6 +793,9 @@ dependencies = [ "common", "console_error_panic_hook", "gloo", + "ron", + "serde", + "thiserror 2.0.9", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -2976,9 +2979,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicase" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-bidi" diff --git a/backend/build.rs b/backend/build.rs index 5628d92..0af3eca 100644 --- a/backend/build.rs +++ b/backend/build.rs @@ -35,7 +35,7 @@ fn main() { } let output = if exists_in_path("sass.bat") { - run_sass(Command::new("cmd").args(&["/C", "sass.bat"])) + run_sass(Command::new("cmd").args(["/C", "sass.bat"])) } else { run_sass(&mut Command::new("sass")) }; diff --git a/backend/scss/style.scss b/backend/scss/style.scss index 1405ecd..761ae70 100644 --- a/backend/scss/style.scss +++ b/backend/scss/style.scss @@ -93,6 +93,22 @@ body { h1 { text-align: center; } + + .group { + border: 0.1em solid white; + } + + .step { + border: 0.1em solid white; + } + + .ingredient { + border: 0.1em solid white; + } + + #hidden-templates { + display: none; + } } form { diff --git a/backend/sql/data_test.sql b/backend/sql/data_test.sql index 632a75e..a00170c 100644 --- a/backend/sql/data_test.sql +++ b/backend/sql/data_test.sql @@ -18,14 +18,61 @@ VALUES ( NULL ); -INSERT INTO [Recipe] ([user_id], [title], [is_published]) -VALUES (1, 'Croissant au jambon', true); +INSERT INTO [Recipe] ([id], [user_id], [title], [is_published]) +VALUES (1, 1, 'Croissant au jambon', true); -INSERT INTO [Recipe] ([user_id], [title], [is_published]) -VALUES (1, 'Gratin de thon aux olives', true); +INSERT INTO [Recipe] ([id], [user_id], [title], [is_published]) +VALUES (2, 1, 'Gratin de thon aux olives', true); -INSERT INTO [Recipe] ([user_id], [title], [is_published]) -VALUES (1, 'Saumon en croute', true); +INSERT INTO [Recipe] ([id], [user_id], [title], [is_published]) +VALUES (3, 1, 'Saumon en croute', true); -INSERT INTO [Recipe] ([user_id], [title], [is_published]) -VALUES (2, 'Ouiche lorraine', true); +INSERT INTO [Recipe] ([id], [user_id], [title], [is_published]) +VALUES (4, 2, 'Ouiche lorraine', true); + + +-- Groups, steps and ingredients for 'Gratin de thon'. +INSERT INTO [Group] ([id], [order], [recipe_id], [name], [comment]) +VALUES (1, 1, 2, "Fond du gratin", ""); + +INSERT INTO [Group] ([id], [order], [recipe_id], [name], [comment]) +VALUES (2, 2, 2, "Sauce", ""); + + +INSERT INTO [Step] ([id], [order], [group_id], [action]) +VALUES (1, 1, 1, "Égoutter et émietter dans un plat à gratting graissé"); + +INSERT INTO [Ingredient] ([id], [step_id], [name], [comment], [quantity_value], [quantity_unit]) +VALUES (1, 1, "Thon en boîte", "", 240, "g"); + + +INSERT INTO [Step] ([id], [order], [group_id], [action]) +VALUES (2, 2, 1, "Saupoudrer"); + +INSERT INTO [Ingredient] ([id], [step_id], [name], [comment], [quantity_value], [quantity_unit]) +VALUES (2, 2, "Sel à l'origan", "", 1, "c-à-c"); + + +INSERT INTO [Step] ([id], [order], [group_id], [action]) +VALUES (3, 3, 2, "Mélanger au fouet et verser sur le thon dans le plat"); + +INSERT INTO [Ingredient] ([id], [step_id], [name], [comment], [quantity_value], [quantity_unit]) +VALUES (3, 3, "Concentré de tomate", "", 4, "c-à-s"); + +INSERT INTO [Ingredient] ([id], [step_id], [name], [comment], [quantity_value], [quantity_unit]) +VALUES (4, 3, "Poivre", "", 0.25, "c-à-c"); + +INSERT INTO [Ingredient] ([id], [step_id], [name], [comment], [quantity_value], [quantity_unit]) +VALUES (5, 3, "Herbe de Provence", "", 0.5, "c-à-c"); + +INSERT INTO [Ingredient] ([id], [step_id], [name], [comment], [quantity_value], [quantity_unit]) +VALUES (6, 3, "Crème à café ou demi-crème", "", 2, "dl"); + +INSERT INTO [Ingredient] ([id], [step_id], [name], [comment], [quantity_value], [quantity_unit]) +VALUES (7, 3, "Olives farcies coupées en deuxs", "", 50, "g"); + + +INSERT INTO [Group] ([id], [order], [recipe_id], [name], [comment]) +VALUES (3, 3, 2, + "15 à 20 minutes de cuisson au four à 220 °C", + "Servir avec du riz ou des patates robe des champs"); \ No newline at end of file diff --git a/backend/sql/version_1.sql b/backend/sql/version_1.sql index 3fffc66..1efde53 100644 --- a/backend/sql/version_1.sql +++ b/backend/sql/version_1.sql @@ -89,21 +89,12 @@ CREATE TABLE [Tag] ( CREATE UNIQUE INDEX [Tag_name_lang_index] ON [Tag] ([name], [lang]); -CREATE TABLE [Ingredient] ( - [id] INTEGER PRIMARY KEY, - [name] TEXT NOT NULL, - [comment] TEXT NOT NULL DEFAULT '', - [quantity_value] INTEGER, - [quantity_unit] TEXT NOT NULL DEFAULT '', - [input_step_id] INTEGER NOT NULL, - - FOREIGN KEY([input_step_id]) REFERENCES [Step]([id]) ON DELETE CASCADE -) STRICT; - CREATE TABLE [Group] ( [id] INTEGER PRIMARY KEY, + [order] INTEGER NOT NULL DEFAULT 0, [recipe_id] INTEGER NOT NULL, + [name] TEXT NOT NULL DEFAULT '', [comment] TEXT NOT NULL DEFAULT '', @@ -114,15 +105,30 @@ CREATE INDEX [Group_order_index] ON [Group]([order]); CREATE TABLE [Step] ( [id] INTEGER PRIMARY KEY, + [order] INTEGER NOT NULL DEFAULT 0, - [action] TEXT NOT NULL DEFAULT '', [group_id] INTEGER NOT NULL, + [action] TEXT NOT NULL DEFAULT '', + FOREIGN KEY(group_id) REFERENCES [Group](id) ON DELETE CASCADE ) STRICT; CREATE INDEX [Step_order_index] ON [Group]([order]); +CREATE TABLE [Ingredient] ( + [id] INTEGER PRIMARY KEY, + + [step_id] INTEGER NOT NULL, + + [name] TEXT NOT NULL, + [comment] TEXT NOT NULL DEFAULT '', + [quantity_value] REAL, + [quantity_unit] TEXT NOT NULL DEFAULT '', + + FOREIGN KEY([step_id]) REFERENCES [Step]([id]) ON DELETE CASCADE +) STRICT; + -- CREATE TABLE [IntermediateSubstance] ( -- [id] INTEGER PRIMARY KEY, -- [name] TEXT NOT NULL DEFAULT '', diff --git a/backend/src/config.rs b/backend/src/config.rs index 59ee30c..3d2a08d 100644 --- a/backend/src/config.rs +++ b/backend/src/config.rs @@ -41,22 +41,31 @@ impl fmt::Debug for Config { pub fn load() -> Config { match File::open(consts::FILE_CONF) { - Ok(file) => from_reader(file).expect(&format!( - "Failed to open configuration file {}", - consts::FILE_CONF - )), + Ok(file) => from_reader(file).unwrap_or_else(|error| { + panic!( + "Failed to open configuration file {}: {}", + consts::FILE_CONF, + error + ) + }), Err(_) => { - let file = File::create(consts::FILE_CONF).expect(&format!( - "Failed to create default configuration file {}", - consts::FILE_CONF - )); + let file = File::create(consts::FILE_CONF).unwrap_or_else(|error| { + panic!( + "Failed to create default configuration file {}: {}", + consts::FILE_CONF, + error + ) + }); let default_config = Config::default(); - to_writer_pretty(file, &default_config, PrettyConfig::new()).expect(&format!( - "Failed to write default configuration file {}", - consts::FILE_CONF - )); + to_writer_pretty(file, &default_config, PrettyConfig::new()).unwrap_or_else(|error| { + panic!( + "Failed to write default configuration file {}: {}", + consts::FILE_CONF, + error + ) + }); default_config } diff --git a/backend/src/consts.rs b/backend/src/consts.rs index 7b776e9..bedffc8 100644 --- a/backend/src/consts.rs +++ b/backend/src/consts.rs @@ -4,10 +4,10 @@ pub const FILE_CONF: &str = "conf.ron"; pub const DB_DIRECTORY: &str = "data"; pub const DB_FILENAME: &str = "recipes.sqlite"; pub const SQL_FILENAME: &str = "sql/version_{VERSION}.sql"; -pub const VALIDATION_TOKEN_DURATION: i64 = 1 * 60 * 60; // [s]. (1 jour). +pub const VALIDATION_TOKEN_DURATION: i64 = 60 * 60; // [s]. (1 jour). pub const COOKIE_AUTH_TOKEN_NAME: &str = "auth_token"; -pub const VALIDATION_PASSWORD_RESET_TOKEN_DURATION: i64 = 1 * 60 * 60; // [s]. (1 jour). +pub const VALIDATION_PASSWORD_RESET_TOKEN_DURATION: i64 = 60 * 60; // [s]. (1 jour). // Number of alphanumeric characters for tokens // (cookie authentication, password reset, validation token). @@ -21,4 +21,4 @@ pub const REVERSE_PROXY_IP_HTTP_FIELD: &str = "x-real-ip"; // Set by the reverse pub const MAX_DB_CONNECTION: u32 = 1024; -pub const LANGUAGES: [(&'static str, &'static str); 2] = [("Français", "fr"), ("English", "en")]; +pub const LANGUAGES: [(&str, &str); 2] = [("Français", "fr"), ("English", "en")]; diff --git a/backend/src/data/db.rs b/backend/src/data/db.rs index baff9d6..848c4d4 100644 --- a/backend/src/data/db.rs +++ b/backend/src/data/db.rs @@ -196,26 +196,10 @@ WHERE [type] = 'table' AND [name] = 'Version' } fn load_sql_file + fmt::Display>(sql_file: P) -> Result { - let mut file = File::open(&sql_file).map_err(|err| { - DBError::Other(format!( - "Cannot open SQL file ({}): {}", - &sql_file, - err.to_string() - )) - })?; + let mut file = File::open(&sql_file) + .map_err(|err| DBError::Other(format!("Cannot open SQL file ({}): {}", &sql_file, err)))?; let mut sql = String::new(); - file.read_to_string(&mut sql).map_err(|err| { - DBError::Other(format!( - "Cannot read SQL file ({}) : {}", - &sql_file, - err.to_string() - )) - })?; + file.read_to_string(&mut sql) + .map_err(|err| DBError::Other(format!("Cannot read SQL file ({}) : {}", &sql_file, err)))?; Ok(sql) } - -// #[cfg(test)] -// mod tests { -// use super::*; - -// } diff --git a/backend/src/data/db/recipe.rs b/backend/src/data/db/recipe.rs index 2525cc5..ba8f677 100644 --- a/backend/src/data/db/recipe.rs +++ b/backend/src/data/db/recipe.rs @@ -45,6 +45,21 @@ ORDER BY [title] .map_err(DBError::from) } + pub async fn can_edit_recipe_group(&self, user_id: i64, group_id: i64) -> Result { + sqlx::query_scalar( + r#" +SELECT COUNT(*) +FROM [Recipe] INNER JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id] +WHERE [Group].[id] = $1 AND [user_id] = $2 + "#, + ) + .bind(group_id) + .bind(user_id) + .fetch_one(&self.pool) + .await + .map_err(DBError::from) + } + pub async fn get_recipe(&self, id: i64) -> Result> { sqlx::query_as( r#" @@ -166,6 +181,88 @@ WHERE [Recipe].[user_id] = $1 .map(|_| ()) .map_err(DBError::from) } + + pub async fn get_groups(&self, recipe_id: i64) -> Result> { + let mut tx = self.tx().await?; + let mut groups: Vec = sqlx::query_as( + r#" +SELECT [id], [name], [comment] +FROM [Group] +WHERE [recipe_id] = $1 +ORDER BY [order] + "#, + ) + .bind(recipe_id) + .fetch_all(&mut *tx) + .await?; + + for group in groups.iter_mut() { + group.steps = sqlx::query_as( + r#" +SELECT [id], [action] +FROM [Step] +WHERE [group_id] = $1 +ORDER BY [order] + "#, + ) + .bind(group.id) + .fetch_all(&mut *tx) + .await?; + + for step in group.steps.iter_mut() { + step.ingredients = sqlx::query_as( + r#" +SELECT [id], [name], [comment], [quantity_value], [quantity_unit] +FROM [Ingredient] +WHERE [step_id] = $1 +ORDER BY [name] + "#, + ) + .bind(step.id) + .fetch_all(&mut *tx) + .await?; + } + } + + Ok(groups) + } + + pub async fn add_recipe_group(&self, recipe_id: i64) -> Result { + let db_result = sqlx::query("INSERT INTO [Group] ([recipe_id]) VALUES ($1)") + .bind(recipe_id) + .execute(&self.pool) + .await?; + Ok(db_result.last_insert_rowid()) + } + + pub async fn rm_recipe_group(&self, group_id: i64) -> Result<()> { + sqlx::query("DELETE FROM [Group] WHERE [id] = $1") + .bind(group_id) + .execute(&self.pool) + .await + .map(|_| ()) + .map_err(DBError::from) + } + + pub async fn set_group_name(&self, group_id: i64, name: &str) -> Result<()> { + sqlx::query("UPDATE [Group] SET [name] = $2 WHERE [id] = $1") + .bind(group_id) + .bind(name) + .execute(&self.pool) + .await + .map(|_| ()) + .map_err(DBError::from) + } + + pub async fn set_group_comment(&self, group_id: i64, comment: &str) -> Result<()> { + sqlx::query("UPDATE [Group] SET [comment] = $2 WHERE [id] = $1") + .bind(group_id) + .bind(comment) + .execute(&self.pool) + .await + .map(|_| ()) + .map_err(DBError::from) + } } #[cfg(test)] @@ -214,7 +311,7 @@ mod tests { assert_eq!(recipe.estimated_time, Some(420)); assert_eq!(recipe.difficulty, Difficulty::Medium); assert_eq!(recipe.lang, "fr"); - assert_eq!(recipe.is_published, true); + assert!(recipe.is_published); Ok(()) } diff --git a/backend/src/data/db/user.rs b/backend/src/data/db/user.rs index 698aa64..4a37d69 100644 --- a/backend/src/data/db/user.rs +++ b/backend/src/data/db/user.rs @@ -190,7 +190,7 @@ FROM [User] WHERE [email] = $1 return Ok(SignUpResult::UserAlreadyExists); } let token = generate_token(); - let hashed_password = hash(password).map_err(|e| DBError::from_dyn_error(e))?; + let hashed_password = hash(password).map_err(DBError::from_dyn_error)?; sqlx::query( r#" UPDATE [User] @@ -208,7 +208,7 @@ WHERE [id] = $1 } None => { let token = generate_token(); - let hashed_password = hash(password).map_err(|e| DBError::from_dyn_error(e))?; + let hashed_password = hash(password).map_err(DBError::from_dyn_error)?; sqlx::query( r#" INSERT INTO [User] @@ -336,19 +336,18 @@ WHERE [id] = $1 pub async fn sign_out(&self, token: &str) -> Result<()> { let mut tx = self.tx().await?; - match sqlx::query_scalar::<_, i64>("SELECT [id] FROM [UserLoginToken] WHERE [token] = $1") - .bind(token) - .fetch_optional(&mut *tx) - .await? + + if let Some(login_id) = + sqlx::query_scalar::<_, i64>("SELECT [id] FROM [UserLoginToken] WHERE [token] = $1") + .bind(token) + .fetch_optional(&mut *tx) + .await? { - Some(login_id) => { - sqlx::query("DELETE FROM [UserLoginToken] WHERE [id] = $1") - .bind(login_id) - .execute(&mut *tx) - .await?; - tx.commit().await?; - } - None => (), + sqlx::query("DELETE FROM [UserLoginToken] WHERE [id] = $1") + .bind(login_id) + .execute(&mut *tx) + .await?; + tx.commit().await?; } Ok(()) } @@ -429,7 +428,7 @@ WHERE [password_reset_token] = $1 .execute(&mut *tx) .await?; - let hashed_new_password = hash(new_password).map_err(|e| DBError::from_dyn_error(e))?; + let hashed_new_password = hash(new_password).map_err(DBError::from_dyn_error)?; sqlx::query( r#" @@ -853,7 +852,7 @@ VALUES ( }; connection - .reset_password(&new_password, &token, Duration::hours(1)) + .reset_password(new_password, &token, Duration::hours(1)) .await?; // Sign in. diff --git a/backend/src/data/model.rs b/backend/src/data/model.rs index 0d82ba8..84bad3c 100644 --- a/backend/src/data/model.rs +++ b/backend/src/data/model.rs @@ -34,20 +34,30 @@ pub struct Recipe { // pub groups: Vec, } +#[derive(FromRow)] pub struct Group { + pub id: i64, pub name: String, pub comment: String, + + #[sqlx(skip)] pub steps: Vec, } +#[derive(FromRow)] pub struct Step { + pub id: i64, pub action: String, + + #[sqlx(skip)] pub ingredients: Vec, } +#[derive(FromRow)] pub struct Ingredient { + pub id: i64, pub name: String, pub comment: String, - pub quantity: i32, + pub quantity_value: f64, pub quantity_unit: String, } diff --git a/backend/src/email.rs b/backend/src/email.rs index fb19d7b..32cf5c3 100644 --- a/backend/src/email.rs +++ b/backend/src/email.rs @@ -9,20 +9,20 @@ use crate::consts; #[derive(Debug, Display)] pub enum Error { - ParseError(lettre::address::AddressError), - SmtpError(lettre::transport::smtp::Error), + Parse(lettre::address::AddressError), + Smtp(lettre::transport::smtp::Error), Email(lettre::error::Error), } impl From for Error { fn from(error: lettre::address::AddressError) -> Self { - Error::ParseError(error) + Error::Parse(error) } } impl From for Error { fn from(error: lettre::transport::smtp::Error) -> Self { - Error::SmtpError(error) + Error::Smtp(error) } } diff --git a/backend/src/main.rs b/backend/src/main.rs index 7ae4503..dcb3d05 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -5,7 +5,7 @@ use axum::{ http::StatusCode, middleware::{self, Next}, response::{Response, Result}, - routing::{get, put}, + routing::{delete, get, post, put}, Router, }; use axum_extra::extract::cookie::CookieJar; @@ -101,6 +101,14 @@ async fn main() { "/recipe/set_is_published", put(services::ron::set_is_published), ) + .route("/recipe/get_groups", get(services::ron::get_groups)) + .route("/recipe/add_group", post(services::ron::add_group)) + .route("/recipe/remove_group", delete(services::ron::rm_group)) + .route("/recipe/set_group_name", put(services::ron::set_group_name)) + .route( + "/recipe/set_group_comment", + put(services::ron::set_group_comment), + ) .fallback(services::ron::not_found); let fragments_routes = Router::new().route( @@ -183,7 +191,7 @@ async fn get_current_user( ) -> Option { match jar.get(consts::COOKIE_AUTH_TOKEN_NAME) { Some(token_cookie) => match connection - .authentication(token_cookie.value(), &client_ip, &client_user_agent) + .authentication(token_cookie.value(), client_ip, client_user_agent) .await { Ok(db::user::AuthenticationResult::NotValidToken) => None, @@ -234,12 +242,15 @@ async fn process_args() -> bool { } }) .unwrap(); - std::fs::copy(&db_path, &db_path_bckup).expect(&format!( - "Unable to make backup of {:?} to {:?}", - &db_path, &db_path_bckup - )); - std::fs::remove_file(&db_path) - .expect(&format!("Unable to remove db file: {:?}", &db_path)); + std::fs::copy(&db_path, &db_path_bckup).unwrap_or_else(|error| { + panic!( + "Unable to make backup of {:?} to {:?}: {}", + &db_path, &db_path_bckup, error + ) + }); + std::fs::remove_file(&db_path).unwrap_or_else(|error| { + panic!("Unable to remove db file {:?}: {}", &db_path, error) + }); } match db::Connection::new().await { diff --git a/backend/src/ron_utils.rs b/backend/src/ron_utils.rs index 8616c70..4aa7d22 100644 --- a/backend/src/ron_utils.rs +++ b/backend/src/ron_utils.rs @@ -60,10 +60,8 @@ where { match from_bytes::(&body) { Ok(ron) => Ok(ron), - Err(error) => { - return Err(RonError { - error: format!("Ron parsing error: {}", error), - }); - } + Err(error) => Err(RonError { + error: format!("Ron parsing error: {}", error), + }), } } diff --git a/backend/src/services.rs b/backend/src/services.rs index 8231335..65067a3 100644 --- a/backend/src/services.rs +++ b/backend/src/services.rs @@ -1,7 +1,7 @@ use axum::{ body, debug_handler, extract::{Extension, Request, State}, - http::header, + http::{header, StatusCode}, middleware::Next, response::{IntoResponse, Response, Result}, }; @@ -66,5 +66,8 @@ pub async fn home_page( #[debug_handler] pub async fn not_found(Extension(user): Extension>) -> impl IntoResponse { - MessageTemplate::new_with_user("404: Not found", user) + ( + StatusCode::NOT_FOUND, + MessageTemplate::new_with_user("404: Not found", user), + ) } diff --git a/backend/src/services/fragments.rs b/backend/src/services/fragments.rs index d0e18ba..84a2d40 100644 --- a/backend/src/services/fragments.rs +++ b/backend/src/services/fragments.rs @@ -1,8 +1,9 @@ use axum::{ debug_handler, - extract::{Extension, State}, + extract::{Extension, Query, State}, response::{IntoResponse, Result}, }; +use serde::Deserialize; // use tracing::{event, Level}; use crate::{ @@ -10,9 +11,15 @@ use crate::{ html_templates::*, }; +#[derive(Deserialize)] +pub struct CurrentRecipeId { + current_recipe_id: Option, +} + #[debug_handler] pub async fn recipes_list_fragments( State(connection): State, + current_recipe: Query, Extension(user): Extension>, ) -> Result { let recipes = Recipes { @@ -24,8 +31,7 @@ pub async fn recipes_list_fragments( } else { vec![] }, - current_id: None, + current_id: current_recipe.current_recipe_id, }; - Ok(RecipesListFragmentTemplate { user, recipes }) } diff --git a/backend/src/services/ron.rs b/backend/src/services/ron.rs index 89cd896..d6dbe7a 100644 --- a/backend/src/services/ron.rs +++ b/backend/src/services/ron.rs @@ -48,13 +48,19 @@ use axum::{ debug_handler, - extract::{Extension, State}, + extract::{Extension, Query, State}, http::StatusCode, response::{ErrorResponse, IntoResponse, Result}, }; +use serde::Deserialize; // use tracing::{event, Level}; -use crate::{data::db, model, ron_extractor::ExtractRon, ron_utils::ron_error}; +use crate::{ + data::db, + model, + ron_extractor::ExtractRon, + ron_utils::{ron_error, ron_response}, +}; #[allow(dead_code)] #[debug_handler] @@ -81,7 +87,7 @@ pub async fn update_user( Ok(StatusCode::OK) } -async fn check_user_rights( +async fn check_user_rights_recipe( connection: &db::Connection, user: &Option, recipe_id: i64, @@ -100,13 +106,32 @@ async fn check_user_rights( } } +async fn check_user_rights_recipe_group( + connection: &db::Connection, + user: &Option, + group_id: i64, +) -> Result<()> { + if user.is_none() + || !connection + .can_edit_recipe_group(user.as_ref().unwrap().id, group_id) + .await? + { + Err(ErrorResponse::from(ron_error( + StatusCode::UNAUTHORIZED, + "Action not authorized", + ))) + } else { + Ok(()) + } +} + #[debug_handler] pub async fn set_recipe_title( State(connection): State, Extension(user): Extension>, ExtractRon(ron): ExtractRon, ) -> Result { - check_user_rights(&connection, &user, ron.recipe_id).await?; + check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; connection .set_recipe_title(ron.recipe_id, &ron.title) .await?; @@ -119,7 +144,7 @@ pub async fn set_recipe_description( Extension(user): Extension>, ExtractRon(ron): ExtractRon, ) -> Result { - check_user_rights(&connection, &user, ron.recipe_id).await?; + check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; connection .set_recipe_description(ron.recipe_id, &ron.description) .await?; @@ -132,7 +157,7 @@ pub async fn set_estimated_time( Extension(user): Extension>, ExtractRon(ron): ExtractRon, ) -> Result { - check_user_rights(&connection, &user, ron.recipe_id).await?; + check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; connection .set_recipe_estimated_time(ron.recipe_id, ron.estimated_time) .await?; @@ -145,7 +170,7 @@ pub async fn set_difficulty( Extension(user): Extension>, ExtractRon(ron): ExtractRon, ) -> Result { - check_user_rights(&connection, &user, ron.recipe_id).await?; + check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; connection .set_recipe_difficulty(ron.recipe_id, ron.difficulty) .await?; @@ -158,7 +183,7 @@ pub async fn set_language( Extension(user): Extension>, ExtractRon(ron): ExtractRon, ) -> Result { - check_user_rights(&connection, &user, ron.recipe_id).await?; + check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; connection .set_recipe_language(ron.recipe_id, &ron.lang) .await?; @@ -171,13 +196,128 @@ pub async fn set_is_published( Extension(user): Extension>, ExtractRon(ron): ExtractRon, ) -> Result { - check_user_rights(&connection, &user, ron.recipe_id).await?; + check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; connection .set_recipe_is_published(ron.recipe_id, ron.is_published) .await?; Ok(StatusCode::OK) } +impl From for common::ron_api::Group { + fn from(group: model::Group) -> Self { + Self { + id: group.id, + name: group.name, + comment: group.comment, + steps: group + .steps + .into_iter() + .map(common::ron_api::Step::from) + .collect(), + } + } +} + +impl From for common::ron_api::Step { + fn from(step: model::Step) -> Self { + Self { + id: step.id, + action: step.action, + ingredients: step + .ingredients + .into_iter() + .map(common::ron_api::Ingredient::from) + .collect(), + } + } +} + +impl From for common::ron_api::Ingredient { + fn from(ingredient: model::Ingredient) -> Self { + Self { + id: ingredient.id, + name: ingredient.name, + comment: ingredient.comment, + quantity_value: ingredient.quantity_value, + quantity_unit: ingredient.quantity_unit, + } + } +} + +#[derive(Deserialize)] +pub struct RecipeId { + #[serde(rename = "recipe_id")] + id: i64, +} + +#[debug_handler] +pub async fn get_groups( + State(connection): State, + recipe_id: Query, +) -> Result { + println!("PROUT"); + // Here we don't check user rights on purpose. + Ok(ron_response( + StatusCode::OK, + connection + .get_groups(recipe_id.id) + .await? + .into_iter() + .map(common::ron_api::Group::from) + .collect::>(), + )) +} + +#[debug_handler] +pub async fn add_group( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; + let group_id = connection.add_recipe_group(ron.recipe_id).await?; + + Ok(ron_response( + StatusCode::OK, + common::ron_api::AddRecipeGroupResult { group_id }, + )) +} + +#[debug_handler] +pub async fn rm_group( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe_group(&connection, &user, ron.group_id).await?; + connection.rm_recipe_group(ron.group_id).await?; + Ok(StatusCode::OK) +} + +#[debug_handler] +pub async fn set_group_name( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe_group(&connection, &user, ron.group_id).await?; + connection.set_group_name(ron.group_id, &ron.name).await?; + Ok(StatusCode::OK) +} + +#[debug_handler] +pub async fn set_group_comment( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe_group(&connection, &user, ron.group_id).await?; + connection + .set_group_comment(ron.group_id, &ron.comment) + .await?; + Ok(StatusCode::OK) +} + ///// 404 ///// #[debug_handler] pub async fn not_found(Extension(_user): Extension>) -> impl IntoResponse { diff --git a/backend/src/services/user.rs b/backend/src/services/user.rs index a50eff1..5eeee8b 100644 --- a/backend/src/services/user.rs +++ b/backend/src/services/user.rs @@ -22,7 +22,7 @@ use crate::{ utils, AppState, }; -//// SIGN UP ///// +/// SIGN UP /// #[debug_handler] pub async fn sign_up_get( @@ -207,7 +207,7 @@ pub async fn sign_up_validation( } } -///// SIGN IN ///// +/// SIGN IN /// #[debug_handler] pub async fn sign_in_get( @@ -271,7 +271,7 @@ pub async fn sign_in_post( } } -///// SIGN OUT ///// +/// SIGN OUT /// #[debug_handler] pub async fn sign_out( @@ -287,7 +287,7 @@ pub async fn sign_out( Ok((jar, Redirect::to("/"))) } -///// RESET PASSWORD ///// +/// RESET PASSWORD /// #[debug_handler] pub async fn ask_reset_password_get( @@ -510,7 +510,7 @@ pub async fn reset_password_post( } } -///// EDIT PROFILE ///// +/// EDIT PROFILE /// #[debug_handler] pub async fn edit_user_get(Extension(user): Extension>) -> Response { @@ -614,7 +614,7 @@ pub async fn edit_user_post( match connection .update_user( user.id, - Some(&email_trimmed), + Some(email_trimmed), Some(&form_data.name), new_password, ) diff --git a/backend/templates/recipe_edit.html b/backend/templates/recipe_edit.html index fdd54f3..f6134f6 100644 --- a/backend/templates/recipe_edit.html +++ b/backend/templates/recipe_edit.html @@ -13,35 +13,27 @@ - - + + - + + {% endmatch %}"/> - @@ -49,16 +41,19 @@ - {% for lang in languages %} - + {% endfor %} Is published
+ +
+ + +
+
+ + + + + + +
+ + +
+ +
+ + + +
+ + +
+ +
+ + + + + + + + +
diff --git a/common/src/ron_api.rs b/common/src/ron_api.rs index b0209ac..639b0c6 100644 --- a/common/src/ron_api.rs +++ b/common/src/ron_api.rs @@ -65,6 +65,57 @@ pub struct SetIsPublished { pub is_published: bool, } +#[derive(Serialize, Deserialize, Clone)] +pub struct AddRecipeGroup { + pub recipe_id: i64, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct AddRecipeGroupResult { + pub group_id: i64, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct RemoveRecipeGroup { + pub group_id: i64, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct SetGroupName { + pub group_id: i64, + pub name: String, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct SetGroupComment { + pub group_id: i64, + pub comment: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Group { + pub id: i64, + pub name: String, + pub comment: String, + pub steps: Vec, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Step { + pub id: i64, + pub action: String, + pub ingredients: Vec, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Ingredient { + pub id: i64, + pub name: String, + pub comment: String, + pub quantity_value: f64, + pub quantity_unit: String, +} + // #[derive(Serialize, Deserialize, Clone)] // pub struct AddRecipeImage { // pub recipe_id: i64, diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml index 318b8e5..8c388f9 100644 --- a/frontend/Cargo.toml +++ b/frontend/Cargo.toml @@ -13,6 +13,10 @@ default = ["console_error_panic_hook"] [dependencies] common = { path = "../common" } +ron = "0.8" +serde = { version = "1.0", features = ["derive"] } +thiserror = "2" + wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" web-sys = { version = "0.3", features = [ @@ -26,6 +30,7 @@ web-sys = { version = "0.3", features = [ "EventTarget", "HtmlLabelElement", "HtmlInputElement", + "HtmlTextAreaElement", "HtmlSelectElement", ] } diff --git a/frontend/src/handles.rs b/frontend/src/handles.rs index 4eca53b..d5aa8ff 100644 --- a/frontend/src/handles.rs +++ b/frontend/src/handles.rs @@ -1,24 +1,21 @@ use gloo::{console::log, events::EventListener, net::http::Request, utils::document}; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::spawn_local; -use web_sys::{Document, HtmlInputElement, HtmlSelectElement}; +use web_sys::{Element, HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement}; -use crate::toast::{self, Level}; +use common::ron_api; -async fn api_request(body: String, api_name: &str) { - if let Err(error) = Request::put(&format!("/ron-api/recipe/{}", api_name)) - .header("Content-Type", "application/ron") - .body(body) - .unwrap() +use crate::{ + request, + toast::{self, Level}, +}; + +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 { - toast::show(Level::Info, &format!("Internal server error: {}", error)); - } -} - -async fn reload_recipes_list() { - match Request::get("/fragments/recipes_list").send().await { Err(error) => { toast::show(Level::Info, &format!("Internal server error: {}", error)); } @@ -35,17 +32,20 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { let input_title = document().get_element_by_id("input-title").unwrap(); let mut current_title = input_title.dyn_ref::().unwrap().value(); let on_input_title_blur = EventListener::new(&input_title, "blur", move |_event| { - let input_title = document().get_element_by_id("input-title").unwrap(); - let title = input_title.dyn_ref::().unwrap(); + let title = document() + .get_element_by_id("input-title") + .unwrap() + .dyn_into::() + .unwrap(); if title.value() != current_title { current_title = title.value(); - let body = common::ron_api::to_string(common::ron_api::SetRecipeTitle { + let body = ron_api::SetRecipeTitle { recipe_id, title: title.value(), - }); + }; spawn_local(async move { - api_request(body, "set_title").await; - reload_recipes_list().await; + let _ = request::put::<(), _>("recipe/set_title", body).await; + reload_recipes_list(recipe_id).await; }); } }); @@ -54,23 +54,28 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { // Description. { - let input_description = document().get_element_by_id("input-description").unwrap(); - let mut current_description = input_description - .dyn_ref::() + let text_area_description = document() + .get_element_by_id("text-area-description") + .unwrap(); + let mut current_description = text_area_description + .dyn_ref::() .unwrap() .value(); let on_input_description_blur = - EventListener::new(&input_description, "blur", move |_event| { - let input_description = document().get_element_by_id("input-description").unwrap(); - let description = input_description.dyn_ref::().unwrap(); + EventListener::new(&text_area_description, "blur", move |_event| { + let description = document() + .get_element_by_id("text-area-description") + .unwrap() + .dyn_into::() + .unwrap(); if description.value() != current_description { current_description = description.value(); - let body = common::ron_api::to_string(common::ron_api::SetRecipeDescription { + let body = ron_api::SetRecipeDescription { recipe_id, description: description.value(), - }); + }; spawn_local(async move { - api_request(body, "set_description").await; + let _ = request::put::<(), _>("recipe/set_description", body).await; }); } }); @@ -88,30 +93,28 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { .value(); let on_input_estimated_time_blur = EventListener::new(&input_estimated_time, "blur", move |_event| { - let input_estimated_time = document() + let estimated_time = document() .get_element_by_id("input-estimated-time") + .unwrap() + .dyn_into::() .unwrap(); - let estimated_time = input_estimated_time.dyn_ref::().unwrap(); if estimated_time.value() != current_time { let time = if estimated_time.value().is_empty() { None + } else if let Ok(t) = estimated_time.value().parse::() { + Some(t) } else { - if let Ok(t) = estimated_time.value().parse::() { - Some(t) - } else { - estimated_time.set_value(¤t_time); - return; - } + estimated_time.set_value(¤t_time); + return; }; current_time = estimated_time.value(); - let body = - common::ron_api::to_string(common::ron_api::SetRecipeEstimatedTime { - recipe_id, - estimated_time: time, - }); + let body = ron_api::SetRecipeEstimatedTime { + recipe_id, + estimated_time: time, + }; spawn_local(async move { - api_request(body, "set_estimated_time").await; + let _ = request::put::<(), _>("recipe/set_estimated_time", body).await; }); } }); @@ -127,20 +130,23 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { .value(); let on_select_difficulty_blur = EventListener::new(&select_difficulty, "blur", move |_event| { - let select_difficulty = document().get_element_by_id("select-difficulty").unwrap(); - let difficulty = select_difficulty.dyn_ref::().unwrap(); + let difficulty = document() + .get_element_by_id("select-difficulty") + .unwrap() + .dyn_into::() + .unwrap(); if difficulty.value() != current_difficulty { current_difficulty = difficulty.value(); - let body = common::ron_api::to_string(common::ron_api::SetRecipeDifficulty { + let body = ron_api::SetRecipeDifficulty { recipe_id, - difficulty: common::ron_api::Difficulty::try_from( + difficulty: ron_api::Difficulty::try_from( current_difficulty.parse::().unwrap(), ) .unwrap(), - }); + }; spawn_local(async move { - api_request(body, "set_difficulty").await; + let _ = request::put::<(), _>("recipe/set_difficulty", body).await; }); } }); @@ -155,17 +161,20 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { .unwrap() .value(); let on_select_language_blur = EventListener::new(&select_language, "blur", move |_event| { - let select_language = document().get_element_by_id("select-language").unwrap(); - let difficulty = select_language.dyn_ref::().unwrap(); - if difficulty.value() != current_language { - current_language = difficulty.value(); + let language = document() + .get_element_by_id("select-language") + .unwrap() + .dyn_into::() + .unwrap(); + if language.value() != current_language { + current_language = language.value(); - let body = common::ron_api::to_string(common::ron_api::SetRecipeLanguage { + let body = ron_api::SetRecipeLanguage { recipe_id, - lang: difficulty.value(), - }); + lang: language.value(), + }; spawn_local(async move { - api_request(body, "set_language").await; + let _ = request::put::<(), _>("recipe/set_language", body).await; }); } }); @@ -177,22 +186,147 @@ pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> { let input_is_published = document().get_element_by_id("input-is-published").unwrap(); let on_input_is_published_blur = EventListener::new(&input_is_published, "input", move |_event| { - let input_is_published = - document().get_element_by_id("input-is-published").unwrap(); - let is_published = input_is_published.dyn_ref::().unwrap(); + let is_published = document() + .get_element_by_id("input-is-published") + .unwrap() + .dyn_into::() + .unwrap(); - let body = common::ron_api::to_string(common::ron_api::SetIsPublished { + let body = ron_api::SetIsPublished { recipe_id, is_published: is_published.checked(), - }); + }; spawn_local(async move { - api_request(body, "set_is_published").await; - reload_recipes_list().await; + let _ = request::put::<(), _>("recipe/set_is_published", body).await; + reload_recipes_list(recipe_id).await; }); }); on_input_is_published_blur.forget(); } + // let groups_container = document().get_element_by_id("groups-container").unwrap(); + // if !groups_container.has_child_nodes() { + + // } + + fn create_group_element(group_id: i64) -> Element { + let group_html = document() + .query_selector("#hidden-templates .group") + .unwrap() + .unwrap() + .clone_node_with_deep(true) + .unwrap() + .dyn_into::() + .unwrap(); + + group_html + .set_attribute("id", &format!("group-{}", group_id)) + .unwrap(); + + let groups_container = document().get_element_by_id("groups-container").unwrap(); + groups_container.append_child(&group_html).unwrap(); + group_html + } + + fn create_step_element(group_element: &Element, step_id: i64) -> Element { + let step_html = document() + .query_selector("#hidden-templates .step") + .unwrap() + .unwrap() + .clone_node_with_deep(true) + .unwrap() + .dyn_into::() + .unwrap(); + step_html + .set_attribute("id", &format!("step-{}", step_id)) + .unwrap(); + + group_element.append_child(&step_html).unwrap(); + step_html + } + + fn create_ingredient_element(step_element: &Element, ingredient_id: i64) -> Element { + let ingredient_html = document() + .query_selector("#hidden-templates .ingredient") + .unwrap() + .unwrap() + .clone_node_with_deep(true) + .unwrap() + .dyn_into::() + .unwrap(); + ingredient_html + .set_attribute("id", &format!("step-{}", ingredient_id)) + .unwrap(); + + step_element.append_child(&ingredient_html).unwrap(); + ingredient_html + } + + // 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.id); + let input_name = group_element + .query_selector(".input-group-name") + .unwrap() + .unwrap() + .dyn_into::() + .unwrap(); + input_name.set_value(&group.name); + + // document().get_element_by_id(&format!("group-{}", group_id)) + + for step in group.steps { + let step_element = create_step_element(&group_element, step.id); + let text_area_action = step_element + .query_selector(".text-area-step-action") + .unwrap() + .unwrap() + .dyn_into::() + .unwrap(); + text_area_action.set_value(&step.action); + + for ingredient in step.ingredients { + let ingredient_element = + create_ingredient_element(&step_element, ingredient.id); + let input_name = ingredient_element + .query_selector(".input-ingredient-name") + .unwrap() + .unwrap() + .dyn_into::() + .unwrap(); + input_name.set_value(&ingredient.name); + } + } + } + + // log!(format!("{:?}", groups)); + }); + } + + // Add a new group. + { + let button_add_group = document().get_element_by_id("button-add-group").unwrap(); + let on_click_add_group = EventListener::new(&button_add_group, "click", move |_event| { + log!("Click!"); + 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(response.group_id); + // group_html.set_attribute("id", "test").unwrap(); + }); + }); + on_click_add_group.forget(); + } + Ok(()) } diff --git a/frontend/src/lib.rs b/frontend/src/lib.rs index c8e6ce9..073eaef 100644 --- a/frontend/src/lib.rs +++ b/frontend/src/lib.rs @@ -1,10 +1,10 @@ mod handles; +mod request; mod toast; mod utils; -use gloo::{console::log, events::EventListener, utils::window}; +use gloo::utils::window; use wasm_bindgen::prelude::*; -use web_sys::console; // #[wasm_bindgen] // extern "C" { @@ -27,17 +27,14 @@ pub fn main() -> Result<(), JsValue> { let location = window().location().pathname()?; let path: Vec<&str> = location.split('/').skip(1).collect(); - match path[..] { - ["recipe", "edit", id] => { - let id = id.parse::().unwrap(); // TODO: remove unwrap. - handles::recipe_edit(id)?; - } + if let ["recipe", "edit", id] = path[..] { + let id = id.parse::().unwrap(); // TODO: remove unwrap. + handles::recipe_edit(id)?; // Disable: user editing data are now submitted as classic form data. // ["user", "edit"] => { // handles::user_edit(document)?; // } - _ => (), } Ok(()) diff --git a/frontend/src/request.rs b/frontend/src/request.rs new file mode 100644 index 0000000..ab4a398 --- /dev/null +++ b/frontend/src/request.rs @@ -0,0 +1,132 @@ +use gloo::net::http::{Request, RequestBuilder}; +use serde::{de::DeserializeOwned, Serialize}; +use thiserror::Error; + +use common::ron_api; + +use crate::toast::{self, Level}; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Gloo error: {0}")] + Gloo(#[from] gloo::net::Error), + + #[error("RON Spanned error: {0}")] + Ron(#[from] ron::error::SpannedError), + + #[error("HTTP error: {0}")] + Http(String), + + #[error("Unknown error: {0}")] + Other(String), +} + +type Result = std::result::Result; + +const CONTENT_TYPE: &str = "Content-Type"; +const CONTENT_TYPE_RON: &str = "application/ron"; + +async fn req_with_body( + api_name: &str, + body: U, + method_fn: fn(&str) -> RequestBuilder, +) -> Result +where + T: DeserializeOwned, + U: Serialize, +{ + let url = format!("/ron-api/{}", api_name); + let request_builder = method_fn(&url).header(CONTENT_TYPE, CONTENT_TYPE_RON); + send_req(request_builder.body(ron_api::to_string(body))?).await +} + +async fn req_with_params<'a, T, U, V>( + api_name: &str, + params: U, + method_fn: fn(&str) -> RequestBuilder, +) -> Result +where + T: DeserializeOwned, + U: IntoIterator, + V: AsRef, +{ + let url = format!("/ron-api/{}", api_name); + let request_builder = method_fn(&url) + .header(CONTENT_TYPE, CONTENT_TYPE_RON) + .query(params); + send_req(request_builder.build()?).await +} + +async fn send_req(request: Request) -> Result +where + T: DeserializeOwned, +{ + match request.send().await { + Err(error) => { + toast::show(Level::Info, &format!("Internal server error: {}", error)); + Err(Error::Gloo(error)) + } + Ok(response) => { + if !response.ok() { + toast::show( + Level::Info, + &format!("HTTP error: {}", response.status_text()), + ); + Err(Error::Http(response.status_text())) + } else { + // Ok(()) + Ok(ron::de::from_bytes::(&response.binary().await?)?) + } + } + } +} + +pub async fn put(api_name: &str, body: U) -> Result +where + T: DeserializeOwned, + U: Serialize, +{ + req_with_body(api_name, body, Request::put).await +} + +pub async fn post(api_name: &str, body: U) -> Result +where + T: DeserializeOwned, + U: Serialize, +{ + req_with_body(api_name, body, Request::post).await +} + +pub async fn delete(api_name: &str, body: U) -> Result +where + T: DeserializeOwned, + U: Serialize, +{ + req_with_body(api_name, body, Request::delete).await +} + +pub async fn get<'a, T, U, V>(api_name: &str, params: U) -> Result +where + T: DeserializeOwned, + U: IntoIterator, + V: AsRef, +{ + req_with_params(api_name, params, Request::get).await +} + +// pub async fn api_request_get(api_name: &str, params: QueryParams) -> Result +// where +// T: DeserializeOwned, +// { +// match Request::get(&format!("/ron-api/recipe/{}?{}", api_name, params)) +// .header("Content-Type", "application/ron") +// .send() +// .await +// { +// Err(error) => { +// toast::show(Level::Info, &format!("Internal server error: {}", error)); +// Err(error.to_string()) +// } +// Ok(response) => Ok(ron::de::from_bytes::(&response.binary().await.unwrap()).unwrap()), +// } +// } diff --git a/frontend/src/toast.rs b/frontend/src/toast.rs index a2791dc..1c30a71 100644 --- a/frontend/src/toast.rs +++ b/frontend/src/toast.rs @@ -1,5 +1,4 @@ -use gloo::{console::log, timers::callback::Timeout, utils::document}; -use web_sys::{console, Document, HtmlInputElement}; +use gloo::{timers::callback::Timeout, utils::document}; pub enum Level { Success,