Implemented public form appearance settings.
parent
653249287d
commit
2119c2e56b
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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(()))
|
||||||
|
}
|
@ -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>
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue