Banner style is no longer a generated file, public page gets appearance as CSS variables. Fixes #13
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -48,5 +48,5 @@ paths:
|
||||
- Custom styles go to a CSS file in `assets/css/` and must be linked with `<Link rel="stylesheet">` in `src/components/header.rs` (order matters: after the vendor files to override them). `style/main.scss` is not linked anywhere and has no effect. Never edit `assets/vendor/` (the one app-owned file there is `assets/vendor/css/control.css`).
|
||||
- The admin portal has a light and a dark theme (`src/components/theme.rs`, switch in the navbar: auto / light / dark, stored in the `theme` cookie). Dark colours are overrides in `assets/css/dark.css`, which `AdminPortal` links with a `media` attribute — the public page and the login page never load it. When you add or restyle something in the admin portal, check it in both themes and add the dark override there; don't hard-code light colours (`bg-light`, `bg-white`, inline `color`) in admin views.
|
||||
- Charts are drawn on the server, so the chart server fns take a `dark` flag from `Theme::is_dark()`.
|
||||
- The public page banner style template is `assets/banner.css`. On start it is copied to the data directory, and the appearance upload generates `banner-<random>.css` from it by replacing the `/*bg-img*/` marker — keep that marker intact.
|
||||
- Styles of the public page are in `assets/css/public.css`, scoped by its own classes (`.public-page` wraps the public route only, `.public-banner*`), so they don't leak into the admin portal. The customer's appearance settings reach it as CSS variables in a `style` attribute (`Appearance::banner_style()` in `src/backend/data.rs`) — never generate CSS files. Every value put into a `style` attribute must be validated first.
|
||||
- Stylesheets and meta tags are declared in `src/components/header.rs`.
|
||||
|
||||
@@ -33,7 +33,7 @@ Unit tests are `#[cfg(test)] mod tests` at the bottom of the file they test (`#[
|
||||
- First start creates the `admin` user with the password from `REZERVATOR_ADMIN_PASSWORD` or `[admin] initial_password` in the config (no default; the server refuses to start when no admin exists and no password is given). It also seeds a placeholder company, empty mail templates and an appearance row (`create_admin`, `check_company`, `check_messages`, `check_appearance` in `src/main.rs`).
|
||||
- With `transport = "File"` e-mails are written as `.eml` into `Maildir/` instead of being sent.
|
||||
- `assets/` is copied to `target/site` on build and served as static files.
|
||||
- Files created at runtime (uploaded banner, generated banner CSS) go to the data directory: `data/` in the working directory by default (gitignored), `[storage] data_dir` in the config, `/app/data` volume in Docker. It is served under `/data` and survives rebuilds. On start the server copies the `banner.css` template there from the site root and regenerates a missing banner style. Server code gets the path with `get_data_dir()`; never write into `target/site`.
|
||||
- Files created at runtime (the uploaded banner) go to the data directory: `data/` in the working directory by default (gitignored), `[storage] data_dir` in the config, `/app/data` volume in Docker. It is served under `/data` and survives rebuilds. Server code gets the path with `get_data_dir()`; never write into `target/site`.
|
||||
|
||||
## Layout
|
||||
|
||||
|
||||
Generated
-1
@@ -3357,7 +3357,6 @@ dependencies = [
|
||||
"lettre",
|
||||
"log",
|
||||
"pwhash",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"rust_decimal",
|
||||
"serde",
|
||||
|
||||
+1
-2
@@ -42,8 +42,7 @@ leptos-captcha = "0.4.0"
|
||||
charts-rs = { version = "0.3.5", optional = true}
|
||||
#image = { version = "0.24.8", optional = true }
|
||||
base64 = "0.22.0"
|
||||
rand = "0.8.5"
|
||||
# rand needs the "js" backend when compiled for the browser
|
||||
# The captcha's proof of work needs the "js" backend when compiled for the browser
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
|
||||
[features]
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ EXPOSE 3000
|
||||
RUN mkdir -p /app/config
|
||||
VOLUME /app/config
|
||||
|
||||
# Uploaded banner and generated styles. Keep this volume when the container is replaced.
|
||||
# Uploaded banner. Keep this volume when the container is replaced.
|
||||
RUN mkdir -p /app/data
|
||||
VOLUME /app/data
|
||||
|
||||
|
||||
@@ -44,9 +44,9 @@ Older versions created `admin` with the password `admin`. The server logs a warn
|
||||
|
||||
## Data directory
|
||||
|
||||
Files created while the application runs (the uploaded banner and the styles generated for it) are stored in the data directory and served under `/data`. By default it is `data` in the working directory; change it with `data_dir` in the `[storage]` section of the config. Keep this directory when you upgrade the application and include it in backups. Never put the config file into it.
|
||||
Files created while the application runs (the uploaded banner) are stored in the data directory and served under `/data`. By default it is `data` in the working directory; change it with `data_dir` in the `[storage]` section of the config. Keep this directory when you upgrade the application and include it in backups. Never put the config file into it.
|
||||
|
||||
When upgrading from a version that kept these files in `target/site/data`, move the banner image to the data directory. The style for it is generated again on start.
|
||||
When upgrading from a version that kept these files in `target/site/data`, move the banner image to the data directory.
|
||||
|
||||
## Running in Docker
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
div.header_banner {
|
||||
height:100px;
|
||||
clip-path: inset(0 0 0 0);
|
||||
/*bg-img*/
|
||||
background-size:cover;
|
||||
padding:30px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
div.header_banner_text {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
h1.header_banner {
|
||||
font-size: x-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;
|
||||
}
|
||||
|
||||
h1.header_banner {
|
||||
font-size: xxx-large;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Public booking page.
|
||||
*
|
||||
* Settings from the appearance table come as CSS variables in the style attribute
|
||||
* (Appearance::banner_style in src/backend/data.rs), so nothing here is generated.
|
||||
******************************************************************************/
|
||||
|
||||
.public-banner {
|
||||
height: 100px;
|
||||
clip-path: inset(0 0 0 0);
|
||||
background-image: var(--rz-banner-image, none);
|
||||
background-size: cover;
|
||||
padding: 30px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.public-banner-title {
|
||||
font-size: x-large;
|
||||
color: white;
|
||||
text-shadow: -3px -3px 0 #000, 3px -3px 0 #000, -3px 3px 0 #000, 3px 3px 0 #000;
|
||||
}
|
||||
|
||||
.public-banner-text {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.public-banner {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.public-banner-title {
|
||||
font-size: xxx-large;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -33,7 +33,7 @@ path = "Maildir"
|
||||
#[admin]
|
||||
#initial_password = ""
|
||||
|
||||
# Directory for uploaded files (banner) and generated styles, served under /data.
|
||||
# Directory for uploaded files (banner), served under /data.
|
||||
# Default is "data" in the working directory. Must survive application upgrades.
|
||||
#[storage]
|
||||
#data_dir = "data"
|
||||
|
||||
+1
-1
@@ -166,7 +166,7 @@ If a customer does not become healthy, the script puts back the database and dat
|
||||
./rezervator.sh restore alpha 20260101T020000Z
|
||||
```
|
||||
|
||||
A backup is two files in `backups/<name>/`: `<timestamp>-db.sql.gz` (database dump) and `<timestamp>-data.tar.gz` (uploaded banner and generated styles). `restore` replaces both the database and the data volume, and keeps the image the customer was running.
|
||||
A backup is two files in `backups/<name>/`: `<timestamp>-db.sql.gz` (database dump) and `<timestamp>-data.tar.gz` (uploaded banner). `restore` replaces both the database and the data volume, and keeps the image the customer was running.
|
||||
|
||||
The script does not schedule backups, does not delete old ones and does not copy them off the host. Run `backup --all` from cron, and ship `backups/`, `customers/` and `.env` somewhere else — `customers/` and `.env` hold the passwords and session keys and cannot be regenerated.
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Banner style is no longer a generated file, the public page gets its settings as CSS variables
|
||||
ALTER TABLE appearance DROP COLUMN css_name;
|
||||
ALTER TABLE appearance DROP COLUMN style_wide;
|
||||
ALTER TABLE appearance DROP COLUMN style_narrow;
|
||||
+2
-1
@@ -96,7 +96,8 @@ pub fn App() -> impl IntoView {
|
||||
<Router>
|
||||
<main>
|
||||
<Routes fallback=|| "Page not found.">
|
||||
<Route path=path!("") view=|| view! { <Public/> }/>
|
||||
// The class scopes the customer's appearance to the public page (assets/css/public.css)
|
||||
<Route path=path!("") view=|| view! { <div class="public-page"><Public/></div> }/>
|
||||
<Route path=path!("login") view=|| view! { <Login/> }/>
|
||||
<Route path=path!("admin") view=|| view! {
|
||||
<AdminPortal>
|
||||
|
||||
+35
-85
@@ -21,14 +21,8 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use crate::backend::get_pool;
|
||||
use actix_web::web::Data;
|
||||
use crate::backend::AppData;
|
||||
use regex::Regex;
|
||||
use rand::Rng;
|
||||
use rand::distributions::Alphanumeric;
|
||||
use log::{info, warn};
|
||||
|
||||
/// Style of the public page banner. The template is shipped with the application,
|
||||
/// the copy in the data directory is the base for generated styles.
|
||||
const BANNER_CSS: &str = "banner.css";
|
||||
const BANNER_EXTENSIONS: [&str; 5] = ["jpg", "jpeg", "png", "gif", "webp"];
|
||||
|
||||
/// Turns the file name sent by the browser into a safe name inside the data directory.
|
||||
@@ -49,19 +43,29 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
Some(format!("{}.{}", stem, extension.to_lowercase()))
|
||||
}
|
||||
|
||||
/// Creates the data directory and puts the banner style template into it, if it is not there yet
|
||||
pub fn ensure_data_dir(data_dir: &Path, site_root: &Path) -> Result<(), AppError> {
|
||||
/// Creates the data directory and removes the banner styles that older versions generated into it
|
||||
pub fn ensure_data_dir(data_dir: &Path) -> Result<(), AppError> {
|
||||
fs::create_dir_all(data_dir)?;
|
||||
|
||||
if !data_dir.join(BANNER_CSS).exists() {
|
||||
info!("Copying {} to data directory {}", BANNER_CSS, data_dir.display());
|
||||
fs::copy(site_root.join(BANNER_CSS), data_dir.join(BANNER_CSS))?;
|
||||
for entry in fs::read_dir(data_dir)? {
|
||||
let name = entry?.file_name().to_string_lossy().to_string();
|
||||
|
||||
if is_old_banner_style(&name) {
|
||||
info!("Removing unused banner style {}", name);
|
||||
// Not needed any more, so a failure is not a problem
|
||||
let _ = fs::remove_file(data_dir.join(&name));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn check_appearance(pool: &PgPool, data_dir: &Path) -> Result<(), AppError> {
|
||||
/// `banner.css` template and `banner-<random>.css` styles generated from it
|
||||
fn is_old_banner_style(name: &str) -> bool {
|
||||
name == "banner.css" || (name.starts_with("banner-") && name.ends_with(".css"))
|
||||
}
|
||||
|
||||
pub async fn check_appearance(pool: &PgPool) -> Result<(), AppError> {
|
||||
let count: (i64,) = query_as("SELECT COUNT(id) FROM appearance")
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
@@ -72,26 +76,6 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
.await?;
|
||||
}
|
||||
|
||||
let app = query_as::<_, Appearance>("SELECT * FROM appearance").fetch_one(pool).await?;
|
||||
if let None = app.css_name {
|
||||
query("UPDATE appearance SET css_name = $1").bind(BANNER_CSS).execute(pool).await?;
|
||||
}
|
||||
|
||||
// The generated style can be missing, e.g. when the data directory was not kept during an upgrade
|
||||
let css_name = app.css_name.unwrap_or(BANNER_CSS.to_string());
|
||||
if !data_dir.join(&css_name).exists() {
|
||||
match app.banner {
|
||||
Some(banner) if data_dir.join(&banner).exists() => {
|
||||
warn!("Banner style {} is missing, generating it again", css_name);
|
||||
modify_style(&banner, pool, data_dir).await?;
|
||||
}
|
||||
_ => {
|
||||
warn!("Banner style {} is missing, using default style", css_name);
|
||||
query("UPDATE appearance SET css_name = $1").bind(BANNER_CSS).execute(pool).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -104,43 +88,6 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn modify_style(file_name: &str, pool: &PgPool, data_dir: &Path) -> Result<(), AppError> {
|
||||
let mut css_str = fs::read_to_string(data_dir.join(BANNER_CSS))?;
|
||||
|
||||
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 old_css: (Option<String>,) = query_as("SELECT css_name FROM appearance")
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
if let Some(old_css) = old_css.0 {
|
||||
if old_css != BANNER_CSS {
|
||||
// The old style may be already gone
|
||||
let _ = fs::remove_file(data_dir.join(old_css));
|
||||
}
|
||||
}
|
||||
|
||||
let s: String = rand::thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(5)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
let css_name = format!("banner-{}.css", s);
|
||||
let mut css_file = File::create(data_dir.join(&css_name))?;
|
||||
css_file.write_all(css_str.as_bytes())?;
|
||||
query("UPDATE appearance SET css_name = $1")
|
||||
.bind(css_name)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn save_banner(field: &mut actix_multipart::Field, file_name: &str, app_data: &AppData) -> Result<(), AppError> {
|
||||
let data_dir = app_data.data_dir();
|
||||
let old_banner: (Option<String>,) = query_as("SELECT banner FROM appearance")
|
||||
@@ -154,7 +101,6 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
}
|
||||
|
||||
set_banner_name(file_name, app_data.db_pool()).await?;
|
||||
modify_style(file_name, app_data.db_pool(), data_dir).await?;
|
||||
|
||||
if let Some(old_banner) = old_banner.0 {
|
||||
if old_banner != file_name {
|
||||
@@ -296,31 +242,35 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_data_dir_copies_template_once() {
|
||||
fn ensure_data_dir_creates_directory() {
|
||||
let base = std::env::temp_dir().join(format!("rezervator-test-data-{}", std::process::id()));
|
||||
let site_root = base.join("site");
|
||||
let data_dir = base.join("data/nested");
|
||||
fs::create_dir_all(&site_root).unwrap();
|
||||
fs::write(site_root.join(BANNER_CSS), "template").unwrap();
|
||||
|
||||
ensure_data_dir(&data_dir, &site_root).unwrap();
|
||||
assert_eq!(fs::read_to_string(data_dir.join(BANNER_CSS)).unwrap(), "template");
|
||||
ensure_data_dir(&data_dir).unwrap();
|
||||
assert!(data_dir.is_dir());
|
||||
|
||||
// An existing file is not overwritten
|
||||
fs::write(data_dir.join(BANNER_CSS), "customized").unwrap();
|
||||
ensure_data_dir(&data_dir, &site_root).unwrap();
|
||||
assert_eq!(fs::read_to_string(data_dir.join(BANNER_CSS)).unwrap(), "customized");
|
||||
// An existing directory is fine
|
||||
ensure_data_dir(&data_dir).unwrap();
|
||||
|
||||
fs::remove_dir_all(base).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_data_dir_fails_without_template() {
|
||||
let base = std::env::temp_dir().join(format!("rezervator-test-notemplate-{}", std::process::id()));
|
||||
fs::create_dir_all(base.join("site")).unwrap();
|
||||
fn ensure_data_dir_removes_only_old_banner_styles() {
|
||||
let data_dir = std::env::temp_dir().join(format!("rezervator-test-styles-{}", std::process::id()));
|
||||
fs::create_dir_all(&data_dir).unwrap();
|
||||
for name in ["banner.css", "banner-DnyIx.css", "banner.jpg", "banner-1.png", "other.css"] {
|
||||
fs::write(data_dir.join(name), "x").unwrap();
|
||||
}
|
||||
|
||||
assert!(ensure_data_dir(&base.join("data"), &base.join("site")).is_err());
|
||||
ensure_data_dir(&data_dir).unwrap();
|
||||
|
||||
fs::remove_dir_all(base).unwrap();
|
||||
assert!(!data_dir.join("banner.css").exists());
|
||||
assert!(!data_dir.join("banner-DnyIx.css").exists());
|
||||
assert!(data_dir.join("banner.jpg").exists());
|
||||
assert!(data_dir.join("banner-1.png").exists());
|
||||
assert!(data_dir.join("other.css").exists());
|
||||
|
||||
fs::remove_dir_all(data_dir).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
+36
-2
@@ -741,14 +741,29 @@ pub struct Appearance {
|
||||
id: i32,
|
||||
pub banner: Option<String>,
|
||||
pub text: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub css_name: Option<String>
|
||||
pub title: Option<String>
|
||||
}
|
||||
|
||||
impl Appearance {
|
||||
pub fn id(&self) -> i32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
/// CSS variables for the banner of the public page, used as its `style` attribute.
|
||||
/// The styles themselves are in `assets/css/public.css`.
|
||||
pub fn banner_style(&self) -> String {
|
||||
match &self.banner {
|
||||
Some(banner) if is_css_safe_file_name(banner) => format!("--rz-banner-image: url('/data/{}')", banner),
|
||||
_ => String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The value is put into a `style` attribute, so it must not be able to end the `url()` or the declaration
|
||||
fn is_css_safe_file_name(name: &str) -> bool {
|
||||
!name.is_empty()
|
||||
&& !name.starts_with('.')
|
||||
&& name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
|
||||
}
|
||||
|
||||
pub fn validate_closing_time(time: &ClosingTime) -> Result<(), ValidationError> {
|
||||
@@ -1272,6 +1287,25 @@ mod tests {
|
||||
assert_eq!(SlotType::Day.to_string(), "Day");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn banner_style_sets_image_variable() {
|
||||
let appearance = Appearance { banner: Some("my-banner_1.jpg".to_string()), ..Default::default() };
|
||||
assert_eq!(appearance.banner_style(), "--rz-banner-image: url('/data/my-banner_1.jpg')");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn banner_style_is_empty_without_banner() {
|
||||
assert_eq!(Appearance::default().banner_style(), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn banner_style_ignores_unsafe_file_names() {
|
||||
for name in ["", "a'); color: red; x: url('.jpg", "a b.jpg", "../config.toml", "a/b.jpg", "a\\b.jpg", "a\"b.jpg", "a;b.jpg", "a)b.jpg", ".hidden"] {
|
||||
let appearance = Appearance { banner: Some(name.to_string()), ..Default::default() };
|
||||
assert_eq!(appearance.banner_style(), "", "name: {:?}", name);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enum_defaults() {
|
||||
assert_eq!(ReservationState::default(), ReservationState::New);
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::*;
|
||||
use crate::app::DialogHelper;
|
||||
use crate::backend::appearance::get_appearance;
|
||||
use crate::components::user_menu::MenuOpener;
|
||||
|
||||
#[component]
|
||||
pub fn Header() -> impl IntoView {
|
||||
let drawer = use_context::<MenuOpener>().expect("No drawer opener");
|
||||
let dlg_helper = use_context::<DialogHelper>().expect("No dialog helper");
|
||||
let appearance = Resource::new_blocking(||(), |_| get_appearance());
|
||||
|
||||
view! {
|
||||
<Html
|
||||
@@ -40,17 +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" />
|
||||
<Transition fallback=move || view! {""}>
|
||||
{move || {
|
||||
appearance.get().map(|a| {
|
||||
let css_name = match a {
|
||||
Ok(a) => a.css_name.unwrap_or_default(),
|
||||
Err(_) => "banner.css".to_string()
|
||||
};
|
||||
view! {<Link rel="stylesheet" href={format!("/data/{}", css_name)} />}
|
||||
})
|
||||
}}
|
||||
</Transition>
|
||||
<Link rel="stylesheet" href="/css/public.css" />
|
||||
<Link rel="stylesheet" href="/vendor/css/control.css" />
|
||||
|
||||
//<!-- Vendors CSS -->
|
||||
|
||||
+1
-1
@@ -133,7 +133,7 @@ impl Logging {
|
||||
}
|
||||
}
|
||||
|
||||
/// Where files created at runtime (uploaded banner, generated CSS) are kept
|
||||
/// Where files created at runtime (uploaded banner) are kept
|
||||
#[cfg(feature = "ssr")]
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct Storage {
|
||||
|
||||
+3
-3
@@ -63,9 +63,9 @@ async fn main() -> std::io::Result<()> {
|
||||
|
||||
let conf = get_configuration(None).unwrap();
|
||||
|
||||
// Files created at runtime (banner, generated styles) live outside the site root, so they survive upgrades
|
||||
// Files created at runtime (uploaded banner) live outside the site root, so they survive upgrades
|
||||
let data_dir = srv_conf.storage().data_dir();
|
||||
if let Err(e) = ensure_data_dir(&data_dir, Path::new(&*conf.leptos_options.site_root)) {
|
||||
if let Err(e) = ensure_data_dir(&data_dir) {
|
||||
error!("Cannot prepare data directory {}: {}", data_dir.display(), e);
|
||||
return Err(std::io::Error::other(e));
|
||||
}
|
||||
@@ -102,7 +102,7 @@ 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, &data_dir).await {
|
||||
if let Err(e) = check_appearance(&pool).await {
|
||||
error!("Error while checking messages: {:?}", e);
|
||||
}
|
||||
|
||||
|
||||
@@ -91,11 +91,12 @@ pub fn appearance() -> impl IntoView {
|
||||
app_edit.set(app.clone());
|
||||
let has_banner = app.banner.is_some();
|
||||
let title = app.title.clone().unwrap_or_default();
|
||||
let style = app.banner_style();
|
||||
view! {
|
||||
<div>
|
||||
<Show when=move || has_banner>
|
||||
<div class="header_banner">
|
||||
<h1 class="header_banner">{title.clone()}</h1>
|
||||
<div class="public-banner" style=style.clone()>
|
||||
<h1 class="public-banner-title">{title.clone()}</h1>
|
||||
</div>
|
||||
</Show>
|
||||
<div>
|
||||
|
||||
+4
-3
@@ -145,15 +145,16 @@ pub fn Public() -> impl IntoView {
|
||||
Ok(a) => {
|
||||
let has_banner = a.banner.is_some();
|
||||
let title = a.title.clone().unwrap_or_default();
|
||||
let style = a.banner_style();
|
||||
view! {
|
||||
<div>
|
||||
<Show when=move || has_banner>
|
||||
<div class="header_banner">
|
||||
<h1 class="header_banner">{title.clone()}</h1>
|
||||
<div class="public-banner" style=style.clone()>
|
||||
<h1 class="public-banner-title">{title.clone()}</h1>
|
||||
</div>
|
||||
</Show>
|
||||
<div>
|
||||
<div class="header_banner_text" inner_html={a.text.unwrap_or("".to_string())}>
|
||||
<div class="public-banner-text" inner_html={a.text.unwrap_or("".to_string())}>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user