Files

5.3 KiB

paths
paths
src/pages/**
src/components/**
src/locales/**
src/app.rs
src/validator.rs
style/**
assets/css/**

Frontend rules

Components

  • 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 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, 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.

Translations

  • Every user-visible string goes through trl("English text"); never hard-code Czech (or any text) in a view. The English text is the key and the fallback.
  • When you add or change a key, add the Czech translation to the cs map in src/locales/catalogues.rs. This includes validation messages and ApiResponse::Error strings from the backend.
  • trl() returns a closure — use it directly as {trl("...")} or attribute value; call it (trl("...")()) when a String is needed.
  • Dates shown to users go through loc_date(), weekday names through show_day().

Styling

  • UI is the Bootstrap-based admin template in assets/vendor/ plus Boxicons (<i class="bx bx-...">). Reuse its classes (card, table, btn btn-primary, form-control, row/col) before writing CSS.
  • Custom styles go to a CSS file in assets/css/ and must be linked with <Link rel="stylesheet"> in src/components/header.rs (order matters: after the vendor files to override them). style/main.scss is not linked anywhere and has no effect. Never edit assets/vendor/ (the one app-owned file there is assets/vendor/css/control.css).
  • The admin portal has a light and a dark theme (src/components/theme.rs, switch in the navbar: auto / light / dark, stored in the theme cookie). Dark colours are overrides in assets/css/dark.css, which AdminPortal links with a media attribute — the public page and the login page never load it. When you add or restyle something in the admin portal, check it in both themes and add the dark override there; don't hard-code light colours (bg-light, bg-white, inline color) in admin views.
  • Charts are drawn on the server, so the chart server fns take a dark flag from Theme::is_dark().
  • The public page banner style template is assets/banner.css. On start it is copied to the data directory, and the appearance upload generates banner-<random>.css from it by replacing the /*bg-img*/ marker — keep that marker intact.
  • Stylesheets and meta tags are declared in src/components/header.rs.