Implemented public form appearance settings.
This commit is contained in:
Generated
+74
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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::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 {
|
||||
<Customers/>
|
||||
</AdminPortal>
|
||||
}/>
|
||||
<Route path="admin/appearance" view=|| view! {
|
||||
<AdminPortal>
|
||||
<Appearance/>
|
||||
</AdminPortal>
|
||||
}/>
|
||||
</Routes>
|
||||
</main>
|
||||
</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 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 customer;
|
||||
pub mod mail;
|
||||
pub mod appearance;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! perm_check {
|
||||
|
||||
@@ -51,6 +51,12 @@ fn settings_menu(opener: MenuOpener) -> impl IntoView {
|
||||
<span class="align-middle">{trl("Mail settings")}</span>
|
||||
</a>
|
||||
</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>
|
||||
}
|
||||
}
|
||||
@@ -133,11 +139,12 @@ pub fn AdminPortal(children: Children) -> impl IntoView {
|
||||
//<!-- /Settings -->
|
||||
|
||||
<ul class="navbar-nav flex-row align-items-center ms-auto">
|
||||
<SettingsMenu opener=settings_menu/>
|
||||
|
||||
<li class="nav-item navbar-dropdown dropdown-user dropdown">
|
||||
<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>
|
||||
</a>
|
||||
<SettingsMenu opener=settings_menu/>
|
||||
</li>
|
||||
//<!-- User -->
|
||||
<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/theme-default.css" />
|
||||
<Link rel="stylesheet" href="/css/demo.css" />
|
||||
<Link rel="stylesheet" href="/banner.css" />
|
||||
|
||||
//<!-- Vendors CSS -->
|
||||
<Link rel="stylesheet" href="/vendor/libs/perfect-scrollbar/perfect-scrollbar.css" />
|
||||
|
||||
+11
-2
@@ -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<lettre::transport::file::Error> for AppError {
|
||||
fn from(value: lettre::transport::file::Error) -> Self {
|
||||
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 "),
|
||||
("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"),
|
||||
|
||||
@@ -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! { <App/> },
|
||||
)
|
||||
.service(appearance::upload)
|
||||
.service(Files::new("/", site_root))
|
||||
//.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;
|
||||
pub mod all_reservations;
|
||||
pub mod customers;
|
||||
pub mod appearance_settings;
|
||||
|
||||
|
||||
@@ -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()/>
|
||||
|
||||
<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">
|
||||
<ActionForm
|
||||
on:submit=move |ev| {
|
||||
|
||||
Reference in New Issue
Block a user