From 2119c2e56bd212879bf51107d41ec4fd6c5b8986 Mon Sep 17 00:00:00 2001 From: Josef Rokos Date: Thu, 22 Feb 2024 21:54:00 +0100 Subject: [PATCH] Implemented public form appearance settings. --- Cargo.lock | 74 +++++++++++++++ Cargo.toml | 3 + assets/banner.css | 20 ++++ src/app.rs | 6 ++ src/backend/appearance.rs | 156 +++++++++++++++++++++++++++++++ src/backend/data.rs | 15 +++ src/backend/mod.rs | 1 + src/components/admin_portal.rs | 9 +- src/components/header.rs | 1 + src/error.rs | 13 ++- src/locales/catalogues.rs | 12 ++- src/main.rs | 5 + src/pages/appearance_settings.rs | 127 +++++++++++++++++++++++++ src/pages/mod.rs | 1 + src/pages/public.rs | 30 ++++++ 15 files changed, 469 insertions(+), 4 deletions(-) create mode 100644 assets/banner.css create mode 100644 src/backend/appearance.rs create mode 100644 src/pages/appearance_settings.rs diff --git a/Cargo.lock b/Cargo.lock index 7b2a869..6ccaab9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,6 +91,44 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "actix-multipart" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b960e2aea75f49c8f069108063d12a48d329fc8b60b786dfc7552a9d5918d2d" +dependencies = [ + "actix-multipart-derive", + "actix-utils", + "actix-web", + "bytes", + "derive_more", + "futures-core", + "futures-util", + "httparse", + "local-waker", + "log", + "memchr", + "mime", + "serde", + "serde_json", + "serde_plain", + "tempfile", + "tokio", +] + +[[package]] +name = "actix-multipart-derive" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a0a77f836d869f700e5b47ac7c3c8b9c8bc82e4aec861954c6198abee3ebd4d" +dependencies = [ + "darling 0.20.3", + "parse-size", + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "actix-router" version = "0.5.1" @@ -2383,6 +2421,24 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "multer" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15d522be0a9c3e46fd2632e272d178f56387bdb5c9fbb3a36c649062e9b5219" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http 1.0.0", + "httparse", + "log", + "memchr", + "mime", + "spin 0.9.8", + "version_check", +] + [[package]] name = "native-tls" version = "0.2.11" @@ -2565,6 +2621,12 @@ dependencies = [ "windows-targets 0.48.0", ] +[[package]] +name = "parse-size" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "944553dd59c802559559161f9816429058b869003836120e262e8caec061b7ae" + [[package]] name = "paste" version = "1.0.12" @@ -2904,6 +2966,7 @@ name = "rezervator" version = "0.1.0" dependencies = [ "actix-files", + "actix-multipart", "actix-session", "actix-web", "base64 0.21.7", @@ -2927,6 +2990,7 @@ dependencies = [ "regex", "rust_decimal", "serde", + "server_fn", "sqlx", "toml 0.8.8", "uuid", @@ -3231,6 +3295,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + [[package]] name = "serde_qs" version = "0.12.0" @@ -3288,6 +3361,7 @@ dependencies = [ "http 1.0.0", "inventory", "js-sys", + "multer", "once_cell", "send_wrapper", "serde", diff --git a/Cargo.toml b/Cargo.toml index bcc3ebe..9e42387 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,11 +10,13 @@ crate-type = ["cdylib", "rlib"] actix-files = { version = "0.6.2", optional = true } actix-web = { version = "4.4.0", optional = true, features = ["macros"] } actix-session = { version = "0.8.0", optional = true, features = ["cookie-session"] } +actix-multipart = { version = "0.6.1", optional = true } console_error_panic_hook = "0.1" cfg-if = "1" leptos = { version = "0.6.5" } leptos_meta = { version = "0.6.5" } leptos_actix = { version = "0.6.5", optional = true } +server_fn = { version = "0.6.5", features = ["multipart"] } leptos_router = { version = "0.6.5" } serde = { version = "1", features = ["derive"] } wasm-bindgen = "=0.2.90" @@ -47,6 +49,7 @@ ssr = [ "dep:actix-web", "dep:leptos_actix", "dep:actix-session", + "dep:actix-multipart", "dep:sqlx", "dep:lettre", "dep:charts-rs", diff --git a/assets/banner.css b/assets/banner.css new file mode 100644 index 0000000..2eb7f97 --- /dev/null +++ b/assets/banner.css @@ -0,0 +1,20 @@ +div.header_banner { + height:100px; + clip-path: inset(0 0 0 0); + /*bg-img*/ + background-size:cover; + padding:30px; + color: white; +} + +h1.header_banner { + font-size: xxx-large; + color: white; + text-shadow: -3px -3px 0 #000, 3px -3px 0 #000, -3px 3px 0 #000, 3px 3px 0 #000; +} + +@media (min-width: 1200px) { + div.header_banner { + height: 200px; + } +} \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index 640a2c5..9cc1b2f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -8,6 +8,7 @@ use crate::components::admin_portal::AdminPortal; use crate::components::header::Header; use crate::components::user_menu::MenuOpener; use crate::pages::all_reservations::Bookings; +use crate::pages::appearance_settings::Appearance; use crate::pages::customers::Customers; use crate::pages::login::Login; use crate::pages::mail_settings::MailSettings; @@ -103,6 +104,11 @@ pub fn App() -> impl IntoView { }/> + + + + }/> diff --git a/src/backend/appearance.rs b/src/backend/appearance.rs new file mode 100644 index 0000000..c11c9a7 --- /dev/null +++ b/src/backend/appearance.rs @@ -0,0 +1,156 @@ +use cfg_if::cfg_if; +use leptos::*; +use validator::Validate; +use crate::backend::data::{ApiResponse, Appearance}; +use crate::components::data_form::ForValidation; + +cfg_if! { if #[cfg(feature = "ssr")] { + + use actix_web::{post, Responder}; + use actix_multipart::Multipart; + use actix_session::Session; + use actix_web::web::Redirect; + use std::fs::File; + use std::io::{Write, Read}; + use futures_util::{StreamExt, TryStreamExt}; + use crate::backend::data::User; + use crate::error::AppError; + use sqlx::{query, query_as, PgPool}; + use crate::backend::get_pool; + use actix_web::web::Data; + use crate::backend::AppData; + use regex::Regex; + + pub async fn check_appearance(pool: &PgPool) -> Result<(), AppError> { + let count: (i64,) = query_as("SELECT COUNT(id) FROM appearance") + .fetch_one(pool) + .await?; + + if count.0 == 0 { + query("INSERT INTO appearance(title) VALUES('Rezervovator')") + .execute(pool) + .await?; + } + + Ok(()) + } + + async fn set_banner_name(file_name: &str, pool: &PgPool) -> Result<(), AppError> { + query("UPDATE appearance SET banner = $1") + .bind(file_name) + .execute(pool) + .await?; + + Ok(()) + } + + async fn modify_style(file_name: &str) -> Result<(), AppError> { + let mut css_file = File::open("target/site/banner.css")?; + let mut css_str= String::new(); + css_file.read_to_string(&mut css_str)?; + + if css_str.contains("/*bg-img*/") { + css_str = css_str.replace("/*bg-img*/", &format!("background-image: url('{}');", file_name)); + } else { + let re = Regex::new(r#"background-image: url\('[aA-zZ._0-9\-]+'\)"#).unwrap(); + css_str = re.replace(&css_str, &format!("background-image: url('{}')", file_name)).to_string(); + } + + let mut css_file = File::create("target/site/banner.css")?; + css_file.write_all(css_str.as_bytes())?; + + Ok(()) + } + + #[post("/upload")] + pub async fn upload(mut data: Multipart, session: Session, app_data: Data) -> impl Responder { + let user = session.get::("user").unwrap_or(None); + + if user.is_none() { + return Redirect::to("/login").see_other(); + } + + if let Some(u) = user { + if !u.admin { + return Redirect::to("/admin/appearance").see_other(); + } + } + + if let Ok(Some(mut field)) = data.try_next().await { + let content_disp = field.content_disposition().clone(); + let file_name = content_disp.get_filename().unwrap(); + + if file_name.is_empty() { + return Redirect::to("/admin/appearance").see_other(); + } + + let mut file = File::create(format!("target/site/{}", file_name)).unwrap(); + let _name = field.name(); + while let Some(chunk) = field.next().await { + let c = chunk.unwrap(); + let _ = file.write_all(&c); + } + let _ = set_banner_name(file_name, &app_data.db_pool).await; + let _ = modify_style(file_name).await; + } + + Redirect::to("/admin/appearance").see_other() + } + +}} + +#[server] +pub async fn get_appearance() -> Result { + let pool = get_pool().await?; + let appearance = query_as::<_, Appearance>("SELECT * FROM appearance") + .fetch_one(&pool) + .await?; + + Ok(appearance) +} + +#[server] +pub async fn update_appearance(appearance: Appearance) -> Result, ServerFnError> { + use crate::perm_check; + + perm_check!(is_admin); + + let pool = get_pool().await?; + let id = appearance.id(); + query("UPDATE appearance SET title = $1, text = $2 WHERE id = $3") + .bind(appearance.title) + .bind(appearance.text) + .bind(id) + .execute(&pool) + .await?; + + Ok(ApiResponse::Data(())) +} + +impl ForValidation for UpdateAppearance { + fn entity(&self) -> &dyn Validate { + &self.appearance + } +} + +#[server] +pub async fn delete_banner() -> Result, ServerFnError> { + use std::fs; + use crate::perm_check; + + perm_check!(is_admin); + + let appearance = get_appearance().await?; + let pool = get_pool().await?; + query("UPDATE appearance SET banner = $1 WHERE id = $2") + .bind(None::) + .bind(appearance.id()) + .execute(&pool) + .await?; + + if let Some(f) = appearance.banner { + fs::remove_file(format!("target/site/{}", f))?; + } + + Ok(ApiResponse::Data(())) +} \ No newline at end of file diff --git a/src/backend/data.rs b/src/backend/data.rs index 7c08382..fef715c 100644 --- a/src/backend/data.rs +++ b/src/backend/data.rs @@ -622,3 +622,18 @@ pub struct ChartData { pub count: i64, pub period: f64 } + +#[derive(Clone, Debug, Default, Serialize, Deserialize, Validate)] +#[cfg_attr(feature = "ssr", derive(sqlx::FromRow))] +pub struct Appearance { + id: i32, + pub banner: Option, + pub text: Option, + pub title: Option +} + +impl Appearance { + pub fn id(&self) -> i32 { + self.id + } +} diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 112eedf..92549f6 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -8,6 +8,7 @@ pub mod property; pub mod reservation; pub mod customer; pub mod mail; +pub mod appearance; #[macro_export] macro_rules! perm_check { diff --git a/src/components/admin_portal.rs b/src/components/admin_portal.rs index dafec98..be10644 100644 --- a/src/components/admin_portal.rs +++ b/src/components/admin_portal.rs @@ -51,6 +51,12 @@ fn settings_menu(opener: MenuOpener) -> impl IntoView { {trl("Mail settings")} +
  • + + + {trl("Appearance")} + +
  • } } @@ -133,11 +139,12 @@ pub fn AdminPortal(children: Children) -> impl IntoView { //