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