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 {
//
-
+
-
+
//
-
diff --git a/src/components/header.rs b/src/components/header.rs
index 157fa61..a576659 100644
--- a/src/components/header.rs
+++ b/src/components/header.rs
@@ -38,6 +38,7 @@ pub fn Header() -> impl IntoView {
+
//
diff --git a/src/error.rs b/src/error.rs
index 07efc6b..84bd88a 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -10,7 +10,8 @@ pub enum AppError {
FatalError(String),
SlotParseError,
MailAddrParseErr(String),
- MailSendError(String)
+ MailSendError(String),
+ FileIOError(String)
}
impl AppError {
@@ -21,7 +22,8 @@ impl AppError {
AppError::FatalError(e) => {format!("Fatal error: {}", e)},
AppError::SlotParseError => {"Book slot parse error".to_string()},
AppError::MailAddrParseErr(e) => {format!("Cannot parse email address: {}", e)},
- AppError::MailSendError(e) => {format!("Cannot send email: {}", e)}
+ AppError::MailSendError(e) => {format!("Cannot send email: {}", e)},
+ AppError::FileIOError(e) => {format!("IO error: {}", e)}
}
}
}
@@ -79,4 +81,11 @@ impl From for AppError {
fn from(value: lettre::transport::file::Error) -> Self {
AppError::MailSendError(value.to_string())
}
+}
+
+#[cfg(feature = "ssr")]
+impl From for AppError {
+ fn from(value: std::io::Error) -> Self {
+ AppError::FileIOError(value.to_string())
+ }
}
\ No newline at end of file
diff --git a/src/locales/catalogues.rs b/src/locales/catalogues.rs
index abe3488..df4ede9 100644
--- a/src/locales/catalogues.rs
+++ b/src/locales/catalogues.rs
@@ -132,7 +132,17 @@ lazy_static! {
("Are you sure you want to delete property ", "Opravdu chcete smazat předmět "),
("Delete property", "Smazat předmět"),
("Are you sure you want to delete user ", "Opravdu chcete smazat uživatele "),
- ("Remember for next time", "Zapamatovat pro příště")
+ ("Remember for next time", "Zapamatovat pro příště"),
+ ("Appearance settings", "Nastavení vzhledu"),
+ ("Top banner", "Titulní banner"),
+ ("Banner file", "Soubor banneru"),
+ ("Upload", "Nahrát"),
+ ("Edit title", "Upravit titulek"),
+ ("Edit text", "Upravit text"),
+ ("Update text", "Upravit text"),
+ ("Update title", "Upravit titulek"),
+ ("Are you sure you want to delete banner?", "Opravdu chcete smazat banner?"),
+ ("Delete banner", "Smazat banner")
])),
("sk", HashMap::from( [
("Dashboard", "Prehlad"),
diff --git a/src/main.rs b/src/main.rs
index 9d2daad..29a511e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,5 +1,6 @@
use leptos_captcha::spow::pow::Pow;
use log::error;
+use rezervator::backend::appearance::check_appearance;
use rezervator::backend::company::check_company;
use rezervator::backend::mail::check_messages;
use rezervator::backend::user::create_admin;
@@ -67,6 +68,9 @@ async fn main() -> std::io::Result<()> {
if let Err(e) = check_messages(&pool).await {
error!("Error while checking messages: {:?}", e);
}
+ if let Err(e) = check_appearance(&pool).await {
+ error!("Error while checking messages: {:?}", e);
+ }
HttpServer::new(move || {
let leptos_options = &conf.leptos_options;
@@ -85,6 +89,7 @@ async fn main() -> std::io::Result<()> {
routes.to_owned(),
|| view! { },
)
+ .service(appearance::upload)
.service(Files::new("/", site_root))
//.wrap(middleware::Compress::default())
})
diff --git a/src/pages/appearance_settings.rs b/src/pages/appearance_settings.rs
new file mode 100644
index 0000000..8008cea
--- /dev/null
+++ b/src/pages/appearance_settings.rs
@@ -0,0 +1,127 @@
+use leptos::*;
+use crate::backend::appearance::{DeleteBanner, get_appearance, UpdateAppearance};
+use crate::backend::data::Appearance;
+use crate::components::data_form::{DataForm, QuestionDialog};
+use crate::components::modal_box::DialogOpener;
+use crate::locales::trl;
+
+#[component]
+fn edit_title(opener: DialogOpener, appearance: ReadSignal) -> impl IntoView {
+ let update = create_server_action::();
+ view! {
+
+
+
+
+
+ }
+}
+
+#[component]
+fn edit_text(opener: DialogOpener, appearance: ReadSignal) -> impl IntoView {
+ let update = create_server_action::();
+ view! {
+
+
+
+
+
+ }
+}
+
+#[component]
+fn delete_banner(opener: DialogOpener) -> impl IntoView {
+ let delete = create_server_action::();
+ view! {
+
+
{trl("Are you sure you want to delete banner?")}
+
+ }
+}
+
+#[component]
+pub fn appearance() -> impl IntoView {
+ let title_edit = DialogOpener::new();
+ let text_edit = DialogOpener::new();
+ let del_dialog = DialogOpener::new();
+ let appearance = create_resource(move || title_edit.visible() || text_edit.visible() || del_dialog.visible(), |_| get_appearance());
+ let app_edit = create_rw_signal(Appearance::default());
+ view! {
+
+
+
+ {trl("Appearance settings")}
+
+
+
+
{trl("Top banner")}
+
+
{trl("Loading...")} }>
+ {
+ appearance.get().map(|a| match a {
+ Ok(a) => {
+ app_edit.set(a.clone());
+ let app = a.clone();
+ view! {
+
+
+
+
+
+
+
+
+
+ {app.text.unwrap_or("<< TEXT >>".to_string())}
+
+
+ {trl("Edit text")}
+
+
+ }
+ }
+ Err(e) => {view! {{trl("Error loading data")}
+
{e.to_string()}
+ }}
+ })
+ }
+
+
+
+
+
+ }
+}
\ No newline at end of file
diff --git a/src/pages/mod.rs b/src/pages/mod.rs
index 57f5784..1695663 100644
--- a/src/pages/mod.rs
+++ b/src/pages/mod.rs
@@ -21,4 +21,5 @@ pub mod mail_settings;
mod mail_view;
pub mod all_reservations;
pub mod customers;
+pub mod appearance_settings;
diff --git a/src/pages/public.rs b/src/pages/public.rs
index 006f872..ff56d3e 100644
--- a/src/pages/public.rs
+++ b/src/pages/public.rs
@@ -3,6 +3,7 @@ use leptos::*;
use leptos_captcha::{Captcha, pow_dispatch};
use leptos_router::*;
use rust_decimal::Decimal;
+use crate::backend::appearance::get_appearance;
use crate::backend::customer::get_remembered;
use crate::backend::data::{ApiResponse, Customer, DayHour, Reservation, ResProperty, SlotType, TmCheck};
use crate::backend::reservation::{CreateReservation, get_public_form_data, is_reserved};
@@ -103,6 +104,7 @@ pub fn Public() -> impl IntoView {
let is_pending = create_rw_signal(None);
let active_str = create_rw_signal("true".to_string());
let get_customer = create_blocking_resource(||(), move |_| get_remembered());
+ let appearance = create_blocking_resource(||(), move |_| get_appearance());
let customer = create_rw_signal(Customer::default());
create_effect(move |_| {
@@ -117,6 +119,34 @@ pub fn Public() -> impl IntoView {
price=price.write_only()
slots=slots.write_only()
captcha_state=is_pending.read_only()/>
+
+ {trl("Loading...")}
}>
+ {
+ appearance.get().map(|a| match a {
+ Ok(a) => {
+ let app = a.clone();
+ view! {
+
+
+
+
+
+
+ {app.text.unwrap_or("".to_string())}
+
+
+
+ }
+ },
+ Err(e) => {view! {{trl("Error loading data")}
+
{e.to_string()}
+ }}
+ })
+ }
+
+