Added layout options for the public booking form. Fixes #16

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 22:27:29 +02:00
co-authored by Claude Opus 5.5
parent 5fcea0cf85
commit 0f48160fb4
11 changed files with 293 additions and 24 deletions
+41 -12
View File
@@ -6,6 +6,7 @@ use crate::backend::appearance::{DeleteBanner, DeleteLogo, get_appearance, Updat
use crate::backend::banner::{image_extension, BannerOptions, ImageKind, IMAGE_ACCEPT, MAX_DIM, MAX_IMAGE_SIZE};
use crate::backend::color::{Colors, PALETTES};
use crate::backend::data::{banner_style, ApiResponse, Appearance};
use crate::backend::layout::LayoutOptions;
use crate::components::data_form::{DataForm, QuestionDialog};
use crate::components::modal_box::DialogOpener;
use crate::locales::trl;
@@ -15,6 +16,7 @@ use crate::locales::trl;
struct Look {
colors: RwSignal<Colors>,
banner: RwSignal<BannerOptions>,
layout: RwSignal<LayoutOptions>,
/// Images selected in the file inputs and not uploaded yet, as object URLs for the preview
new_banner: RwSignal<Option<String>>,
new_logo: RwSignal<Option<String>>,
@@ -276,7 +278,8 @@ fn preview(look: Look, appearance: ReadSignal<Appearance>) -> impl IntoView {
};
view! {
<div class="public-page public-preview" style=move || look.colors.get().style()>
<div class=move || format!("public-page public-preview {}", look.layout.get().classes())
style=move || look.colors.get().style()>
<Show when=has_banner>
<div class="public-banner" style=move || banner_style(image(), &look.banner.get()) on:click=set_focus>
{move || logo().map(|url| view! {
@@ -292,8 +295,8 @@ fn preview(look: Look, appearance: ReadSignal<Appearance>) -> impl IntoView {
</div>
</Show>
<div class="public-banner-text" inner_html=move || appearance.get().text.unwrap_or_default()></div>
<div class="card-body">
<div class="row">
<div class="card-body public-form">
<div class="row public-form-columns">
<div class="col-sm">
<div class="card mb-3">
<div class="card-body">
@@ -423,12 +426,29 @@ fn banner_settings(
}
}
/// Saves colours and banner options together, they make one look
#[component]
fn save_bar(look: Look, saved: RwSignal<(Colors, BannerOptions)>) -> impl IntoView {
fn layout_settings(layout: RwSignal<LayoutOptions>) -> impl IntoView {
view! {
<Choice label="Content width" options=vec![("full", "Full width"), ("centered", "Centred")]
value=move || layout.get().width on_change=move |v| layout.update(|l| l.width = v)/>
<Choice label="Form" options=vec![("columns", "Side by side"), ("stacked", "Stacked")]
value=move || layout.get().arrangement on_change=move |v| layout.update(|l| l.arrangement = v)/>
<Choice label="Time slots" options=vec![("normal", "Normal"), ("large", "Large")]
value=move || layout.get().slots on_change=move |v| layout.update(|l| l.slots = v)/>
<Choice label="Corners" options=vec![("rounded", "Rounded"), ("square", "Square")]
value=move || layout.get().corners on_change=move |v| layout.update(|l| l.corners = v)/>
}
}
/// Everything that is saved together as one look
type SavedLook = (Colors, BannerOptions, LayoutOptions);
/// Saves colours, banner options and layout together, they make one look
#[component]
fn save_bar(look: Look, saved: RwSignal<SavedLook>) -> impl IntoView {
let save = ServerAction::<UpdateStyle>::new();
let sent = StoredValue::new((Colors::default(), BannerOptions::default()));
let current = move || (look.colors.get(), look.banner.get());
let sent = StoredValue::new(SavedLook::default());
let current = move || (look.colors.get(), look.banner.get(), look.layout.get());
let dirty = move || current() != saved.get();
Effect::new(move |_| {
@@ -440,15 +460,16 @@ fn save_bar(look: Look, saved: RwSignal<(Colors, BannerOptions)>) -> impl IntoVi
view! {
<div>
<button type="button" class="btn btn-primary me-2" on:click=move |_| {
let (colors, banner) = current();
sent.set_value((colors.clone(), banner.clone()));
save.dispatch(UpdateStyle { colors, banner });
let (colors, banner, layout) = current();
sent.set_value((colors.clone(), banner.clone(), layout.clone()));
save.dispatch(UpdateStyle { colors, banner, layout });
}>
{trl("Save")}
</button>
<button type="button" class="btn btn-outline-secondary" on:click=move |_| {
look.colors.set(Colors::default());
look.banner.set(BannerOptions::default());
look.layout.set(LayoutOptions::default());
}>
{trl("Reset to default")}
</button>
@@ -493,10 +514,11 @@ pub fn appearance() -> impl IntoView {
let look = Look {
colors: RwSignal::new(Colors::default()),
banner: RwSignal::new(BannerOptions::default()),
layout: RwSignal::new(LayoutOptions::default()),
new_banner: RwSignal::new(None),
new_logo: RwSignal::new(None),
};
let saved = RwSignal::new((Colors::default(), BannerOptions::default()));
let saved = RwSignal::new(SavedLook::default());
let app_preview = RwSignal::new(Appearance::default());
let look_loaded = StoredValue::new(false);
@@ -507,7 +529,8 @@ pub fn appearance() -> impl IntoView {
if !look_loaded.get_value() {
look.colors.set(app.colors.clone());
look.banner.set(app.banner_options.clone());
saved.set((app.colors.clone(), app.banner_options.clone()));
look.layout.set(app.layout.clone());
saved.set((app.colors.clone(), app.banner_options.clone(), app.layout.clone()));
look_loaded.set_value(true);
}
app_preview.set(app);
@@ -576,6 +599,12 @@ pub fn appearance() -> impl IntoView {
delete_banner=del_banner delete_logo=del_logo/>
</div>
</div>
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">{trl("Layout")}</h5>
<LayoutSettings layout=look.layout/>
</div>
</div>
<div class="card mb-3">
<div class="card-body">
<SaveBar look=look saved=saved/>
+5 -5
View File
@@ -120,10 +120,10 @@ pub fn PublicPage() -> impl IntoView {
<Transition fallback=move || view! {<p>{trl("Loading...")}</p> }>
{move || {
appearance.get().map(|a| {
// Default colours when the settings can't be loaded, the form reports the error itself
let style = a.map(|a| a.colors).unwrap_or_default().style();
// Default look when the settings can't be loaded, the form reports the error itself
let a = a.unwrap_or_default();
view! {
<div class="public-page" style=style>
<div class=format!("public-page {}", a.layout.classes()) style=a.colors.style()>
<Public/>
</div>
}
@@ -193,7 +193,7 @@ pub fn Public() -> impl IntoView {
}}
</Transition>
<div class="card-body">
<div class="card-body public-form">
<ActionForm
// Capture phase, so the form is validated and the captcha solved before it is dispatched
on:submit:capture=move |ev: SubmitEvent| {
@@ -213,7 +213,7 @@ pub fn Public() -> impl IntoView {
}
}
action=cr_reservation>
<div class="row mb-5">
<div class="row mb-5 public-form-columns">
<div class="col-md">
<div class="card mb-3">
<div class="card-body">