Implemented public form appearance settings.
This commit is contained in:
Generated
+74
@@ -91,6 +91,44 @@ dependencies = [
|
|||||||
"syn 1.0.109",
|
"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]]
|
[[package]]
|
||||||
name = "actix-router"
|
name = "actix-router"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@@ -2383,6 +2421,24 @@ dependencies = [
|
|||||||
"windows-sys 0.48.0",
|
"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]]
|
[[package]]
|
||||||
name = "native-tls"
|
name = "native-tls"
|
||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
@@ -2565,6 +2621,12 @@ dependencies = [
|
|||||||
"windows-targets 0.48.0",
|
"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]]
|
[[package]]
|
||||||
name = "paste"
|
name = "paste"
|
||||||
version = "1.0.12"
|
version = "1.0.12"
|
||||||
@@ -2904,6 +2966,7 @@ name = "rezervator"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-files",
|
"actix-files",
|
||||||
|
"actix-multipart",
|
||||||
"actix-session",
|
"actix-session",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
@@ -2927,6 +2990,7 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
"rust_decimal",
|
"rust_decimal",
|
||||||
"serde",
|
"serde",
|
||||||
|
"server_fn",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"toml 0.8.8",
|
"toml 0.8.8",
|
||||||
"uuid",
|
"uuid",
|
||||||
@@ -3231,6 +3295,15 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_plain"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_qs"
|
name = "serde_qs"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
@@ -3288,6 +3361,7 @@ dependencies = [
|
|||||||
"http 1.0.0",
|
"http 1.0.0",
|
||||||
"inventory",
|
"inventory",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
"multer",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"send_wrapper",
|
"send_wrapper",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ crate-type = ["cdylib", "rlib"]
|
|||||||
actix-files = { version = "0.6.2", optional = true }
|
actix-files = { version = "0.6.2", optional = true }
|
||||||
actix-web = { version = "4.4.0", optional = true, features = ["macros"] }
|
actix-web = { version = "4.4.0", optional = true, features = ["macros"] }
|
||||||
actix-session = { version = "0.8.0", optional = true, features = ["cookie-session"] }
|
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"
|
console_error_panic_hook = "0.1"
|
||||||
cfg-if = "1"
|
cfg-if = "1"
|
||||||
leptos = { version = "0.6.5" }
|
leptos = { version = "0.6.5" }
|
||||||
leptos_meta = { version = "0.6.5" }
|
leptos_meta = { version = "0.6.5" }
|
||||||
leptos_actix = { version = "0.6.5", optional = true }
|
leptos_actix = { version = "0.6.5", optional = true }
|
||||||
|
server_fn = { version = "0.6.5", features = ["multipart"] }
|
||||||
leptos_router = { version = "0.6.5" }
|
leptos_router = { version = "0.6.5" }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
wasm-bindgen = "=0.2.90"
|
wasm-bindgen = "=0.2.90"
|
||||||
@@ -47,6 +49,7 @@ ssr = [
|
|||||||
"dep:actix-web",
|
"dep:actix-web",
|
||||||
"dep:leptos_actix",
|
"dep:leptos_actix",
|
||||||
"dep:actix-session",
|
"dep:actix-session",
|
||||||
|
"dep:actix-multipart",
|
||||||
"dep:sqlx",
|
"dep:sqlx",
|
||||||
"dep:lettre",
|
"dep:lettre",
|
||||||
"dep:charts-rs",
|
"dep:charts-rs",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ use crate::components::admin_portal::AdminPortal;
|
|||||||
use crate::components::header::Header;
|
use crate::components::header::Header;
|
||||||
use crate::components::user_menu::MenuOpener;
|
use crate::components::user_menu::MenuOpener;
|
||||||
use crate::pages::all_reservations::Bookings;
|
use crate::pages::all_reservations::Bookings;
|
||||||
|
use crate::pages::appearance_settings::Appearance;
|
||||||
use crate::pages::customers::Customers;
|
use crate::pages::customers::Customers;
|
||||||
use crate::pages::login::Login;
|
use crate::pages::login::Login;
|
||||||
use crate::pages::mail_settings::MailSettings;
|
use crate::pages::mail_settings::MailSettings;
|
||||||
@@ -103,6 +104,11 @@ pub fn App() -> impl IntoView {
|
|||||||
<Customers/>
|
<Customers/>
|
||||||
</AdminPortal>
|
</AdminPortal>
|
||||||
}/>
|
}/>
|
||||||
|
<Route path="admin/appearance" view=|| view! {
|
||||||
|
<AdminPortal>
|
||||||
|
<Appearance/>
|
||||||
|
</AdminPortal>
|
||||||
|
}/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@@ -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<AppData>) -> impl Responder {
|
||||||
|
let user = session.get::<User>("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<Appearance, ServerFnError> {
|
||||||
|
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<ApiResponse<()>, 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<ApiResponse<()>, 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::<String>)
|
||||||
|
.bind(appearance.id())
|
||||||
|
.execute(&pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Some(f) = appearance.banner {
|
||||||
|
fs::remove_file(format!("target/site/{}", f))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ApiResponse::Data(()))
|
||||||
|
}
|
||||||
@@ -622,3 +622,18 @@ pub struct ChartData {
|
|||||||
pub count: i64,
|
pub count: i64,
|
||||||
pub period: f64
|
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<String>,
|
||||||
|
pub text: Option<String>,
|
||||||
|
pub title: Option<String>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Appearance {
|
||||||
|
pub fn id(&self) -> i32 {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ pub mod property;
|
|||||||
pub mod reservation;
|
pub mod reservation;
|
||||||
pub mod customer;
|
pub mod customer;
|
||||||
pub mod mail;
|
pub mod mail;
|
||||||
|
pub mod appearance;
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! perm_check {
|
macro_rules! perm_check {
|
||||||
|
|||||||
@@ -51,6 +51,12 @@ fn settings_menu(opener: MenuOpener) -> impl IntoView {
|
|||||||
<span class="align-middle">{trl("Mail settings")}</span>
|
<span class="align-middle">{trl("Mail settings")}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="/admin/appearance">
|
||||||
|
<i class="bx bx-envelope me-2"></i>
|
||||||
|
<span class="align-middle">{trl("Appearance")}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,11 +139,12 @@ pub fn AdminPortal(children: Children) -> impl IntoView {
|
|||||||
//<!-- /Settings -->
|
//<!-- /Settings -->
|
||||||
|
|
||||||
<ul class="navbar-nav flex-row align-items-center ms-auto">
|
<ul class="navbar-nav flex-row align-items-center ms-auto">
|
||||||
<SettingsMenu opener=settings_menu/>
|
|
||||||
<li class="nav-item navbar-dropdown dropdown-user dropdown">
|
<li class="nav-item navbar-dropdown dropdown-user dropdown">
|
||||||
<a class="nav-link dropdown-toggle hide-arrow" href="#" on:click=move |_| settings_menu.toggle()>
|
<a class="nav-link dropdown-toggle hide-arrow" href="#" on:click=move |_| settings_menu.toggle()>
|
||||||
<i class="bx bx-cog fs-3 lh-0"></i>
|
<i class="bx bx-cog fs-3 lh-0"></i>
|
||||||
</a>
|
</a>
|
||||||
|
<SettingsMenu opener=settings_menu/>
|
||||||
</li>
|
</li>
|
||||||
//<!-- User -->
|
//<!-- User -->
|
||||||
<li class="nav-item navbar-dropdown dropdown-user dropdown">
|
<li class="nav-item navbar-dropdown dropdown-user dropdown">
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ pub fn Header() -> impl IntoView {
|
|||||||
<Link rel="stylesheet" href="/vendor/css/core.css" />
|
<Link rel="stylesheet" href="/vendor/css/core.css" />
|
||||||
<Link rel="stylesheet" href="/vendor/css/theme-default.css" />
|
<Link rel="stylesheet" href="/vendor/css/theme-default.css" />
|
||||||
<Link rel="stylesheet" href="/css/demo.css" />
|
<Link rel="stylesheet" href="/css/demo.css" />
|
||||||
|
<Link rel="stylesheet" href="/banner.css" />
|
||||||
|
|
||||||
//<!-- Vendors CSS -->
|
//<!-- Vendors CSS -->
|
||||||
<Link rel="stylesheet" href="/vendor/libs/perfect-scrollbar/perfect-scrollbar.css" />
|
<Link rel="stylesheet" href="/vendor/libs/perfect-scrollbar/perfect-scrollbar.css" />
|
||||||
|
|||||||
+11
-2
@@ -10,7 +10,8 @@ pub enum AppError {
|
|||||||
FatalError(String),
|
FatalError(String),
|
||||||
SlotParseError,
|
SlotParseError,
|
||||||
MailAddrParseErr(String),
|
MailAddrParseErr(String),
|
||||||
MailSendError(String)
|
MailSendError(String),
|
||||||
|
FileIOError(String)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppError {
|
impl AppError {
|
||||||
@@ -21,7 +22,8 @@ impl AppError {
|
|||||||
AppError::FatalError(e) => {format!("Fatal error: {}", e)},
|
AppError::FatalError(e) => {format!("Fatal error: {}", e)},
|
||||||
AppError::SlotParseError => {"Book slot parse error".to_string()},
|
AppError::SlotParseError => {"Book slot parse error".to_string()},
|
||||||
AppError::MailAddrParseErr(e) => {format!("Cannot parse email address: {}", e)},
|
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)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,3 +82,10 @@ impl From<lettre::transport::file::Error> for AppError {
|
|||||||
AppError::MailSendError(value.to_string())
|
AppError::MailSendError(value.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
impl From<std::io::Error> for AppError {
|
||||||
|
fn from(value: std::io::Error) -> Self {
|
||||||
|
AppError::FileIOError(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -132,7 +132,17 @@ lazy_static! {
|
|||||||
("Are you sure you want to delete property ", "Opravdu chcete smazat předmět "),
|
("Are you sure you want to delete property ", "Opravdu chcete smazat předmět "),
|
||||||
("Delete property", "Smazat předmět"),
|
("Delete property", "Smazat předmět"),
|
||||||
("Are you sure you want to delete user ", "Opravdu chcete smazat uživatele "),
|
("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( [
|
("sk", HashMap::from( [
|
||||||
("Dashboard", "Prehlad"),
|
("Dashboard", "Prehlad"),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use leptos_captcha::spow::pow::Pow;
|
use leptos_captcha::spow::pow::Pow;
|
||||||
use log::error;
|
use log::error;
|
||||||
|
use rezervator::backend::appearance::check_appearance;
|
||||||
use rezervator::backend::company::check_company;
|
use rezervator::backend::company::check_company;
|
||||||
use rezervator::backend::mail::check_messages;
|
use rezervator::backend::mail::check_messages;
|
||||||
use rezervator::backend::user::create_admin;
|
use rezervator::backend::user::create_admin;
|
||||||
@@ -67,6 +68,9 @@ async fn main() -> std::io::Result<()> {
|
|||||||
if let Err(e) = check_messages(&pool).await {
|
if let Err(e) = check_messages(&pool).await {
|
||||||
error!("Error while checking messages: {:?}", e);
|
error!("Error while checking messages: {:?}", e);
|
||||||
}
|
}
|
||||||
|
if let Err(e) = check_appearance(&pool).await {
|
||||||
|
error!("Error while checking messages: {:?}", e);
|
||||||
|
}
|
||||||
|
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
let leptos_options = &conf.leptos_options;
|
let leptos_options = &conf.leptos_options;
|
||||||
@@ -85,6 +89,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
routes.to_owned(),
|
routes.to_owned(),
|
||||||
|| view! { <App/> },
|
|| view! { <App/> },
|
||||||
)
|
)
|
||||||
|
.service(appearance::upload)
|
||||||
.service(Files::new("/", site_root))
|
.service(Files::new("/", site_root))
|
||||||
//.wrap(middleware::Compress::default())
|
//.wrap(middleware::Compress::default())
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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<Appearance>) -> impl IntoView {
|
||||||
|
let update = create_server_action::<UpdateAppearance>();
|
||||||
|
view! {
|
||||||
|
<DataForm opener=opener action=update title="Update title">
|
||||||
|
<input type="hidden" prop:value={move || appearance.get().text.unwrap_or_default()} name="appearance[text]" />
|
||||||
|
<input type="hidden" prop:value={move || appearance.get().id()} name="appearance[id]"/>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col mb-3">
|
||||||
|
<label for="title" class="form-label">{trl("Title")}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="title"
|
||||||
|
class="form-control"
|
||||||
|
placeholder={trl("Enter title")}
|
||||||
|
prop:value={move || appearance.get().title.unwrap_or_default()}
|
||||||
|
name="appearance[title]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DataForm>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn edit_text(opener: DialogOpener, appearance: ReadSignal<Appearance>) -> impl IntoView {
|
||||||
|
let update = create_server_action::<UpdateAppearance>();
|
||||||
|
view! {
|
||||||
|
<DataForm opener=opener action=update title="Update text">
|
||||||
|
<input type="hidden" prop:value={move || appearance.get().title.unwrap_or_default()} name="appearance[title]" />
|
||||||
|
<input type="hidden" prop:value={move || appearance.get().id()} name="appearance[id]"/>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col mb-3">
|
||||||
|
<label for="text" class="form-label">{trl("Text")}</label>
|
||||||
|
<textarea
|
||||||
|
id="title"
|
||||||
|
class="form-control"
|
||||||
|
prop:value={move || appearance.get().text.unwrap_or_default()}
|
||||||
|
name="appearance[text]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DataForm>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn delete_banner(opener: DialogOpener) -> impl IntoView {
|
||||||
|
let delete = create_server_action::<DeleteBanner>();
|
||||||
|
view! {
|
||||||
|
<QuestionDialog opener=opener action=delete title="Delete banner">
|
||||||
|
<div>{trl("Are you sure you want to delete banner?")}</div>
|
||||||
|
</QuestionDialog>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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! {
|
||||||
|
<EditTitle opener=title_edit appearance=app_edit.read_only()/>
|
||||||
|
<EditText opener=text_edit appearance=app_edit.read_only()/>
|
||||||
|
<DeleteBanner opener=del_dialog/>
|
||||||
|
<h1>{trl("Appearance settings")}</h1>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{trl("Top banner")}</h5>
|
||||||
|
<form method="post" action="/upload" enctype="multipart/form-data">
|
||||||
|
<label for="banner_file">{trl("Banner file")}</label>
|
||||||
|
<input id="banner_file" type="file" class="form-control" name="file_to_upload"/>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
{trl("Upload")}
|
||||||
|
</button>
|
||||||
|
</form><br/>
|
||||||
|
<Transition fallback=move || view! {<p>{trl("Loading...")}</p> }>
|
||||||
|
{
|
||||||
|
appearance.get().map(|a| match a {
|
||||||
|
Ok(a) => {
|
||||||
|
app_edit.set(a.clone());
|
||||||
|
let app = a.clone();
|
||||||
|
view! {
|
||||||
|
<div>
|
||||||
|
<Show when=move || a.clone().banner.is_some()>
|
||||||
|
<div class="header_banner">
|
||||||
|
<h1 class="header_banner">{app.title.clone().unwrap_or_default()}</h1>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-info" on:click=move |_| title_edit.show()>
|
||||||
|
<i class="bx bx-edit-alt me-1"></i> {trl("Edit title")}</button>
|
||||||
|
<button class="btn btn-danger" on:click=move |_| del_dialog.show()>
|
||||||
|
{trl("Delete")}
|
||||||
|
</button>
|
||||||
|
<br/><br/>
|
||||||
|
<div>
|
||||||
|
{app.text.unwrap_or("<< TEXT >>".to_string())}
|
||||||
|
</div>
|
||||||
|
<a class="dropdown-item" href="javascript:void(0);" on:click=move |_| text_edit.show()>
|
||||||
|
<i class="bx bx-edit-alt me-1"></i> {trl("Edit text")}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {view! {<div><p>{trl("Error loading data")}</p>
|
||||||
|
<p>{e.to_string()}</p></div>
|
||||||
|
}}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Transition>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,4 +21,5 @@ pub mod mail_settings;
|
|||||||
mod mail_view;
|
mod mail_view;
|
||||||
pub mod all_reservations;
|
pub mod all_reservations;
|
||||||
pub mod customers;
|
pub mod customers;
|
||||||
|
pub mod appearance_settings;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use leptos::*;
|
|||||||
use leptos_captcha::{Captcha, pow_dispatch};
|
use leptos_captcha::{Captcha, pow_dispatch};
|
||||||
use leptos_router::*;
|
use leptos_router::*;
|
||||||
use rust_decimal::Decimal;
|
use rust_decimal::Decimal;
|
||||||
|
use crate::backend::appearance::get_appearance;
|
||||||
use crate::backend::customer::get_remembered;
|
use crate::backend::customer::get_remembered;
|
||||||
use crate::backend::data::{ApiResponse, Customer, DayHour, Reservation, ResProperty, SlotType, TmCheck};
|
use crate::backend::data::{ApiResponse, Customer, DayHour, Reservation, ResProperty, SlotType, TmCheck};
|
||||||
use crate::backend::reservation::{CreateReservation, get_public_form_data, is_reserved};
|
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 is_pending = create_rw_signal(None);
|
||||||
let active_str = create_rw_signal("true".to_string());
|
let active_str = create_rw_signal("true".to_string());
|
||||||
let get_customer = create_blocking_resource(||(), move |_| get_remembered());
|
let get_customer = create_blocking_resource(||(), move |_| get_remembered());
|
||||||
|
let appearance = create_blocking_resource(||(), move |_| get_appearance());
|
||||||
let customer = create_rw_signal(Customer::default());
|
let customer = create_rw_signal(Customer::default());
|
||||||
|
|
||||||
create_effect(move |_| {
|
create_effect(move |_| {
|
||||||
@@ -117,6 +119,34 @@ pub fn Public() -> impl IntoView {
|
|||||||
price=price.write_only()
|
price=price.write_only()
|
||||||
slots=slots.write_only()
|
slots=slots.write_only()
|
||||||
captcha_state=is_pending.read_only()/>
|
captcha_state=is_pending.read_only()/>
|
||||||
|
|
||||||
|
<Transition fallback=move || view! {<p>{trl("Loading...")}</p> }>
|
||||||
|
{
|
||||||
|
appearance.get().map(|a| match a {
|
||||||
|
Ok(a) => {
|
||||||
|
let app = a.clone();
|
||||||
|
view! {
|
||||||
|
<div>
|
||||||
|
<Show when=move || a.clone().banner.is_some()>
|
||||||
|
<div class="header_banner">
|
||||||
|
<h1 class="header_banner">{app.title.clone().unwrap_or_default()}</h1>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
{app.text.unwrap_or("".to_string())}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {view! {<div><p>{trl("Error loading data")}</p>
|
||||||
|
<p>{e.to_string()}</p></div>
|
||||||
|
}}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Transition>
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<ActionForm
|
<ActionForm
|
||||||
on:submit=move |ev| {
|
on:submit=move |ev| {
|
||||||
|
|||||||
Reference in New Issue
Block a user