Upgraded Leptos to 0.8. Fixes #1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# spow (proof of work for leptos-captcha) needs the getrandom backend selected for the browser
|
||||
[target.wasm32-unknown-unknown]
|
||||
rustflags = ['--cfg', 'getrandom_backend="wasm_js"']
|
||||
@@ -11,7 +11,8 @@ paths:
|
||||
## Server functions
|
||||
|
||||
- One file per domain in `src/backend/`. Server-only helpers and their imports go in the `cfg_if! { if #[cfg(feature = "ssr")] { ... }}` block at the top; `#[server]` functions follow below it. Extra server-only imports inside a server fn body are written as local `use` statements.
|
||||
- Use plain `#[server]` for new functions (older ones spell out `#[server(Name, "/api", "Url", "name")]`; both end up under `/api`).
|
||||
- Use plain `#[server]` for new functions (a few older ones use `#[server(name = Name, prefix = "/api", endpoint = "name")]`; both end up under `/api`). The positional form with `"Url"` no longer compiles. Server functions are registered by `leptos_routes` in `src/main.rs`; there is no separate `/api` route.
|
||||
- The current request is `use_context::<leptos_actix::Request>()` (it derefs to `HttpRequest`); extractors go through `leptos_actix::extract().await?`.
|
||||
- Return `Result<ApiResponse<T>, ServerFnError>`. Expected/business failures are `Ok(ApiResponse::Error("English message".to_string()))` — the client passes that string through `trl()`, so add it to the catalogue. `Err(ServerFnError)` is for real failures (DB down etc.).
|
||||
- A function used with `<DataForm>` / `<QuestionDialog>` must return `ApiResponse<()>` and take the form entity as a single struct argument; its name is the prefix of the form field names (`company: Company` ↔ `name="company[name]"`).
|
||||
- For `<DataForm>`, implement `ForValidation` for the generated struct (PascalCase of the fn name) right below the function:
|
||||
|
||||
@@ -12,15 +12,25 @@ paths:
|
||||
|
||||
## Components
|
||||
|
||||
- Leptos 0.6 API only. Components are `#[component]` functions returning `impl IntoView`, one page or dialog per file in `src/pages/`; reusable building blocks go to `src/components/`. Register new files in the respective `mod.rs` (`pub mod` only if used from `app.rs`).
|
||||
- Leptos 0.8 API only (`use leptos::prelude::*`). Components are `#[component]` functions returning `impl IntoView`, one page or dialog per file in `src/pages/`; reusable building blocks go to `src/components/`. Register new files in the respective `mod.rs` (`pub mod` only if used from `app.rs`).
|
||||
- This code runs on the server (SSR) and in the browser (hydration). Browser-only APIs (`window()`, `web_sys`) must not run during SSR — keep them in event handlers/effects or behind `#[cfg(not(feature = "ssr"))]`. Markup must render the same on both sides or hydration breaks.
|
||||
- A new admin page needs a `<Route path="admin/..."/>` in `src/app.rs` wrapped in `<AdminPortal>`, and a menu entry in `src/components/admin_portal.rs`. Paths starting with `/admin` are login-protected by the middleware; data access is still guarded by `perm_check!` in the server fn.
|
||||
|
||||
## Leptos 0.8 pitfalls
|
||||
|
||||
- Views are statically typed: `if`/`else` and `match` arms that return different markup must each end with `.into_any()`.
|
||||
- Context is not available in event handlers (`on:click` etc.). Call `use_context` in the component body and keep the result — `DialogOpener` and `MenuOpener` do this in `new()`, so create them in a component body, not inside a handler.
|
||||
- Markup must be valid HTML, or hydration panics in the browser and the whole page stops reacting: no block elements (`<div>`, `<table>`, another `<p>`) inside `<p>`, and `<tr>` only inside `<tbody>`/`<thead>`. The compiler doesn't catch this — load the page with a full reload and check the browser console.
|
||||
- HTML attributes are typed. Deprecated ones (`align`, `width` on `<table>`) don't compile; use `style`.
|
||||
- A value used twice in one `view!` (e.g. in an attribute and as a child) must be cloned for the first use.
|
||||
- Client-side form validation hooks into `<ActionForm>` with `on:submit:capture` and `ev.prevent_default()`; a plain `on:submit` runs too late. `DataForm` already does this.
|
||||
- `action.value()` is not an `RwSignal`; pass it on as `Signal::derive(move || action.value().get())`.
|
||||
|
||||
## Data loading and forms
|
||||
|
||||
- Load data with `create_blocking_resource` / `create_resource` calling the server fn, rendered inside `<Transition fallback=...>` with `{trl("Loading...")}`. Use a dialog's `opener.visible()` as the resource source so lists reload after the dialog closes (see `src/pages/users.rs`).
|
||||
- Load data with `Resource::new_blocking` / `Resource::new` calling the server fn, rendered inside `<Transition fallback=...>` with `{trl("Loading...")}`. Read the resource in a closure — `{move || res.get().map(...)}` — a plain `{res.get()...}` block is evaluated once and never updates. Use a dialog's `opener.visible()` as the resource source so lists reload after the dialog closes (see `src/pages/users.rs`).
|
||||
- Handle all three outcomes of a server call: `Err(e)`, `Ok(ApiResponse::Error(msg))`, `Ok(ApiResponse::Data(d))`.
|
||||
- Edit/create dialogs: `DialogOpener::new()` in the parent, `create_server_action::<ServerFnStruct>()` in the dialog, wrapped in `<DataForm opener=.. action=.. title="..">`. It supplies the modal, Save/Close buttons, client validation and server error display. Confirmations use `<QuestionDialog>`.
|
||||
- Edit/create dialogs: `DialogOpener::new()` in the parent, `ServerAction::<ServerFnStruct>::new()` in the dialog, wrapped in `<DataForm opener=.. action=.. title="..">`. It supplies the modal, Save/Close buttons, client validation and server error display. Confirmations use `<QuestionDialog>`.
|
||||
- Form field names follow the server fn argument: `name="company[name]"`. Pass the id as a hidden input. Fill inputs with `prop:value={move || signal.get().field}`.
|
||||
- `DataForm`/`ModalDialog` take `title` as an untranslated English key (`&'static str`); they translate it themselves.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Rezervovator
|
||||
|
||||
Booking system for sport/service facilities. Full-stack Rust: Leptos 0.6 (SSR + hydration) on Actix Web, PostgreSQL via sqlx. Crate name is `rezervator`, product name is "Rezervovator".
|
||||
Booking system for sport/service facilities. Full-stack Rust: Leptos 0.8 (SSR + hydration) on Actix Web, PostgreSQL via sqlx. Crate name is `rezervator`, product name is "Rezervovator".
|
||||
|
||||
One crate, compiled twice by cargo-leptos:
|
||||
|
||||
@@ -19,7 +19,7 @@ cargo check --lib --features hydrate --target wasm32-unknown-unknown # type-ch
|
||||
cargo test --features ssr # unit tests
|
||||
```
|
||||
|
||||
Run both `cargo check` commands and the tests after any change; a plain `cargo check` / `cargo test` (no features) checks neither side properly.
|
||||
Run both `cargo check` commands and the tests after any change. They don't catch hydration errors, so after changing a view also load the page in a browser with a full reload (the browser caches the old WASM otherwise) and check the console; a plain `cargo check` / `cargo test` (no features) checks neither side properly.
|
||||
|
||||
Unit tests are `#[cfg(test)] mod tests` at the bottom of the file they test (`#[cfg(all(test, feature = "ssr"))]` for ssr-only code). They cover pure logic only — parsing, validation, paging, slot merging, mail templates, config. Nothing touches the database, so server functions, permissions and SQL are untested. Add tests for new pure logic; keep them free of DB and network.
|
||||
|
||||
@@ -54,8 +54,10 @@ Routes: `/` public booking form, `/login`, `/admin`, `/admin/settings`, `/admin/
|
||||
- Code, comments, identifiers, log messages and commit messages are in English. UI strings are English keys translated at runtime (see `.claude/rules/frontend.md`).
|
||||
- Commit messages are always written in English, even when the conversation is in Czech: short past-tense sentences ending with a period, e.g. "Added setting for closing days."
|
||||
- The project has no rustfmt config and the code is not rustfmt-formatted. Don't run `cargo fmt` over existing files; match the surrounding style and keep diffs limited to what you changed.
|
||||
- Leptos is 0.6 — use the 0.6 API (`create_signal`, `create_resource`, `create_server_action`, `#[component]` returning `impl IntoView`), not 0.7/0.8 (`signal()`, `Resource::new`, …).
|
||||
- `wasm-bindgen` is pinned to `=0.2.100`; it must match the wasm-bindgen version of the installed cargo-leptos. Don't bump it on its own.
|
||||
- Leptos is 0.8 — use the 0.8 API (`use leptos::prelude::*`, `signal()`, `RwSignal::new`, `Resource::new_blocking`, `ServerAction::<T>::new()`), not the 0.6 `create_*` functions.
|
||||
- `wasm-bindgen` is pinned to an exact version in `Cargo.toml`. The `wasm-bindgen` CLI that cargo-leptos runs must be exactly the same version (`cargo install -f wasm-bindgen-cli --version <same>`), otherwise the front-end build fails with a schema version mismatch. Leptos 0.8 needs at least 0.2.108. Change the pin and the CLI together, never one of them.
|
||||
- `#![recursion_limit = "512"]` at the top of `src/lib.rs` and `src/main.rs` is required: without it the release build fails with "queries overflow the depth limit".
|
||||
- `.cargo/config.toml` selects the `getrandom` backend for the WASM target (needed by the captcha's proof of work). `getrandom` and `uuid` have their `js` features enabled in `Cargo.toml` for the same reason; don't remove them.
|
||||
- Version lives in `Cargo.toml` and is shown in the page footer.
|
||||
|
||||
Detailed rules are in `.claude/rules/`: `backend.md`, `frontend.md`, `database.md`.
|
||||
|
||||
Generated
+914
-683
File diff suppressed because it is too large
Load Diff
+15
-11
@@ -13,20 +13,20 @@ actix-session = { version = "0.10.0", optional = true, features = ["cookie-sessi
|
||||
actix-multipart = { version = "0.7.2", optional = true }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
cfg-if = "1"
|
||||
leptos = { version = "0.6.13" }
|
||||
leptos_meta = { version = "0.6.13" }
|
||||
leptos_actix = { version = "0.6.13", optional = true }
|
||||
server_fn = { version = "0.6.13", features = ["multipart"] }
|
||||
leptos_router = { version = "0.6.13" }
|
||||
leptos = { version = "0.8.22" }
|
||||
leptos_meta = { version = "0.8" }
|
||||
leptos_actix = { version = "0.8", optional = true }
|
||||
server_fn = { version = "0.8" }
|
||||
leptos_router = { version = "0.8" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1.0.117"
|
||||
wasm-bindgen = "=0.2.100"
|
||||
wasm-bindgen = "=0.2.129"
|
||||
web-sys = { version = "0.3.69", features = ["Navigator"] }
|
||||
lazy_static = "1.4.0"
|
||||
chrono = { version = "0.4.38", features = ["serde"]}
|
||||
sqlx = { version = "0.8.0", optional = true, features = ["runtime-tokio-rustls", "postgres", "chrono", "rust_decimal", "uuid"] }
|
||||
rust_decimal = "1.35.0"
|
||||
uuid = {version = "1.8.0", features = ["v4", "serde"]}
|
||||
uuid = {version = "1.8.0", features = ["v4", "serde", "js"]}
|
||||
validator = {version = "0.16.1", features = ["derive"]}
|
||||
pwhash = "1.0.0"
|
||||
futures-util = "0.3.30"
|
||||
@@ -35,17 +35,19 @@ toml = "0.8.12"
|
||||
log = "0.4.21"
|
||||
env_logger = "0.11.3"
|
||||
getopts = "0.2.21"
|
||||
leptos-use = "0.13.13"
|
||||
leptos-use = "0.19.2"
|
||||
lettre = {version = "0.11.6", features = ["tokio1-native-tls", "smtp-transport", "file-transport"], optional = true}
|
||||
leptos-captcha = "0.2.0"
|
||||
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
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
|
||||
[features]
|
||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||
csr = ["leptos/csr"]
|
||||
hydrate = ["leptos/hydrate"]
|
||||
ssr = [
|
||||
"dep:actix-files",
|
||||
"dep:actix-web",
|
||||
@@ -59,6 +61,8 @@ ssr = [
|
||||
"leptos/ssr",
|
||||
"leptos_meta/ssr",
|
||||
"leptos_router/ssr",
|
||||
"leptos-use/ssr",
|
||||
"leptos-use/actix",
|
||||
]
|
||||
|
||||
[profile.wasm-release]
|
||||
|
||||
@@ -13,6 +13,10 @@ You will need cargo-leptos tool:
|
||||
|
||||
`cargo install cargo-leptos`
|
||||
|
||||
The `wasm-bindgen` command line tool must be exactly the same version as the `wasm-bindgen` dependency pinned in `Cargo.toml`:
|
||||
|
||||
`cargo install -f wasm-bindgen-cli --version 0.2.129`
|
||||
|
||||
## Running project
|
||||
|
||||
`cargo leptos watch`
|
||||
|
||||
+23
-19
@@ -1,9 +1,11 @@
|
||||
use crate::locales::init_locales;
|
||||
use crate::pages::home_page::HomePage;
|
||||
use crate::pages::settings::Settings;
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use leptos_router::components::{Route, Router, Routes};
|
||||
use leptos_router::hooks::use_location;
|
||||
use leptos_router::path;
|
||||
use crate::components::admin_portal::AdminPortal;
|
||||
use crate::components::header::Header;
|
||||
use crate::components::user_menu::MenuOpener;
|
||||
@@ -14,29 +16,31 @@ use crate::pages::login::Login;
|
||||
use crate::pages::mail_settings::MailSettings;
|
||||
use crate::pages::public::Public;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct MenuHelper {
|
||||
opened: RwSignal<MenuOpener>
|
||||
opened: RwSignal<Option<MenuOpener>>
|
||||
}
|
||||
|
||||
impl MenuHelper {
|
||||
pub fn new() -> Self {
|
||||
let opened = create_rw_signal(MenuOpener::new());
|
||||
let opened = RwSignal::new(None);
|
||||
Self {
|
||||
opened
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_opened(&self, opened: MenuOpener) {
|
||||
self.opened.set(opened);
|
||||
self.opened.set(Some(opened));
|
||||
}
|
||||
|
||||
pub fn close(&self) {
|
||||
self.opened.get().close();
|
||||
if let Some(opened) = self.opened.get_untracked() {
|
||||
opened.close();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&self) {
|
||||
self.opened.set(MenuOpener::new());
|
||||
self.opened.set(None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +51,7 @@ pub struct DialogHelper {
|
||||
|
||||
impl DialogHelper {
|
||||
pub fn new() -> Self {
|
||||
let opened = create_rw_signal(false);
|
||||
let opened = RwSignal::new(false);
|
||||
Self {
|
||||
opened
|
||||
}
|
||||
@@ -68,7 +72,7 @@ fn app_footer() -> impl IntoView {
|
||||
|
||||
view! {
|
||||
<footer class="content-footer footer bg-footer-theme" style={move || if loc.pathname.get().starts_with("/admin") {"display: none;"} else {"display: block;"}}>
|
||||
<div class="mb-2 mb-md-0" align="center">
|
||||
<div class="mb-2 mb-md-0" style="text-align: center">
|
||||
<a href="https://rezervovator.cz" target="_blank"><img src="/rezervovator_l.svg" width="110"/></a> {format!(" v {}", env!("CARGO_PKG_VERSION"))}
|
||||
</div>
|
||||
</footer>
|
||||
@@ -89,35 +93,35 @@ pub fn App() -> impl IntoView {
|
||||
|
||||
<Router>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route path="" view=|| view! { <Public/> }/>
|
||||
<Route path="login" view=|| view! { <Login/> }/>
|
||||
<Route path="admin" view=|| view! {
|
||||
<Routes fallback=|| "Page not found.">
|
||||
<Route path=path!("") view=|| view! { <Public/> }/>
|
||||
<Route path=path!("login") view=|| view! { <Login/> }/>
|
||||
<Route path=path!("admin") view=|| view! {
|
||||
<AdminPortal>
|
||||
<HomePage/>
|
||||
</AdminPortal>
|
||||
}/>
|
||||
<Route path="admin/settings" view=|| view! {
|
||||
<Route path=path!("admin/settings") view=|| view! {
|
||||
<AdminPortal>
|
||||
<Settings/>
|
||||
</AdminPortal>
|
||||
}/>
|
||||
<Route path="admin/mail_settings" view=|| view! {
|
||||
<Route path=path!("admin/mail_settings") view=|| view! {
|
||||
<AdminPortal>
|
||||
<MailSettings/>
|
||||
</AdminPortal>
|
||||
}/>
|
||||
<Route path="admin/bookings" view=|| view! {
|
||||
<Route path=path!("admin/bookings") view=|| view! {
|
||||
<AdminPortal>
|
||||
<Bookings/>
|
||||
</AdminPortal>
|
||||
}/>
|
||||
<Route path="admin/customers" view=|| view! {
|
||||
<Route path=path!("admin/customers") view=|| view! {
|
||||
<AdminPortal>
|
||||
<Customers/>
|
||||
</AdminPortal>
|
||||
}/>
|
||||
<Route path="admin/appearance" view=|| view! {
|
||||
<Route path=path!("admin/appearance") view=|| view! {
|
||||
<AdminPortal>
|
||||
<Appearance/>
|
||||
</AdminPortal>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use validator::Validate;
|
||||
use crate::backend::data::{ApiResponse, Appearance};
|
||||
use crate::components::data_form::ForValidation;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use validator::Validate;
|
||||
use crate::backend::data::{ApiResponse, Company};
|
||||
use crate::components::data_form::ForValidation;
|
||||
@@ -25,7 +25,7 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
}
|
||||
}}
|
||||
|
||||
#[server(GetCompany, "/api", "Url", "get_company")]
|
||||
#[server(name = GetCompany, prefix = "/api", endpoint = "get_company")]
|
||||
pub async fn get_company() -> Result<ApiResponse<Company>, ServerFnError> {
|
||||
use crate::perm_check;
|
||||
|
||||
@@ -40,7 +40,7 @@ pub async fn get_company() -> Result<ApiResponse<Company>, ServerFnError> {
|
||||
Ok(ApiResponse::Data(cmp))
|
||||
}
|
||||
|
||||
#[server(UpdateCompany, "/api", "Url", "update_company")]
|
||||
#[server(name = UpdateCompany, prefix = "/api", endpoint = "update_company")]
|
||||
pub async fn update_company(company: Company) -> Result<ApiResponse<()>, ServerFnError> {
|
||||
use crate::perm_check;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::{server, ServerFnError};
|
||||
use leptos::prelude::*;
|
||||
use crate::backend::data::ApiResponse;
|
||||
use crate::backend::data::Customer;
|
||||
|
||||
@@ -8,7 +8,6 @@ cfg_if! { if #[cfg(feature = "ssr")] {
|
||||
use sqlx::{query_as, query};
|
||||
use sqlx::Error;
|
||||
use std::ops::DerefMut;
|
||||
use leptos::expect_context;
|
||||
use leptos_actix::extract;
|
||||
use actix_web::cookie::Cookie;
|
||||
use actix_web::cookie::SameSite;
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use validator::Validate;
|
||||
use crate::backend::data::{ApiResponse, Message, MessageType};
|
||||
use crate::components::data_form::ForValidation;
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ cfg_if!{
|
||||
use sqlx::PgPool;
|
||||
use actix_web::web::Data;
|
||||
use leptos_actix::extract;
|
||||
use leptos::ServerFnError;
|
||||
use leptos::prelude::ServerFnError;
|
||||
use crate::backend::mail::Mailer;
|
||||
|
||||
#[derive(Clone)]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use cfg_if::cfg_if;
|
||||
use chrono::Weekday;
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use validator::Validate;
|
||||
use crate::backend::data::{ApiResponse, ClosingTime, DayHour, WeekHours};
|
||||
use crate::components::data_form::ForValidation;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use validator::Validate;
|
||||
use crate::backend::data::{ApiResponse, ResProperty};
|
||||
use crate::components::data_form::ForValidation;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use validator::Validate;
|
||||
use crate::backend::data::{ApiResponse, CrReservation, Reservation, PublicFormData, ResSumWithItems, Page, PagedResponse};
|
||||
use crate::components::data_form::ForValidation;
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
use cfg_if::cfg_if;
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use validator::Validate;
|
||||
use crate::backend::data::{ApiResponse, PwdChange, User, UserProfile};
|
||||
use crate::components::data_form::ForValidation;
|
||||
@@ -157,7 +157,7 @@ pub async fn get_user() -> Result<Option<User>, ServerFnError> {
|
||||
Ok(logged_in_user().await)
|
||||
}
|
||||
|
||||
#[server(GetUsers, "/api", "Url", "get_users")]
|
||||
#[server(name = GetUsers, prefix = "/api", endpoint = "get_users")]
|
||||
pub async fn get_users() -> Result<ApiResponse<Vec<User>>, ServerFnError> {
|
||||
use crate::perm_check;
|
||||
use crate::backend::get_pool;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use crate::backend::data::User;
|
||||
use crate::components::modal_box::{DialogOpener, ModalBody, ModalDialog};
|
||||
use crate::components::user_menu::{MenuOpener, UserMenu};
|
||||
@@ -12,7 +12,7 @@ fn about(opener: DialogOpener) -> impl IntoView {
|
||||
<ModalDialog opener=opener title="">
|
||||
<ModalBody>
|
||||
<img src="/rezervovator_l.svg" width="180"/> <br /><br/>
|
||||
<p>
|
||||
<div>
|
||||
{trl("Online booking application for sports facilities and service providers.")}<br/>
|
||||
{format!(" v {}", env!("CARGO_PKG_VERSION"))}<br/><br/>
|
||||
<div align="center">
|
||||
@@ -20,7 +20,7 @@ fn about(opener: DialogOpener) -> impl IntoView {
|
||||
<a href="https://leptos.dev" target="_blank"><img src="/Leptos_logo.png" height="40"/></a> <br/><br/>
|
||||
"(c) 2023 - 2025"
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</ModalDialog>
|
||||
}
|
||||
@@ -66,7 +66,7 @@ fn settings_menu(opener: MenuOpener) -> impl IntoView {
|
||||
pub fn AdminPortal(children: Children) -> impl IntoView {
|
||||
let user_menu = MenuOpener::new();
|
||||
let settings_menu = MenuOpener::new();
|
||||
let (user, set_user) = create_signal(User::default());
|
||||
let (user, set_user) = signal(User::default());
|
||||
let editor = DialogOpener::new();
|
||||
let pw_changer = DialogOpener::new();
|
||||
let drawer = use_context::<MenuOpener>().expect("No drawer opener");
|
||||
|
||||
+33
-31
@@ -1,11 +1,12 @@
|
||||
use crate::backend::data::ApiResponse;
|
||||
use crate::components::modal_box::{DialogOpener, ModalBody, ModalDialog, ModalFooter};
|
||||
use leptos::*;
|
||||
use leptos::server_fn::client::browser::BrowserClient;
|
||||
use leptos::server_fn::codec::{Json, PostUrl};
|
||||
use leptos::server_fn::error::NoCustomError;
|
||||
use leptos::server_fn::ServerFn;
|
||||
use leptos_router::*;
|
||||
use leptos::prelude::*;
|
||||
use leptos::form::{ActionForm, FromFormData};
|
||||
use leptos::ev::SubmitEvent;
|
||||
use leptos::server_fn::client::Client;
|
||||
use leptos::server_fn::codec::PostUrl;
|
||||
use leptos::server_fn::request::ClientReq;
|
||||
use leptos::server_fn::{Http, ServerFn};
|
||||
use serde::de::DeserializeOwned;
|
||||
use validator::Validate;
|
||||
use crate::components::server_err::ServerErr;
|
||||
@@ -18,27 +19,28 @@ pub trait ForValidation {
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn data_form<T: 'static + Clone +
|
||||
ServerFn<Client = BrowserClient,
|
||||
InputEncoding = PostUrl,
|
||||
OutputEncoding = Json,
|
||||
Error = NoCustomError,
|
||||
Output = ApiResponse<()>> +
|
||||
DeserializeOwned + ForValidation>(
|
||||
pub fn data_form<T, O>(
|
||||
opener: DialogOpener,
|
||||
action: Action<T, Result<ApiResponse<()>, ServerFnError>>,
|
||||
action: ServerAction<T>,
|
||||
title: &'static str,
|
||||
children: Children
|
||||
) -> impl IntoView {
|
||||
let upd_val = action.value();
|
||||
) -> impl IntoView
|
||||
where
|
||||
T: ServerFn<Protocol = Http<PostUrl, O>, Output = ApiResponse<()>, Error = ServerFnError>
|
||||
+ DeserializeOwned + ForValidation + Clone + Send + Sync + 'static,
|
||||
T::Client: Client<ServerFnError>,
|
||||
<<T::Client as Client<ServerFnError>>::Request as ClientReq<ServerFnError>>::FormData: From<web_sys::FormData>,
|
||||
O: 'static
|
||||
{
|
||||
let upd_val = Signal::derive(move || action.value().get());
|
||||
let validator = Validator::new();
|
||||
|
||||
view! {
|
||||
<ActionForm
|
||||
on:submit=move |ev| {
|
||||
let act = T::from_event(&ev);
|
||||
if !act.is_err() {
|
||||
validator.check(act.unwrap().entity(), &ev);
|
||||
// Capture phase, so the validation runs before the form is dispatched
|
||||
on:submit:capture=move |ev: SubmitEvent| {
|
||||
if let Ok(act) = T::from_event(&ev) {
|
||||
validator.check(act.entity(), &ev);
|
||||
}
|
||||
}
|
||||
action=action>
|
||||
@@ -65,20 +67,20 @@ pub fn data_form<T: 'static + Clone +
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn question_dialog<T: 'static
|
||||
+ ServerFn<Client = BrowserClient,
|
||||
InputEncoding = PostUrl,
|
||||
OutputEncoding = Json,
|
||||
Error = NoCustomError,
|
||||
Output = ApiResponse<()>>
|
||||
+ DeserializeOwned
|
||||
+ Clone>(
|
||||
pub fn question_dialog<T, O>(
|
||||
opener: DialogOpener,
|
||||
action: Action<T, Result<ApiResponse<()>, ServerFnError>>,
|
||||
action: ServerAction<T>,
|
||||
title: &'static str,
|
||||
children: Children
|
||||
) -> impl IntoView {
|
||||
let upd_val = action.value();
|
||||
) -> impl IntoView
|
||||
where
|
||||
T: ServerFn<Protocol = Http<PostUrl, O>, Output = ApiResponse<()>, Error = ServerFnError>
|
||||
+ DeserializeOwned + Clone + Send + Sync + 'static,
|
||||
T::Client: Client<ServerFnError>,
|
||||
<<T::Client as Client<ServerFnError>>::Request as ClientReq<ServerFnError>>::FormData: From<web_sys::FormData>,
|
||||
O: 'static
|
||||
{
|
||||
let upd_val = Signal::derive(move || action.value().get());
|
||||
|
||||
view! {
|
||||
<ActionForm action=action>
|
||||
|
||||
+15
-14
@@ -1,4 +1,4 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::*;
|
||||
use crate::app::DialogHelper;
|
||||
use crate::backend::appearance::get_appearance;
|
||||
@@ -8,18 +8,18 @@ use crate::components::user_menu::MenuOpener;
|
||||
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 = create_blocking_resource(||(), |_| get_appearance());
|
||||
let appearance = Resource::new_blocking(||(), |_| get_appearance());
|
||||
|
||||
view! {
|
||||
<Html
|
||||
{..}
|
||||
lang="cz"
|
||||
dir="ltr"
|
||||
class = {move || if drawer.visible() {"light-style layout-menu-fixed layout-menu-expanded"}
|
||||
else {"light-style layout-menu-fixed"} }
|
||||
attributes=vec![
|
||||
("data-theme", "theme-default".into_attribute()),
|
||||
("data-template", "vertical-menu-template-free".into_attribute()),
|
||||
("data-assets-path", "/".into_attribute())]
|
||||
data-theme="theme-default"
|
||||
data-template="vertical-menu-template-free"
|
||||
data-assets-path="/"
|
||||
/>
|
||||
<Title text="Rezervovator"/>
|
||||
<Meta charset="utf-8"/>
|
||||
@@ -41,19 +41,20 @@ pub fn Header() -> impl IntoView {
|
||||
<Link rel="stylesheet" href="/vendor/css/theme-default.css" />
|
||||
<Link rel="stylesheet" href="/css/demo.css" />
|
||||
<Transition fallback=move || view! {""}>
|
||||
{
|
||||
appearance.get().map(|a| match a {
|
||||
Ok(a) => view! {<Link rel="stylesheet" href={format!("/data/{}", a.css_name.unwrap_or_default())} />},
|
||||
Err(_) => view! {<Link rel="stylesheet" href="/data/banner.css" />}
|
||||
{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="/vendor/css/control.css" />
|
||||
|
||||
//<!-- Vendors CSS -->
|
||||
<Link rel="stylesheet" href="/vendor/libs/perfect-scrollbar/perfect-scrollbar.css" />
|
||||
<Body attributes=vec![
|
||||
("style", {move || if dlg_helper.is_open() {"overflow: hidden;"} else {""}}.into_attribute())
|
||||
]/>
|
||||
<Body {..} style={move || if dlg_helper.is_open() {"overflow: hidden;"} else {""}}/>
|
||||
}
|
||||
}
|
||||
+18
-13
@@ -1,5 +1,5 @@
|
||||
use crate::locales::trl;
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use crate::app::DialogHelper;
|
||||
use crate::validator::Validator;
|
||||
|
||||
@@ -11,15 +11,17 @@ pub struct DialogOpener {
|
||||
set_empty: WriteSignal<String>,
|
||||
not_checked: ReadSignal<Option<String>>,
|
||||
set_not_checked: WriteSignal<Option<String>>,
|
||||
show_err: RwSignal<bool>
|
||||
show_err: RwSignal<bool>,
|
||||
// Context is not available in event handlers, so the helper is looked up when the opener is created
|
||||
dlg_helper: Option<DialogHelper>
|
||||
}
|
||||
|
||||
impl DialogOpener {
|
||||
pub fn new() -> Self {
|
||||
let (visible, set_visible) = create_signal(false);
|
||||
let (empty, set_empty) = create_signal("".to_string());
|
||||
let (not_checked, set_not_checked) = create_signal(None);
|
||||
let show_err = create_rw_signal(false);
|
||||
let (visible, set_visible) = signal(false);
|
||||
let (empty, set_empty) = signal("".to_string());
|
||||
let (not_checked, set_not_checked) = signal(None);
|
||||
let show_err = RwSignal::new(false);
|
||||
DialogOpener {
|
||||
visible,
|
||||
set_visible,
|
||||
@@ -27,7 +29,8 @@ impl DialogOpener {
|
||||
set_empty,
|
||||
not_checked,
|
||||
set_not_checked,
|
||||
show_err
|
||||
show_err,
|
||||
dlg_helper: use_context::<DialogHelper>()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,15 +39,17 @@ impl DialogOpener {
|
||||
}
|
||||
|
||||
pub fn show(&self) {
|
||||
let dlg_helper = use_context::<DialogHelper>().expect("No dialog helper");
|
||||
dlg_helper.set_opened(true);
|
||||
self.set_visible.update(|state| *state = true);
|
||||
if let Some(dlg_helper) = self.dlg_helper {
|
||||
dlg_helper.set_opened(true);
|
||||
}
|
||||
self.set_visible.set(true);
|
||||
}
|
||||
|
||||
pub fn hide(&self) {
|
||||
let dlg_helper = use_context::<DialogHelper>().expect("No dialog helper");
|
||||
dlg_helper.set_opened(false);
|
||||
self.set_visible.update(|state| *state = false);
|
||||
if let Some(dlg_helper) = self.dlg_helper {
|
||||
dlg_helper.set_opened(false);
|
||||
}
|
||||
self.set_visible.set(false);
|
||||
self.set_empty.set("".to_string());
|
||||
self.set_not_checked.set(None);
|
||||
self.show_err.set(false);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use crate::components::modal_box::DialogOpener;
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use crate::backend::data::ApiResponse;
|
||||
use crate::locales::trl;
|
||||
|
||||
#[component]
|
||||
pub fn ServerErr(
|
||||
result: RwSignal<Option<Result<ApiResponse<()>, ServerFnError>>>,
|
||||
#[prop(into)]
|
||||
result: Signal<Option<Result<ApiResponse<()>, ServerFnError>>>,
|
||||
opener: DialogOpener,
|
||||
) -> impl IntoView {
|
||||
view! {{move || {
|
||||
@@ -23,22 +24,22 @@ pub fn ServerErr(
|
||||
}}>
|
||||
{trl(&err)}
|
||||
</div>
|
||||
}
|
||||
}.into_any()
|
||||
}
|
||||
else {
|
||||
opener.hide();
|
||||
view! {<div></div>}
|
||||
view! {<div></div>}.into_any()
|
||||
}
|
||||
Err(e) => {
|
||||
view! {
|
||||
<div class="alert alert-danger">
|
||||
"Server error: " {e.to_string()}
|
||||
</div>
|
||||
}
|
||||
}.into_any()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
view! {<div></div>}
|
||||
view! {<div></div>}.into_any()
|
||||
}
|
||||
}}
|
||||
}
|
||||
|
||||
+21
-16
@@ -1,4 +1,4 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use crate::app::MenuHelper;
|
||||
use crate::backend::data::User;
|
||||
use crate::backend::user::{get_user, logout};
|
||||
@@ -9,14 +9,17 @@ use crate::locales::trl;
|
||||
pub struct MenuOpener {
|
||||
visible: ReadSignal<bool>,
|
||||
set_visible: WriteSignal<bool>,
|
||||
// Context is not available in event handlers, so the helper is looked up when the opener is created
|
||||
helper: Option<MenuHelper>,
|
||||
}
|
||||
|
||||
impl MenuOpener {
|
||||
pub fn new() -> Self {
|
||||
let (visible, set_visible) = create_signal(false);
|
||||
let (visible, set_visible) = signal(false);
|
||||
MenuOpener {
|
||||
visible,
|
||||
set_visible,
|
||||
helper: use_context::<MenuHelper>(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,22 +28,24 @@ impl MenuOpener {
|
||||
}
|
||||
|
||||
pub fn toggle(self) {
|
||||
let visible = self.visible.get();
|
||||
self.set_visible.update(|v| *v = !visible);
|
||||
let visible = self.visible.get_untracked();
|
||||
self.set_visible.set(!visible);
|
||||
|
||||
let helper = use_context::<MenuHelper>().expect("No menu helper");
|
||||
if !visible {
|
||||
helper.close();
|
||||
helper.set_opened(self);
|
||||
} else {
|
||||
helper.reset();
|
||||
if let Some(helper) = self.helper {
|
||||
if !visible {
|
||||
helper.close();
|
||||
helper.set_opened(self);
|
||||
} else {
|
||||
helper.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close(&self) {
|
||||
self.set_visible.set(false);
|
||||
let helper = use_context::<MenuHelper>().expect("No menu helper");
|
||||
helper.reset();
|
||||
if let Some(helper) = self.helper {
|
||||
helper.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +55,7 @@ pub fn UserMenu(
|
||||
editor: DialogOpener,
|
||||
pw_dialog: DialogOpener,
|
||||
user_profile: WriteSignal<User>) -> impl IntoView {
|
||||
let user = create_resource(move || opener.visible(), move |_| get_user());
|
||||
let user = Resource::new(move || opener.visible(), move |_| get_user());
|
||||
|
||||
view! {
|
||||
<ul
|
||||
@@ -84,13 +89,13 @@ pub fn UserMenu(
|
||||
<span class="fw-semibold d-block">
|
||||
{usr.full_name}
|
||||
</span>
|
||||
}
|
||||
}.into_any()
|
||||
}
|
||||
Err(_) => {
|
||||
view! {
|
||||
// <small class="text-muted">"Admin"</small>
|
||||
<span>"Error loading user"</span>
|
||||
}
|
||||
}.into_any()
|
||||
}
|
||||
})
|
||||
}}
|
||||
@@ -114,7 +119,7 @@ pub fn UserMenu(
|
||||
class="dropdown-item"
|
||||
href="/login"
|
||||
on:click=move |_| {
|
||||
spawn_local(async move {
|
||||
leptos::task::spawn_local(async move {
|
||||
let _ = logout().await;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use crate::locales::trl;
|
||||
use crate::validator::Validator;
|
||||
|
||||
@@ -20,16 +20,16 @@ pub fn ValidationErr(
|
||||
<div>
|
||||
{out_msgs}
|
||||
</div>
|
||||
}
|
||||
}.into_any()
|
||||
} else {
|
||||
view! {
|
||||
<div class="alert alert-danger">
|
||||
{trl("Validation error")}
|
||||
</div>
|
||||
}
|
||||
}.into_any()
|
||||
}
|
||||
} else {
|
||||
view! {<div></div>}
|
||||
view! {<div></div>}.into_any()
|
||||
}
|
||||
}}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Debug, Display, Formatter};
|
||||
use chrono::ParseError;
|
||||
use leptos::ServerFnError;
|
||||
use leptos::prelude::ServerFnError;
|
||||
use log::error;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
|
||||
+9
-17
@@ -1,3 +1,6 @@
|
||||
// Leptos view types are deeply nested; release builds overflow the default limit
|
||||
#![recursion_limit = "512"]
|
||||
|
||||
pub mod app;
|
||||
pub mod locales;
|
||||
pub mod backend;
|
||||
@@ -7,23 +10,12 @@ mod validator;
|
||||
pub mod error;
|
||||
pub mod config;
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use crate::app::*;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "hydrate")] {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn hydrate() {
|
||||
use app::*;
|
||||
use leptos::*;
|
||||
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::mount_to_body(move || {
|
||||
view! { <App/> }
|
||||
});
|
||||
}
|
||||
}
|
||||
leptos::mount::hydrate_body(App);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use lazy_static::lazy_static;
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use crate::locales::Locales;
|
||||
|
||||
lazy_static! {
|
||||
@@ -182,16 +182,15 @@ lazy_static! {
|
||||
};
|
||||
}
|
||||
|
||||
fn get_locales() -> Vec<Option<String>> {
|
||||
pub fn get_locales() -> Vec<Option<String>> {
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
{
|
||||
use_context::<Locales>().unwrap_or(Locales(vec![])).0
|
||||
// Context is not available in event handlers, ask the browser there
|
||||
use_context::<Locales>().unwrap_or_else(crate::locales::browser_locales).0
|
||||
}
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
use actix_web::HttpRequest;
|
||||
|
||||
let req = use_context::<HttpRequest>();
|
||||
let req = use_context::<leptos_actix::Request>();
|
||||
if let Some(r) = req {
|
||||
if let Some(al) = r.head().headers.get("accept-language") {
|
||||
Locales(al.to_str().unwrap_or("").split(",").map(|l| Some(l.to_string())).collect()).0
|
||||
|
||||
+18
-18
@@ -1,6 +1,6 @@
|
||||
use chrono::{NaiveDate, Weekday};
|
||||
use leptos::use_context;
|
||||
use crate::locales::catalogues::get_dictionary;
|
||||
use leptos::prelude::*;
|
||||
use crate::locales::catalogues::{get_dictionary, get_locales};
|
||||
|
||||
mod catalogues;
|
||||
|
||||
@@ -10,24 +10,12 @@ pub struct Locales(pub Vec<Option<String>>);
|
||||
pub fn init_locales() {
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
{
|
||||
use leptos::*;
|
||||
let loc = Locales(
|
||||
window()
|
||||
.navigator()
|
||||
.languages()
|
||||
.into_iter()
|
||||
.map(|val| val.as_string())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
provide_context(loc);
|
||||
provide_context(browser_locales());
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
{
|
||||
use actix_web::HttpRequest;
|
||||
use leptos::provide_context;
|
||||
|
||||
let req = use_context::<HttpRequest>();
|
||||
let req = use_context::<leptos_actix::Request>();
|
||||
if let Some(r) = req {
|
||||
if r.query_string().contains("lang") {
|
||||
let languages = r.query_string()
|
||||
@@ -49,6 +37,19 @@ pub fn init_locales() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Languages preferred by the browser
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
pub fn browser_locales() -> Locales {
|
||||
Locales(
|
||||
window()
|
||||
.navigator()
|
||||
.languages()
|
||||
.into_iter()
|
||||
.map(|val| val.as_string())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn trl(phrase: &str) -> impl Fn() -> String {
|
||||
let mut translated = phrase;
|
||||
if let Some(dict) = get_dictionary() {
|
||||
@@ -65,8 +66,7 @@ pub fn trl(phrase: &str) -> impl Fn() -> String {
|
||||
// ToDo better date formatting
|
||||
pub fn loc_date(date: NaiveDate) -> impl Fn() -> String {
|
||||
let mut dt = date.format("%Y-%m-%d").to_string();
|
||||
let locs = use_context::<Locales>().unwrap_or(Locales(vec![])).0;
|
||||
for loc in locs {
|
||||
for loc in get_locales() {
|
||||
if let Some(key) = loc {
|
||||
if let Some(k) = key.split("-").collect::<Vec<_>>().get(0) {
|
||||
if *k != "en" {
|
||||
|
||||
+32
-19
@@ -1,3 +1,6 @@
|
||||
// Leptos view types are deeply nested; release builds overflow the default limit
|
||||
#![recursion_limit = "512"]
|
||||
|
||||
use actix_web::middleware::Logger;
|
||||
use leptos_captcha::spow::pow::Pow;
|
||||
use log::error;
|
||||
@@ -11,7 +14,9 @@ use rezervator::backend::user::create_admin;
|
||||
async fn main() -> std::io::Result<()> {
|
||||
use actix_files::Files;
|
||||
use actix_web::*;
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use leptos::config::get_configuration;
|
||||
use leptos_meta::MetaTags;
|
||||
use leptos_actix::{generate_route_list, LeptosRoutes};
|
||||
use rezervator::app::*;
|
||||
use rezervator::backend::*;
|
||||
@@ -48,9 +53,7 @@ async fn main() -> std::io::Result<()> {
|
||||
env_logger::Builder::from_env(Env::default().default_filter_or(srv_conf.logging().severity())).init();
|
||||
info!("Starting server");
|
||||
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(|| view! { <App/> });
|
||||
let conf = get_configuration(None).unwrap();
|
||||
let key = Key::from(srv_conf.session().key());
|
||||
|
||||
let pool = PgPoolOptions::new()
|
||||
@@ -75,8 +78,10 @@ async fn main() -> std::io::Result<()> {
|
||||
}
|
||||
|
||||
HttpServer::new(move || {
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(App);
|
||||
let leptos_options = &conf.leptos_options;
|
||||
let site_root = &leptos_options.site_root;
|
||||
let site_root = leptos_options.site_root.clone().to_string();
|
||||
|
||||
App::new()
|
||||
.app_data(Data::new(AppData::new(pool.clone(), mailer.clone())))
|
||||
@@ -86,12 +91,26 @@ async fn main() -> std::io::Result<()> {
|
||||
CookieSessionStore::default(),
|
||||
key.clone()
|
||||
))
|
||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||
.leptos_routes(
|
||||
leptos_options.to_owned(),
|
||||
routes.to_owned(),
|
||||
|| view! { <App/> },
|
||||
)
|
||||
// Server functions under /api are registered by leptos_routes
|
||||
.leptos_routes(routes, {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || {
|
||||
view! {
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<AutoReload options=leptos_options.clone()/>
|
||||
<HydrationScripts options=leptos_options.clone()/>
|
||||
<MetaTags/>
|
||||
</head>
|
||||
<body>
|
||||
<App/>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
}
|
||||
})
|
||||
.app_data(Data::new(leptos_options.to_owned()))
|
||||
.service(appearance::upload)
|
||||
.service(Files::new("/", site_root))
|
||||
.wrap(middleware::Compress::default())
|
||||
@@ -114,15 +133,9 @@ pub fn main() {
|
||||
// a client-side main function is required for using `trunk serve`
|
||||
// prefer using `cargo leptos serve` instead
|
||||
// to run: `trunk serve --open --features ssg`
|
||||
use leptos::*;
|
||||
use leptos_start::app::*;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use rezervator::app::*;
|
||||
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
leptos::mount_to_body(move || {
|
||||
// note: for testing it may be preferrable to replace this with a
|
||||
// more specific component, although leptos_router should still work
|
||||
view! {<App/> }
|
||||
});
|
||||
leptos::mount::mount_to_body(App);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use chrono::{Datelike, Local};
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use crate::backend::data::{ApiResponse, ReservationState, ResSumWithItems};
|
||||
use crate::backend::reservation::{month_chart, reservations_in_month, year_chart, years};
|
||||
use crate::components::modal_box::{DialogOpener, ModalBody, ModalDialog};
|
||||
@@ -11,7 +11,7 @@ use crate::pages::new_reservations::CancelDialog;
|
||||
fn booking_detail(reservation: ReadSignal<ResSumWithItems>, opener: DialogOpener) -> impl IntoView {
|
||||
let menu = MenuOpener::new();
|
||||
let cancel_opener = DialogOpener::new();
|
||||
let menu_visible = create_rw_signal(false);
|
||||
let menu_visible = RwSignal::new(false);
|
||||
view! {
|
||||
<CancelDialog opener=cancel_opener reservation=reservation/>
|
||||
<ModalDialog title="Booking detail" opener=opener>
|
||||
@@ -27,7 +27,7 @@ fn booking_detail(reservation: ReadSignal<ResSumWithItems>, opener: DialogOpener
|
||||
let:item>
|
||||
{item.property.name}": "{item.reservation.from.to_string()}" - "{item.reservation.to.to_string()}<br/>
|
||||
</For>
|
||||
{trl("Customer: ")}{detail.customer.full_name}", "<a href={format!("mailto:{}", detail.customer.email)}>{detail.customer.email}</a>", "{detail.customer.phone}<br/>
|
||||
{trl("Customer: ")}{detail.customer.full_name}", "<a href={format!("mailto:{}", detail.customer.email)}>{detail.customer.email.clone()}</a>", "{detail.customer.phone}<br/>
|
||||
{
|
||||
let note = detail.summary.note.clone();
|
||||
let show = note.is_some() && !note.clone().unwrap().is_empty();
|
||||
@@ -42,7 +42,7 @@ fn booking_detail(reservation: ReadSignal<ResSumWithItems>, opener: DialogOpener
|
||||
}}
|
||||
</p>
|
||||
<Show when=move || menu_visible.get()>
|
||||
<table width="100%">
|
||||
<table style="width: 100%"><tbody>
|
||||
<tr>
|
||||
<td width="100%"></td>
|
||||
<td>
|
||||
@@ -62,7 +62,7 @@ fn booking_detail(reservation: ReadSignal<ResSumWithItems>, opener: DialogOpener
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</tbody></table>
|
||||
</Show>
|
||||
</ModalBody>
|
||||
</ModalDialog>
|
||||
@@ -71,15 +71,15 @@ fn booking_detail(reservation: ReadSignal<ResSumWithItems>, opener: DialogOpener
|
||||
|
||||
#[component]
|
||||
pub fn bookings() -> impl IntoView {
|
||||
let year_chart = create_blocking_resource(||(), |_| year_chart());
|
||||
let years = create_blocking_resource(||(), |_| years());
|
||||
let year = create_rw_signal(Local::now().year());
|
||||
let month = create_rw_signal(Local::now().month());
|
||||
let chart = create_blocking_resource(move || year.get(),move |y| month_chart(y));
|
||||
let year_chart = Resource::new_blocking(||(), |_| year_chart());
|
||||
let years = Resource::new_blocking(||(), |_| years());
|
||||
let year = RwSignal::new(Local::now().year());
|
||||
let month = RwSignal::new(Local::now().month());
|
||||
let chart = Resource::new_blocking(move || year.get(),move |y| month_chart(y));
|
||||
let detail_dlg = DialogOpener::new();
|
||||
let reservations = create_blocking_resource(move || (year.get(), month.get(), detail_dlg.visible()), move |p| reservations_in_month(p.0, p.1));
|
||||
let reservations = Resource::new_blocking(move || (year.get(), month.get(), detail_dlg.visible()), move |p| reservations_in_month(p.0, p.1));
|
||||
let all_months: Vec<u32> = vec![1,2,3,4,5,6,7,8,9,10,11,12];
|
||||
let res_detail = create_rw_signal(ResSumWithItems::default());
|
||||
let res_detail = RwSignal::new(ResSumWithItems::default());
|
||||
|
||||
view! {
|
||||
<h1>{trl("Booking overview")}</h1>
|
||||
@@ -94,7 +94,7 @@ pub fn bookings() -> impl IntoView {
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<Transition fallback=move || view! {<div>"Loading"</div>} >
|
||||
{
|
||||
{move || {
|
||||
years.get().map(|y| match y {
|
||||
Ok(y) => {
|
||||
view! {
|
||||
@@ -106,11 +106,11 @@ pub fn bookings() -> impl IntoView {
|
||||
<option prop:value=move || y prop:selected=move || year.get() == y>{y}</option>
|
||||
</For>
|
||||
</select>
|
||||
}
|
||||
}.into_any()
|
||||
}
|
||||
Err(_) => {view! {<select><option></option></select>}}
|
||||
Err(_) => {view! {<select><option></option></select>}.into_any()}
|
||||
})
|
||||
}
|
||||
}}
|
||||
</Transition>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
@@ -135,30 +135,24 @@ pub fn bookings() -> impl IntoView {
|
||||
<div class="col-md">
|
||||
<div class="card md-3">
|
||||
<Transition fallback=move || view! {<div>"Loading"</div>}>
|
||||
{
|
||||
{move || {
|
||||
chart.get().map(move |c| match c {
|
||||
Ok(c) => { match c {
|
||||
ApiResponse::Data(c) => {view! {<img style="margin: 1em" src={c.clone()} /> }}
|
||||
ApiResponse::Error(_) => {view! {<img src=""/> }} }
|
||||
}
|
||||
Err(_) => { view! {<img src=""/> } }
|
||||
Ok(ApiResponse::Data(c)) => {view! {<img style="margin: 1em" src={c.clone()} /> }.into_any()}
|
||||
_ => { view! {<img src=""/> }.into_any() }
|
||||
})
|
||||
}
|
||||
}}
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="card md-3">
|
||||
<Transition fallback=move || view! {<div>"Loading"</div>}>
|
||||
{
|
||||
{move || {
|
||||
year_chart.get().map(move |c| match c {
|
||||
Ok(c) => { match c {
|
||||
ApiResponse::Data(c) => {view! {<img style="margin: 1em" src={c.clone()} /> }}
|
||||
ApiResponse::Error(_) => {view! {<img src=""/> }} }
|
||||
}
|
||||
Err(_) => { view! {<img src=""/> } }
|
||||
Ok(ApiResponse::Data(c)) => {view! {<img style="margin: 1em" src={c.clone()} /> }.into_any()}
|
||||
_ => { view! {<img src=""/> }.into_any() }
|
||||
})
|
||||
}
|
||||
}}
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
@@ -178,7 +172,7 @@ pub fn bookings() -> impl IntoView {
|
||||
<th>{trl("Actions")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{
|
||||
{move || {
|
||||
reservations.get().map(|r| match r {
|
||||
Ok(r) => { match r {
|
||||
ApiResponse::Data(r) => {
|
||||
@@ -202,20 +196,20 @@ pub fn bookings() -> impl IntoView {
|
||||
</tr>
|
||||
</For>
|
||||
</tbody>
|
||||
}
|
||||
}.into_any()
|
||||
}
|
||||
ApiResponse::Error(e) => {
|
||||
view! {<tbody class="table-border-bottom-0">
|
||||
<tr><td colspan=5>{trl("Something went wrong")}<br/>{e.to_string()}</td></tr></tbody>}
|
||||
<tr><td colspan=5>{trl("Something went wrong")}<br/>{e.to_string()}</td></tr></tbody>}.into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
view! {<tbody class="table-border-bottom-0">
|
||||
<tr><td colspan=5>{trl("Something went wrong")}<br/>{e.to_string()}</td></tr></tbody>}
|
||||
<tr><td colspan=5>{trl("Something went wrong")}<br/>{e.to_string()}</td></tr></tbody>}.into_any()
|
||||
}
|
||||
})
|
||||
}
|
||||
}}
|
||||
</table>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use crate::backend::appearance::{DeleteBanner, get_appearance, UpdateAppearance};
|
||||
use crate::backend::data::Appearance;
|
||||
use crate::components::data_form::{DataForm, QuestionDialog};
|
||||
@@ -7,7 +7,7 @@ use crate::locales::trl;
|
||||
|
||||
#[component]
|
||||
fn edit_title(opener: DialogOpener, appearance: ReadSignal<Appearance>) -> impl IntoView {
|
||||
let update = create_server_action::<UpdateAppearance>();
|
||||
let update = ServerAction::<UpdateAppearance>::new();
|
||||
view! {
|
||||
<DataForm opener=opener action=update title="Update title">
|
||||
<input type="hidden" prop:value={move || appearance.get().text.unwrap_or_default()} name="appearance[text]" />
|
||||
@@ -31,7 +31,7 @@ fn edit_title(opener: DialogOpener, appearance: ReadSignal<Appearance>) -> impl
|
||||
|
||||
#[component]
|
||||
fn edit_text(opener: DialogOpener, appearance: ReadSignal<Appearance>) -> impl IntoView {
|
||||
let update = create_server_action::<UpdateAppearance>();
|
||||
let update = ServerAction::<UpdateAppearance>::new();
|
||||
view! {
|
||||
<DataForm opener=opener action=update title="Update text">
|
||||
<input type="hidden" prop:value={move || appearance.get().title.unwrap_or_default()} name="appearance[title]" />
|
||||
@@ -53,7 +53,7 @@ fn edit_text(opener: DialogOpener, appearance: ReadSignal<Appearance>) -> impl I
|
||||
|
||||
#[component]
|
||||
fn delete_banner(opener: DialogOpener) -> impl IntoView {
|
||||
let delete = create_server_action::<DeleteBanner>();
|
||||
let delete = ServerAction::<DeleteBanner>::new();
|
||||
view! {
|
||||
<QuestionDialog opener=opener action=delete title="Delete banner">
|
||||
<div>{trl("Are you sure you want to delete banner?")}</div>
|
||||
@@ -66,8 +66,8 @@ 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());
|
||||
let appearance = Resource::new(move || title_edit.visible() || text_edit.visible() || del_dialog.visible(), |_| get_appearance());
|
||||
let app_edit = RwSignal::new(Appearance::default());
|
||||
view! {
|
||||
<EditTitle opener=title_edit appearance=app_edit.read_only()/>
|
||||
<EditText opener=text_edit appearance=app_edit.read_only()/>
|
||||
@@ -85,16 +85,17 @@ pub fn appearance() -> impl IntoView {
|
||||
</button>
|
||||
</form><br/>
|
||||
<Transition fallback=move || view! {<p>{trl("Loading...")}</p> }>
|
||||
{
|
||||
{move || {
|
||||
appearance.get().map(|a| match a {
|
||||
Ok(a) => {
|
||||
app_edit.set(a.clone());
|
||||
let app = a.clone();
|
||||
Ok(app) => {
|
||||
app_edit.set(app.clone());
|
||||
let has_banner = app.banner.is_some();
|
||||
let title = app.title.clone().unwrap_or_default();
|
||||
view! {
|
||||
<div>
|
||||
<Show when=move || a.clone().banner.is_some()>
|
||||
<Show when=move || has_banner>
|
||||
<div class="header_banner">
|
||||
<h1 class="header_banner">{app.title.clone().unwrap_or_default()}</h1>
|
||||
<h1 class="header_banner">{title.clone()}</h1>
|
||||
</div>
|
||||
</Show>
|
||||
<div>
|
||||
@@ -111,13 +112,13 @@ pub fn appearance() -> impl IntoView {
|
||||
<i class="bx bx-edit-alt me-1"></i> {trl("Edit text")}</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}.into_any()
|
||||
}
|
||||
Err(e) => {view! {<div><p>{trl("Error loading data")}</p>
|
||||
<p>{e.to_string()}</p></div>
|
||||
}}
|
||||
}.into_any()}
|
||||
})
|
||||
}
|
||||
}}
|
||||
</Transition>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use crate::backend::data::User;
|
||||
use crate::backend::user::{ChangePwd, get_user};
|
||||
use crate::components::data_form::DataForm;
|
||||
@@ -7,8 +7,8 @@ use crate::locales::trl;
|
||||
|
||||
#[component]
|
||||
pub fn change_password(user: ReadSignal<User>, opener: DialogOpener) -> impl IntoView {
|
||||
let change_pwd = create_server_action::<ChangePwd>();
|
||||
let logged_in = create_resource(||(), move |_| get_user());
|
||||
let change_pwd = ServerAction::<ChangePwd>::new();
|
||||
let logged_in = Resource::new(||(), move |_| get_user());
|
||||
|
||||
view! {
|
||||
<DataForm opener=opener action=change_pwd title="Change password">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use chrono::Local;
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use crate::backend::data::ClosingTime;
|
||||
use crate::backend::opening_hours::{get_closing_times, DeleteClosingTime, InsertClosingTime};
|
||||
use crate::components::data_form::{DataForm, QuestionDialog};
|
||||
@@ -8,7 +8,7 @@ use crate::locales::{loc_date, trl};
|
||||
|
||||
#[component]
|
||||
pub fn del_closing_day(closing_time: ReadSignal<ClosingTime>, opener: DialogOpener) -> impl IntoView {
|
||||
let delete = create_server_action::<DeleteClosingTime>();
|
||||
let delete = ServerAction::<DeleteClosingTime>::new();
|
||||
|
||||
view! {
|
||||
<QuestionDialog opener=opener action=delete title="Delete closing days">
|
||||
@@ -20,7 +20,7 @@ pub fn del_closing_day(closing_time: ReadSignal<ClosingTime>, opener: DialogOpen
|
||||
|
||||
#[component]
|
||||
pub fn insert_closing_days(opener: DialogOpener) -> impl IntoView {
|
||||
let insert_day = create_server_action::<InsertClosingTime>();
|
||||
let insert_day = ServerAction::<InsertClosingTime>::new();
|
||||
|
||||
view! {
|
||||
<DataForm opener=opener action=insert_day title="Closing days">
|
||||
@@ -52,8 +52,8 @@ pub fn insert_closing_days(opener: DialogOpener) -> impl IntoView {
|
||||
pub fn closing_days() -> impl IntoView {
|
||||
let delete_dialog = DialogOpener::new();
|
||||
let editor = DialogOpener::new();
|
||||
let times = create_blocking_resource(move || editor.visible() || delete_dialog.visible(), move |_| {get_closing_times()});
|
||||
let time_to_del = create_rw_signal(ClosingTime::default());
|
||||
let times = Resource::new_blocking(move || editor.visible() || delete_dialog.visible(), move |_| {get_closing_times()});
|
||||
let time_to_del = RwSignal::new(ClosingTime::default());
|
||||
view! {
|
||||
<DelClosingDay closing_time=time_to_del.read_only() opener=delete_dialog/>
|
||||
<InsertClosingDays opener=editor/>
|
||||
@@ -78,7 +78,7 @@ pub fn closing_days() -> impl IntoView {
|
||||
e.to_string()
|
||||
};
|
||||
view! {<tbody class="table-border-bottom-0">
|
||||
<tr><td colspan=3>{trl(&err)}</td></tr></tbody>}}
|
||||
<tr><td colspan=3>{trl(&err)}</td></tr></tbody>}.into_any()}
|
||||
Ok(c) => {
|
||||
view! {<tbody class="table-border-bottom-0">
|
||||
<For each=move || c.clone()
|
||||
@@ -100,7 +100,7 @@ pub fn closing_days() -> impl IntoView {
|
||||
</tr>
|
||||
}
|
||||
}/></tbody>
|
||||
}
|
||||
}.into_any()
|
||||
}
|
||||
})
|
||||
}}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::backend::data::Company;
|
||||
use crate::backend::company::UpdateCompany;
|
||||
use crate::components::modal_box::DialogOpener;
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use crate::components::data_form::DataForm;
|
||||
use crate::locales::trl;
|
||||
|
||||
@@ -10,7 +10,7 @@ pub fn CompanyEdit(
|
||||
company: ReadSignal<Company>,
|
||||
opener: DialogOpener,
|
||||
) -> impl IntoView {
|
||||
let update_company = create_server_action::<UpdateCompany>();
|
||||
let update_company = ServerAction::<UpdateCompany>::new();
|
||||
|
||||
view! {
|
||||
<DataForm opener=opener action=update_company title="Edit company">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use crate::backend::company::get_company;
|
||||
use crate::backend::data::{ApiResponse, Company};
|
||||
use crate::components::modal_box::DialogOpener;
|
||||
@@ -8,21 +8,21 @@ use crate::pages::company_edit::CompanyEdit;
|
||||
#[component]
|
||||
pub fn CompanyInfo() -> impl IntoView {
|
||||
let editor = DialogOpener::new();
|
||||
let company = create_blocking_resource(move|| editor.visible(), move |_| { get_company() });
|
||||
let (cmp, set_cmp) = create_signal(Company::default());
|
||||
let company = Resource::new_blocking(move|| editor.visible(), move |_| { get_company() });
|
||||
let (cmp, set_cmp) = signal(Company::default());
|
||||
|
||||
view! {
|
||||
<CompanyEdit company={cmp} opener=editor/>
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="bx bx-buildings"></i>" "{trl("Company info")}</h5>
|
||||
<p class="card-text">
|
||||
<div class="card-text">
|
||||
<Transition fallback=move || view! {<p>{trl("Loading...")}</p> }>
|
||||
{move || {
|
||||
company.get().map(|c| match c {
|
||||
Err(e) => {view! {<div><p>{trl("Error loading data")}</p>
|
||||
<p>{e.to_string()}</p></div>
|
||||
}}
|
||||
}.into_any()}
|
||||
Ok(c) => {
|
||||
match c {
|
||||
ApiResponse::Data(d) => {set_cmp.update(|cmp| *cmp = d.clone());
|
||||
@@ -30,16 +30,16 @@ pub fn CompanyInfo() -> impl IntoView {
|
||||
<p><b>{d.name}</b></p>
|
||||
<p>{d.street}" "{d.house_number}<br/>
|
||||
{d.zip_code}" "{d.city}
|
||||
</p></div>}
|
||||
</p></div>}.into_any()
|
||||
}
|
||||
ApiResponse::Error(_) => {view! {<div><p>{trl("Error loading data")}</p></div>}}
|
||||
ApiResponse::Error(_) => {view! {<div><p>{trl("Error loading data")}</p></div>}.into_any()}
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}}
|
||||
</Transition>
|
||||
</p>
|
||||
</div>
|
||||
<a href="javascript:void(0)" class="card-link" on:click = move |_| editor.show()>
|
||||
<i class="bx bx-edit-alt fs-4 lh-0"></i></a>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use crate::backend::customer::get_customers;
|
||||
use crate::backend::data::ApiResponse;
|
||||
use crate::locales::trl;
|
||||
|
||||
#[component]
|
||||
pub fn customers() -> impl IntoView {
|
||||
let customers = create_blocking_resource(||(), |_| get_customers());
|
||||
let customers = Resource::new_blocking(||(), |_| get_customers());
|
||||
|
||||
view! {
|
||||
<h1>{trl("Customers")}</h1>
|
||||
@@ -21,7 +21,7 @@ pub fn customers() -> impl IntoView {
|
||||
<th>{trl("Phone")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{
|
||||
{move || {
|
||||
customers.get().map(|c| match c {
|
||||
Ok(c) => {match c {
|
||||
ApiResponse::Data(c) => {
|
||||
@@ -35,20 +35,20 @@ pub fn customers() -> impl IntoView {
|
||||
</tr>
|
||||
</For>
|
||||
</tbody>
|
||||
}
|
||||
}.into_any()
|
||||
}
|
||||
ApiResponse::Error(e) => {
|
||||
view! {<tbody class="table-border-bottom-0">
|
||||
<tr><td colspan=3>{trl("Something went wrong")}<br/>{e.to_string()}</td></tr></tbody>}
|
||||
<tr><td colspan=3>{trl("Something went wrong")}<br/>{e.to_string()}</td></tr></tbody>}.into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
view! {<tbody class="table-border-bottom-0">
|
||||
<tr><td colspan=3>{trl("Something went wrong")}<br/>{e.to_string()}</td></tr></tbody>}
|
||||
<tr><td colspan=3>{trl("Something went wrong")}<br/>{e.to_string()}</td></tr></tbody>}.into_any()
|
||||
}
|
||||
})
|
||||
}
|
||||
}}
|
||||
</table>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use crate::components::modal_box::DialogOpener;
|
||||
use crate::locales::trl;
|
||||
use crate::pages::new_reservations::NewReservations;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use crate::backend::data::WeekHours;
|
||||
use crate::backend::opening_hours::{DeleteClosingTime, UpdateHours};
|
||||
use crate::components::data_form::{DataForm, QuestionDialog};
|
||||
@@ -7,7 +7,7 @@ use crate::locales::trl;
|
||||
|
||||
#[component]
|
||||
pub fn EditHours(opener: DialogOpener, hours: ReadSignal<WeekHours>) -> impl IntoView {
|
||||
let update_hours = create_server_action::<UpdateHours>();
|
||||
let update_hours = ServerAction::<UpdateHours>::new();
|
||||
|
||||
view! {
|
||||
<DataForm opener=opener action=update_hours title="Edit hours">
|
||||
@@ -31,7 +31,7 @@ pub fn EditHours(opener: DialogOpener, hours: ReadSignal<WeekHours>) -> impl Int
|
||||
|
||||
#[component]
|
||||
pub fn del_closing_days(opener: DialogOpener) -> impl IntoView {
|
||||
let delete = create_server_action::<DeleteClosingTime>();
|
||||
let delete = ServerAction::<DeleteClosingTime>::new();
|
||||
|
||||
view! {
|
||||
<QuestionDialog opener=opener action=delete title="Delete closing days">
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::ActionForm;
|
||||
use leptos::form::ActionForm;
|
||||
use crate::backend::user::Login;
|
||||
use crate::components::modal_box::DialogOpener;
|
||||
use crate::components::server_err::ServerErr;
|
||||
@@ -8,8 +8,8 @@ use crate::locales::trl;
|
||||
|
||||
#[component]
|
||||
pub fn Login() -> impl IntoView {
|
||||
let login = create_server_action::<Login>();
|
||||
let login_val = login.value();
|
||||
let login = ServerAction::<Login>::new();
|
||||
let login_val = Signal::derive(move || login.value().get());
|
||||
view! {
|
||||
<Link rel="stylesheet" href="/vendor/css/pages/page-auth.css" />
|
||||
<div class="authentication-wrapper authentication-basic container-p-y">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use crate::backend::data::MessageType;
|
||||
use crate::locales::trl;
|
||||
use crate::pages::mail_view::MailView;
|
||||
|
||||
+10
-10
@@ -1,4 +1,4 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use crate::backend::data::{Message, MessageType};
|
||||
use crate::backend::mail::{get_message, UpdateMessage};
|
||||
use crate::components::data_form::DataForm;
|
||||
@@ -7,7 +7,7 @@ use crate::locales::trl;
|
||||
|
||||
#[component]
|
||||
fn mail_edit(opener: DialogOpener, mail: ReadSignal<Message>) -> impl IntoView {
|
||||
let update = create_server_action::<UpdateMessage>();
|
||||
let update = ServerAction::<UpdateMessage>::new();
|
||||
|
||||
view! {
|
||||
<DataForm opener=opener title="Edit mail" action=update>
|
||||
@@ -44,17 +44,17 @@ fn mail_edit(opener: DialogOpener, mail: ReadSignal<Message>) -> impl IntoView {
|
||||
#[component]
|
||||
pub fn mail_view(title: &'static str, mail_type: MessageType) -> impl IntoView {
|
||||
let editor = DialogOpener::new();
|
||||
let mail = create_blocking_resource( move || editor.visible(), move |_| get_message(mail_type.clone()));
|
||||
let for_edit = create_rw_signal(Message::default());
|
||||
let mail = Resource::new_blocking( move || editor.visible(), move |_| get_message(mail_type.clone()));
|
||||
let for_edit = RwSignal::new(Message::default());
|
||||
|
||||
view! {
|
||||
<MailEdit opener=editor mail={for_edit.read_only()}/>
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="bx bx-buildings"></i>" "{trl(title)}</h5>
|
||||
<p class="card-text">
|
||||
<div class="card-text">
|
||||
<Transition fallback=move || view! {<p>{trl("Loading...")}</p> }>
|
||||
{
|
||||
{move || {
|
||||
mail.get().map(|m| match m {
|
||||
Ok(m) => {
|
||||
for_edit.set(m.clone());
|
||||
@@ -63,16 +63,16 @@ pub fn mail_view(title: &'static str, mail_type: MessageType) -> impl IntoView {
|
||||
<p>{trl("Subject: ")}{m.subject}</p>
|
||||
<p>{m.text}</p>
|
||||
</div>
|
||||
}
|
||||
}.into_any()
|
||||
}
|
||||
Err(_) => {
|
||||
view! {
|
||||
<div>"Error loading data"</div>
|
||||
}
|
||||
}.into_any()
|
||||
}})
|
||||
}
|
||||
}}
|
||||
</Transition>
|
||||
</p>
|
||||
</div>
|
||||
<a href="javascript:void(0)" class="card-link" on:click = move |_| editor.show()>
|
||||
<i class="bx bx-edit-alt fs-4 lh-0"></i></a>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use crate::backend::data::{ApiResponse, Page, ResSumWithItems};
|
||||
use crate::backend::reservation::{Approve, Cancel, get_new_reservations};
|
||||
use crate::components::data_form::QuestionDialog;
|
||||
@@ -10,7 +10,7 @@ use chrono::Datelike;
|
||||
|
||||
#[component]
|
||||
fn approve_dialog(reservation: ReadSignal<ResSumWithItems>, opener: DialogOpener) -> impl IntoView {
|
||||
let approve = create_server_action::<Approve>();
|
||||
let approve = ServerAction::<Approve>::new();
|
||||
|
||||
view! {
|
||||
<QuestionDialog opener=opener action=approve title="Approve booking">
|
||||
@@ -27,7 +27,7 @@ fn approve_dialog(reservation: ReadSignal<ResSumWithItems>, opener: DialogOpener
|
||||
|
||||
#[component]
|
||||
pub fn cancel_dialog(reservation: ReadSignal<ResSumWithItems>, opener: DialogOpener) -> impl IntoView {
|
||||
let cancel = create_server_action::<Cancel>();
|
||||
let cancel = ServerAction::<Cancel>::new();
|
||||
|
||||
view! {
|
||||
<QuestionDialog opener=opener action=cancel title="Cancel booking">
|
||||
@@ -56,13 +56,13 @@ fn create_dialog(opener: DialogOpener) -> impl IntoView {
|
||||
#[component]
|
||||
pub fn new_reservations(app_opener: DialogOpener, cancel_opener: DialogOpener) -> impl IntoView {
|
||||
let create = DialogOpener::new();
|
||||
let page = create_rw_signal(Page::new(5));
|
||||
let first = create_rw_signal(false);
|
||||
let last = create_rw_signal(false);
|
||||
let total_pages = create_rw_signal(0i32);
|
||||
let res = create_blocking_resource(move || (app_opener.visible() || cancel_opener.visible() || create.visible(), page),
|
||||
let page = RwSignal::new(Page::new(5));
|
||||
let first = RwSignal::new(false);
|
||||
let last = RwSignal::new(false);
|
||||
let total_pages = RwSignal::new(0i32);
|
||||
let res = Resource::new_blocking(move || (app_opener.visible() || cancel_opener.visible() || create.visible(), page),
|
||||
move |d| get_new_reservations(d.1.get()));
|
||||
let reservation = create_rw_signal(ResSumWithItems::default());
|
||||
let reservation = RwSignal::new(ResSumWithItems::default());
|
||||
|
||||
view! {
|
||||
<ApproveDialog opener=app_opener reservation=reservation.read_only() />
|
||||
@@ -75,7 +75,7 @@ pub fn new_reservations(app_opener: DialogOpener, cancel_opener: DialogOpener) -
|
||||
{move || {
|
||||
res.get().map(|r| match r {
|
||||
Err(e) => {
|
||||
view! {<div>{trl("Something went wrong")}<br/>{e.to_string()}</div>}}
|
||||
view! {<div>{trl("Something went wrong")}<br/>{e.to_string()}</div>}.into_any()}
|
||||
Ok(r) => { match r {
|
||||
ApiResponse::Data(r) => {
|
||||
first.set(r.is_first());
|
||||
@@ -98,7 +98,7 @@ pub fn new_reservations(app_opener: DialogOpener, cancel_opener: DialogOpener) -
|
||||
let:item>
|
||||
{item.property.name}": "{item.reservation.from.to_string()}" - "{item.reservation.to.to_string()}<br/>
|
||||
</For>
|
||||
{trl("Customer: ")}{data.customer.full_name}", "<a href={format!("mailto:{}", data.customer.email)}>{data.customer.email}</a>", "{data.customer.phone}<br/>
|
||||
{trl("Customer: ")}{data.customer.full_name}", "<a href={format!("mailto:{}", data.customer.email)}>{data.customer.email.clone()}</a>", "{data.customer.phone}<br/>
|
||||
{move || {
|
||||
let note = data.summary.note.clone();
|
||||
let show = note.is_some() && !note.clone().unwrap().is_empty();
|
||||
@@ -109,7 +109,7 @@ pub fn new_reservations(app_opener: DialogOpener, cancel_opener: DialogOpener) -
|
||||
}
|
||||
}}
|
||||
{trl("Price: ")}{data.summary.price.to_string()}<br/>
|
||||
<table width="100%">
|
||||
<table style="width: 100%"><tbody>
|
||||
<tr>
|
||||
<td width="100%"></td>
|
||||
<td>
|
||||
@@ -135,15 +135,15 @@ pub fn new_reservations(app_opener: DialogOpener, cancel_opener: DialogOpener) -
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</tbody></table>
|
||||
<hr/>
|
||||
}
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
}
|
||||
}.into_any()
|
||||
}
|
||||
ApiResponse::Error(e) => {view! {<div>{trl(&e)}</div>}}}
|
||||
ApiResponse::Error(e) => {view! {<div>{trl(&e)}</div>}.into_any()}}
|
||||
}
|
||||
})
|
||||
}}
|
||||
|
||||
+11
-11
@@ -1,5 +1,5 @@
|
||||
use chrono::Weekday;
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use crate::backend::data::{DayHours, WeekHours};
|
||||
use crate::backend::opening_hours::get_hours;
|
||||
use crate::components::modal_box::DialogOpener;
|
||||
@@ -18,8 +18,8 @@ fn show_time(tm: &str) -> impl Fn() -> String {
|
||||
pub fn OpeningHours() -> impl IntoView {
|
||||
let editor = DialogOpener::new();
|
||||
let closing_delete = DialogOpener::new();
|
||||
let hours = create_blocking_resource(move || editor.visible(), move |_| {get_hours()});
|
||||
let hrs = create_rw_signal(WeekHours::default());
|
||||
let hours = Resource::new_blocking(move || editor.visible(), move |_| {get_hours()});
|
||||
let hrs = RwSignal::new(WeekHours::default());
|
||||
|
||||
view! {
|
||||
<EditHours opener=editor hours=hrs.read_only() />
|
||||
@@ -27,16 +27,16 @@ pub fn OpeningHours() -> impl IntoView {
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><i class="bx bxs-watch"></i>" "{trl("Opening hours")}</h5>
|
||||
<p class="card-text">
|
||||
<div class="card-text">
|
||||
<Transition fallback=move || view! {<p>{trl("Loading...")}</p> }>
|
||||
{move || {
|
||||
hours.get().map(|h| match h {
|
||||
Ok(h) => {
|
||||
let h = create_rw_signal(h);
|
||||
let d = create_rw_signal((0..7).collect::<Vec<u8>>());
|
||||
let h = RwSignal::new(h);
|
||||
let d = RwSignal::new((0..7).collect::<Vec<u8>>());
|
||||
view! {
|
||||
<div>
|
||||
<table class="table card-table">
|
||||
<table class="table card-table"><tbody>
|
||||
<For each=move || d.get() key=|day| *day
|
||||
children=move |day| {
|
||||
let week_day = Weekday::try_from(day).unwrap_or(Weekday::Mon);
|
||||
@@ -54,17 +54,17 @@ pub fn OpeningHours() -> impl IntoView {
|
||||
}
|
||||
}
|
||||
/>
|
||||
</table>
|
||||
</tbody></table>
|
||||
</div>
|
||||
}
|
||||
}.into_any()
|
||||
}
|
||||
Err(e) => {view! {<div><p>{trl("Error loading data")}</p>
|
||||
<p>{e.to_string()}</p></div>
|
||||
}}
|
||||
}.into_any()}
|
||||
})
|
||||
}}
|
||||
</Transition>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use crate::backend::data::User;
|
||||
use crate::backend::user::{get_user, UpdateProfile};
|
||||
use crate::components::data_form::DataForm;
|
||||
@@ -7,8 +7,8 @@ use crate::locales::trl;
|
||||
|
||||
#[component]
|
||||
pub fn ProfileEdit(user: ReadSignal<User>, opener: DialogOpener) -> impl IntoView {
|
||||
let update_user = create_server_action::<UpdateProfile>();
|
||||
let logged_in = create_resource(||(), move |_| get_user());
|
||||
let update_user = ServerAction::<UpdateProfile>::new();
|
||||
let logged_in = Resource::new(||(), move |_| get_user());
|
||||
|
||||
view! {
|
||||
<DataForm opener=opener action=update_user title="Edit profile">
|
||||
|
||||
+12
-12
@@ -1,4 +1,4 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use leptos_use::use_media_query;
|
||||
use crate::backend::data::{ApiResponse, ResProperty};
|
||||
use crate::backend::property::get_properties;
|
||||
@@ -11,13 +11,13 @@ use crate::pages::property_edit::PropertyEdit;
|
||||
#[component]
|
||||
pub fn properties() -> impl IntoView {
|
||||
let is_wide = use_media_query("(min-width: 500px)");
|
||||
let properties = create_rw_signal::<Vec<ResProperty>>(vec![]);
|
||||
let prop = create_rw_signal(ResProperty::default());
|
||||
let empty_prop = create_rw_signal(ResProperty::default());
|
||||
let properties = RwSignal::<Vec<ResProperty>>::new(vec![]);
|
||||
let prop = RwSignal::new(ResProperty::default());
|
||||
let empty_prop = RwSignal::new(ResProperty::default());
|
||||
let create_form = DialogOpener::new();
|
||||
let edit_form = DialogOpener::new();
|
||||
let delete_dlg = DialogOpener::new();
|
||||
let props = create_blocking_resource(move || create_form.visible() || edit_form.visible() || delete_dlg.visible(), move |_| get_properties());
|
||||
let props = Resource::new_blocking(move || create_form.visible() || edit_form.visible() || delete_dlg.visible(), move |_| get_properties());
|
||||
|
||||
view! {
|
||||
<PropertyEdit property=prop.read_only() opener=edit_form edit=true/>
|
||||
@@ -31,8 +31,8 @@ pub fn properties() -> impl IntoView {
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{trl("Name")}</th>
|
||||
{move || if is_wide.get() {view! {<th>{trl("Description")}</th>}}
|
||||
else {view! {<th></th>}} }
|
||||
{move || if is_wide.get() {view! {<th>{trl("Description")}</th>}.into_any()}
|
||||
else {view! {<th></th>}.into_any()} }
|
||||
<th>{trl("Price")}</th>
|
||||
<th>{trl("Actions")}</th>
|
||||
</tr>
|
||||
@@ -41,7 +41,7 @@ pub fn properties() -> impl IntoView {
|
||||
props.get().map(|u| match u {
|
||||
Err(e) => {
|
||||
view! {<tbody class="table-border-bottom-0">
|
||||
<tr><td colspan=4>{trl("Something went wrong")}<br/>{e.to_string()}</td></tr></tbody>}}
|
||||
<tr><td colspan=4>{trl("Something went wrong")}<br/>{e.to_string()}</td></tr></tbody>}.into_any()}
|
||||
Ok(u) => {
|
||||
match u {
|
||||
ApiResponse::Data(p) => {
|
||||
@@ -58,8 +58,8 @@ pub fn properties() -> impl IntoView {
|
||||
view! {
|
||||
<tr>
|
||||
<td>{data.name.clone()}</td>
|
||||
{move || if is_wide.get() {view! {<td>{data.description.clone()}</td>}}
|
||||
else {view! {<td></td>}} }
|
||||
{move || if is_wide.get() {view! {<td>{data.description.clone()}</td>}.into_any()}
|
||||
else {view! {<td></td>}.into_any()} }
|
||||
<td>{data.price.to_string()}</td>
|
||||
<td>
|
||||
<div class="dropdown">
|
||||
@@ -87,11 +87,11 @@ pub fn properties() -> impl IntoView {
|
||||
}}
|
||||
</For>
|
||||
</tbody>
|
||||
}
|
||||
}.into_any()
|
||||
},
|
||||
ApiResponse::Error(s) => {
|
||||
view! {<tbody class="table-border-bottom-0">
|
||||
<tr><td colspan=4>{trl(&s)}</td></tr></tbody>}
|
||||
<tr><td colspan=4>{trl(&s)}</td></tr></tbody>}.into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use crate::backend::data::ResProperty;
|
||||
use crate::backend::property::DeleteProperty;
|
||||
use crate::components::data_form::QuestionDialog;
|
||||
@@ -7,7 +7,7 @@ use crate::locales::trl;
|
||||
|
||||
#[component]
|
||||
pub fn property_delete(property: ReadSignal<ResProperty>, opener: DialogOpener) -> impl IntoView {
|
||||
let del_property = create_server_action::<DeleteProperty>();
|
||||
let del_property = ServerAction::<DeleteProperty>::new();
|
||||
|
||||
view! {
|
||||
<QuestionDialog opener=opener action=del_property title="Delete property">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use crate::backend::data::{ResProperty, SlotType};
|
||||
use crate::backend::property::{CreateProperty, EditProperty};
|
||||
use crate::components::data_form::DataForm;
|
||||
@@ -7,7 +7,7 @@ use crate::locales::trl;
|
||||
|
||||
#[component]
|
||||
fn form_inner(property: ReadSignal<ResProperty>) -> impl IntoView {
|
||||
let active_str = create_rw_signal("false".to_string());
|
||||
let active_str = RwSignal::new("false".to_string());
|
||||
view! {
|
||||
<input type="hidden" prop:value={move || property.get().id()} name="property[id]"/>
|
||||
<div class="row">
|
||||
@@ -79,9 +79,9 @@ fn form_inner(property: ReadSignal<ResProperty>) -> impl IntoView {
|
||||
<input type="hidden" prop:value=active_str name="property[active]"/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}.into_any()
|
||||
} else {
|
||||
view! {<div></div>}
|
||||
view! {<div></div>}.into_any()
|
||||
}
|
||||
}
|
||||
}}
|
||||
@@ -89,19 +89,19 @@ fn form_inner(property: ReadSignal<ResProperty>) -> impl IntoView {
|
||||
|
||||
#[component]
|
||||
pub fn property_edit(property: ReadSignal<ResProperty>, edit: bool, opener: DialogOpener) -> impl IntoView {
|
||||
let action_create = create_server_action::<CreateProperty>();
|
||||
let action_edit = create_server_action::<EditProperty>();
|
||||
let action_create = ServerAction::<CreateProperty>::new();
|
||||
let action_edit = ServerAction::<EditProperty>::new();
|
||||
|
||||
view! {
|
||||
{move ||
|
||||
if edit {
|
||||
view! {<DataForm opener=opener action=action_edit title="Edit property">
|
||||
<FormInner property=property/>
|
||||
</DataForm>}}
|
||||
</DataForm>}.into_any()}
|
||||
else {
|
||||
view! {<DataForm opener=opener action=action_create title="Create property">
|
||||
<FormInner property=property/>
|
||||
</DataForm>}}
|
||||
</DataForm>}.into_any()}
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
-29
@@ -1,7 +1,8 @@
|
||||
use chrono::{Duration, Local, NaiveDate, NaiveTime, Timelike};
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use leptos_captcha::{Captcha, pow_dispatch};
|
||||
use leptos_router::*;
|
||||
use leptos::form::{ActionForm, FromFormData};
|
||||
use leptos::ev::SubmitEvent;
|
||||
use rust_decimal::Decimal;
|
||||
use crate::backend::appearance::get_appearance;
|
||||
use crate::backend::customer::get_remembered;
|
||||
@@ -36,7 +37,7 @@ fn time_selector(
|
||||
match property.slot {
|
||||
SlotType::Quarter => {
|
||||
let mut ret: Vec<TmCheck> = vec![];
|
||||
logging::log!("quarter");
|
||||
leptos::logging::log!("quarter");
|
||||
for n in 0..(h.to() - h.from()).num_minutes() * 4 / 60 {
|
||||
ret.push(TmCheck {
|
||||
from: h.from() + Duration::minutes(n * 15),
|
||||
@@ -47,7 +48,7 @@ fn time_selector(
|
||||
}
|
||||
SlotType::Half => {
|
||||
let mut ret: Vec<TmCheck> = vec![];
|
||||
logging::log!("half");
|
||||
leptos::logging::log!("half");
|
||||
for n in 0..(h.to() - h.from()).num_minutes() * 2 / 60 {
|
||||
ret.push(TmCheck {
|
||||
from: h.from() + Duration::minutes(n * 30),
|
||||
@@ -110,22 +111,22 @@ fn time_selector(
|
||||
|
||||
#[component]
|
||||
pub fn Public() -> impl IntoView {
|
||||
let day = create_rw_signal(NaiveDate::default());
|
||||
let form_data = create_blocking_resource(move || day.get(), move |d| get_public_form_data(d));
|
||||
let slots = create_rw_signal::<Vec<String>>(vec![]);
|
||||
let price = create_rw_signal(Decimal::from(0));
|
||||
let cr_reservation = create_server_action::<CreateReservation>();
|
||||
let day = RwSignal::new(NaiveDate::default());
|
||||
let form_data = Resource::new_blocking(move || day.get(), move |d| get_public_form_data(d));
|
||||
let slots = RwSignal::<Vec<String>>::new(vec![]);
|
||||
let price = RwSignal::new(Decimal::from(0));
|
||||
let cr_reservation = ServerAction::<CreateReservation>::new();
|
||||
let validator = Validator::new();
|
||||
let invalid_dlg = DialogOpener::new();
|
||||
let result_dlg = DialogOpener::new();
|
||||
let result = cr_reservation.value();
|
||||
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());
|
||||
let result = Signal::derive(move || cr_reservation.value().get());
|
||||
let is_pending = RwSignal::new(None);
|
||||
let active_str = RwSignal::new("true".to_string());
|
||||
let get_customer = Resource::new_blocking(||(), move |_| get_remembered());
|
||||
let appearance = Resource::new_blocking(||(), move |_| get_appearance());
|
||||
let customer = RwSignal::new(Customer::default());
|
||||
|
||||
create_effect(move |_| {
|
||||
Effect::new(move |_| {
|
||||
day.set(Local::now().date_naive());
|
||||
});
|
||||
|
||||
@@ -139,36 +140,37 @@ pub fn Public() -> impl IntoView {
|
||||
captcha_state=is_pending.read_only()/>
|
||||
|
||||
<Transition fallback=move || view! {<p>{trl("Loading...")}</p> }>
|
||||
{
|
||||
{move || {
|
||||
appearance.get().map(|a| match a {
|
||||
Ok(a) => {
|
||||
let app = a.clone();
|
||||
let has_banner = a.banner.is_some();
|
||||
let title = a.title.clone().unwrap_or_default();
|
||||
view! {
|
||||
<div>
|
||||
<Show when=move || a.clone().banner.is_some()>
|
||||
<Show when=move || has_banner>
|
||||
<div class="header_banner">
|
||||
<h1 class="header_banner">{app.title.clone().unwrap_or_default()}</h1>
|
||||
<h1 class="header_banner">{title.clone()}</h1>
|
||||
</div>
|
||||
</Show>
|
||||
<div>
|
||||
<div class="header_banner_text" inner_html={app.text.unwrap_or("".to_string())}>
|
||||
<div class="header_banner_text" inner_html={a.text.unwrap_or("".to_string())}>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}.into_any()
|
||||
},
|
||||
Err(e) => {view! {<div><p>{trl("Error loading data")}</p>
|
||||
<p>{e.to_string()}</p></div>
|
||||
}}
|
||||
}.into_any()}
|
||||
})
|
||||
}
|
||||
}}
|
||||
</Transition>
|
||||
|
||||
<div class="card-body">
|
||||
<ActionForm
|
||||
on:submit=move |ev| {
|
||||
cr_reservation.set_pending(true);
|
||||
// Capture phase, so the form is validated and the captcha solved before it is dispatched
|
||||
on:submit:capture=move |ev: SubmitEvent| {
|
||||
if let Ok(mut act) = CreateReservation::from_event(&ev) {
|
||||
validator.check(act.entity(), &ev);
|
||||
|
||||
@@ -219,7 +221,7 @@ pub fn Public() -> impl IntoView {
|
||||
});
|
||||
form_data.get().map(|u| match u {
|
||||
Err(e) => {
|
||||
view! {<div>{e.to_string()}</div>}}
|
||||
view! {<div>{e.to_string()}</div>}.into_any()}
|
||||
Ok(u) => {
|
||||
match u {
|
||||
ApiResponse::Data(p) => {
|
||||
@@ -252,10 +254,10 @@ pub fn Public() -> impl IntoView {
|
||||
value={data}/>
|
||||
</For>
|
||||
</div>
|
||||
}
|
||||
}.into_any()
|
||||
},
|
||||
ApiResponse::Error(s) => {
|
||||
view! {<div>{trl(&s)}</div>}
|
||||
view! {<div>{trl(&s)}</div>}.into_any()
|
||||
}
|
||||
}}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use chrono::{Local, NaiveDate};
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use rust_decimal::Decimal;
|
||||
use crate::backend::data::ApiResponse;
|
||||
use crate::components::modal_box::{DialogOpener, ModalBody, ModalDialog, ModalFooter};
|
||||
@@ -29,7 +29,8 @@ pub fn res_error(opener: DialogOpener, validator: Validator) -> impl IntoView {
|
||||
#[component]
|
||||
pub fn res_saved(
|
||||
opener: DialogOpener,
|
||||
save_result: RwSignal<Option<Result<ApiResponse<NaiveDate>, ServerFnError>>>,
|
||||
#[prop(into)]
|
||||
save_result: Signal<Option<Result<ApiResponse<NaiveDate>, ServerFnError>>>,
|
||||
day: RwSignal<NaiveDate>,
|
||||
price: WriteSignal<Decimal>,
|
||||
slots: WriteSignal<Vec<String>>,
|
||||
@@ -68,7 +69,7 @@ pub fn res_saved(
|
||||
</ModalFooter>
|
||||
</ModalDialog>
|
||||
</div>
|
||||
}
|
||||
}.into_any()
|
||||
},
|
||||
ApiResponse::Error(err) => {
|
||||
view! {
|
||||
@@ -88,7 +89,7 @@ pub fn res_saved(
|
||||
</ModalFooter>
|
||||
</ModalDialog>
|
||||
</div>
|
||||
}
|
||||
}.into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,11 +111,11 @@ pub fn res_saved(
|
||||
</ModalFooter>
|
||||
</ModalDialog>
|
||||
</div>
|
||||
}
|
||||
}.into_any()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
view! {<div></div>}
|
||||
view! {<div></div>}.into_any()
|
||||
}
|
||||
}}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use crate::locales::trl;
|
||||
use crate::pages::closing_days::ClosingDays;
|
||||
use crate::pages::company_info::CompanyInfo;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use crate::backend::data::{ApiResponse, ResSumWithItems};
|
||||
use crate::backend::reservation::get_next_reservations;
|
||||
use crate::components::modal_box::DialogOpener;
|
||||
@@ -9,9 +9,9 @@ use crate::pages::new_reservations::CancelDialog;
|
||||
|
||||
#[component]
|
||||
pub fn next_reservations(app_opener: DialogOpener, cancel_opener: DialogOpener) -> impl IntoView {
|
||||
let res = create_blocking_resource( move || app_opener.visible() || cancel_opener.visible(), move |_| get_next_reservations());
|
||||
let res = Resource::new_blocking( move || app_opener.visible() || cancel_opener.visible(), move |_| get_next_reservations());
|
||||
let menu = MenuOpener::new();
|
||||
let reservation = create_rw_signal(ResSumWithItems::default());
|
||||
let reservation = RwSignal::new(ResSumWithItems::default());
|
||||
|
||||
view! {
|
||||
<CancelDialog opener=cancel_opener reservation=reservation.read_only()/>
|
||||
@@ -22,7 +22,7 @@ pub fn next_reservations(app_opener: DialogOpener, cancel_opener: DialogOpener)
|
||||
{move || {
|
||||
res.get().map(|r| match r {
|
||||
Err(e) => {
|
||||
view! {<div>{trl("Something went wrong")}<br/>{e.to_string()}</div>}}
|
||||
view! {<div>{trl("Something went wrong")}<br/>{e.to_string()}</div>}.into_any()}
|
||||
Ok(r) => { match r {
|
||||
ApiResponse::Data(r) => {
|
||||
view! {
|
||||
@@ -40,7 +40,7 @@ pub fn next_reservations(app_opener: DialogOpener, cancel_opener: DialogOpener)
|
||||
let:item>
|
||||
{item.property.name}": "{item.reservation.from.to_string()}" - "{item.reservation.to.to_string()}<br/>
|
||||
</For>
|
||||
{trl("Customer: ")}{data.customer.full_name}", "<a href={format!("mailto:{}", data.customer.email)}>{data.customer.email}</a>", "{data.customer.phone}<br/>
|
||||
{trl("Customer: ")}{data.customer.full_name}", "<a href={format!("mailto:{}", data.customer.email)}>{data.customer.email.clone()}</a>", "{data.customer.phone}<br/>
|
||||
{move || {
|
||||
let note = data.summary.note.clone();
|
||||
let show = note.is_some() && !note.clone().unwrap().is_empty();
|
||||
@@ -51,7 +51,7 @@ pub fn next_reservations(app_opener: DialogOpener, cancel_opener: DialogOpener)
|
||||
}
|
||||
}}
|
||||
{trl("Price: ")}{data.summary.price.to_string()}<br/>
|
||||
<table width="100%">
|
||||
<table style="width: 100%"><tbody>
|
||||
<tr>
|
||||
<td width="100%"></td>
|
||||
<td>
|
||||
@@ -72,15 +72,15 @@ pub fn next_reservations(app_opener: DialogOpener, cancel_opener: DialogOpener)
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</tbody></table>
|
||||
<hr/>
|
||||
}
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
}
|
||||
}.into_any()
|
||||
}
|
||||
ApiResponse::Error(e) => {view! {<div>{trl(&e)}</div>}}}
|
||||
ApiResponse::Error(e) => {view! {<div>{trl(&e)}</div>}.into_any()}}
|
||||
}
|
||||
})
|
||||
}}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use crate::backend::data::User;
|
||||
use crate::backend::user::DeleteUser;
|
||||
use crate::components::data_form::QuestionDialog;
|
||||
@@ -7,7 +7,7 @@ use crate::locales::trl;
|
||||
|
||||
#[component]
|
||||
pub fn user_delete(user: ReadSignal<User>, opener: DialogOpener) -> impl IntoView {
|
||||
let del_user = create_server_action::<DeleteUser>();
|
||||
let del_user = ServerAction::<DeleteUser>::new();
|
||||
|
||||
view! {
|
||||
<QuestionDialog opener=opener action=del_user title="Delete user">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use crate::backend::user::CreateUser;
|
||||
use crate::components::data_form::DataForm;
|
||||
use crate::components::modal_box::DialogOpener;
|
||||
@@ -6,7 +6,7 @@ use crate::locales::trl;
|
||||
|
||||
#[component]
|
||||
pub fn user_edit(opener: DialogOpener) -> impl IntoView {
|
||||
let create_usr = create_server_action::<CreateUser>();
|
||||
let create_usr = ServerAction::<CreateUser>::new();
|
||||
view! {
|
||||
<DataForm opener=opener action=create_usr title="Create user">
|
||||
//<input type="hidden" value={move || company.get().id()} name="company[id]"/>
|
||||
|
||||
+14
-14
@@ -1,4 +1,4 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use crate::backend::data::{ApiResponse, User};
|
||||
use crate::backend::user::get_users;
|
||||
use crate::components::modal_box::DialogOpener;
|
||||
@@ -16,10 +16,10 @@ pub fn users() -> impl IntoView {
|
||||
let profile_editor = DialogOpener::new();
|
||||
let pwd_dialog = DialogOpener::new();
|
||||
let delete_dialog = DialogOpener::new();
|
||||
let users = create_blocking_resource(
|
||||
let users = Resource::new_blocking(
|
||||
move || editor.visible() || profile_editor.visible() || delete_dialog.visible(), move |_| {get_users()});
|
||||
let (usr, set_usr) = create_signal::<Vec<User>>(vec![]);
|
||||
let (profile, set_profile) = create_signal(User::default());
|
||||
let (usr, set_usr) = signal::<Vec<User>>(vec![]);
|
||||
let (profile, set_profile) = signal(User::default());
|
||||
let is_wide = use_media_query("(min-width: 500px)");
|
||||
|
||||
view! {
|
||||
@@ -35,8 +35,8 @@ pub fn users() -> impl IntoView {
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{trl("Username")}</th>
|
||||
{move || if is_wide.get() {view! {<th>{trl("Full name")}</th>}}
|
||||
else {view! {<th></th>}} }
|
||||
{move || if is_wide.get() {view! {<th>{trl("Full name")}</th>}.into_any()}
|
||||
else {view! {<th></th>}.into_any()} }
|
||||
<th>{trl("Admin")}</th>
|
||||
<th>{trl("Actions")}</th>
|
||||
</tr>
|
||||
@@ -50,7 +50,7 @@ pub fn users() -> impl IntoView {
|
||||
e.to_string()
|
||||
};
|
||||
view! {<tbody class="table-border-bottom-0">
|
||||
<tr><td colspan=4>{trl(&err)}</td></tr></tbody>}}
|
||||
<tr><td colspan=4>{trl(&err)}</td></tr></tbody>}.into_any()}
|
||||
Ok(u) => {
|
||||
match u {
|
||||
ApiResponse::Data(u) => {
|
||||
@@ -66,11 +66,11 @@ pub fn users() -> impl IntoView {
|
||||
let full_name = user.full_name.clone().unwrap_or("".to_string());
|
||||
view! {
|
||||
<tr>
|
||||
<td>{&user.login}</td>
|
||||
{move || if is_wide.get() {view! {<td>{full_name.clone()}</td>}}
|
||||
else {view! {<td></td>}} }
|
||||
<td>{if user.admin {view! {<i class="bx bx-check"></i>}}
|
||||
else {view! {<i></i>}}}</td>
|
||||
<td>{user.login.clone()}</td>
|
||||
{move || if is_wide.get() {view! {<td>{full_name.clone()}</td>}.into_any()}
|
||||
else {view! {<td></td>}.into_any()} }
|
||||
<td>{if user.admin {view! {<i class="bx bx-check"></i>}.into_any()}
|
||||
else {view! {<i></i>}.into_any()}}</td>
|
||||
<td>
|
||||
<div class="dropdown">
|
||||
<button type="button" class="btn p-0 dropdown-toggle hide-arrow"
|
||||
@@ -101,10 +101,10 @@ pub fn users() -> impl IntoView {
|
||||
</tr>
|
||||
}
|
||||
}/></tbody>
|
||||
}
|
||||
}.into_any()
|
||||
}
|
||||
ApiResponse::Error(e) => {view! {<tbody class="table-border-bottom-0">
|
||||
<tr><td colspan=4>{trl(&e)}</td></tr></tbody>}}
|
||||
<tr><td colspan=4>{trl(&e)}</td></tr></tbody>}.into_any()}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
use leptos::*;
|
||||
use leptos::prelude::*;
|
||||
use validator::Validate;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
@@ -13,9 +13,9 @@ pub struct Validator {
|
||||
|
||||
impl Validator {
|
||||
pub fn new() -> Self {
|
||||
let (valid, set_valid) = create_signal(true);
|
||||
//let (message, set_message) = create_signal(None);
|
||||
let (messages, set_messages) = create_signal(None);
|
||||
let (valid, set_valid) = signal(true);
|
||||
//let (message, set_message) = signal(None);
|
||||
let (messages, set_messages) = signal(None);
|
||||
Self {
|
||||
//message,
|
||||
//set_message,
|
||||
|
||||
Reference in New Issue
Block a user