diff --git a/ht_booking/assets/views/base.html b/ht_booking/assets/views/base.html index 54b62dd..2caa442 100644 --- a/ht_booking/assets/views/base.html +++ b/ht_booking/assets/views/base.html @@ -67,19 +67,30 @@ } } - /* Mobile: a backdrop behind an open navbar dropdown. It absorbs taps - outside the menu — closing the dropdown rather than letting the tap - reach the page — and dims the page to show the menu is modal. It sits - below the dropdown content (z-50) so the menu items stay tappable. */ + /* Mobile: a dimming backdrop behind an open navbar dropdown, driven by + CSS alone. `:has()` shows it whenever a dropdown holds focus; a tap + outside the menu blurs the trigger, which closes the dropdown. The + delayed `visibility` transition keeps the backdrop hit-testable for a + beat after that tap, so the tap lands on the backdrop instead of + falling through to the page. It sits below the dropdown content + (z-50) so the menu items stay tappable. */ #nav-backdrop { display: none; } @media (max-width: 767px) { #nav-backdrop { + display: block; position: fixed; inset: 0; z-index: 40; background-color: rgba(0, 0, 0, 0.25); + opacity: 0; + visibility: hidden; + transition: opacity 0.15s ease, visibility 0s linear 0.2s; + } + .navbar:has(.dropdown:focus-within) ~ #nav-backdrop { + opacity: 1; + visibility: visible; + transition: opacity 0.15s ease, visibility 0s; } - #nav-backdrop.nav-backdrop--on { display: block; } } {% block head %}{% endblock head %} @@ -139,11 +150,15 @@ + +
+
@@ -229,38 +245,6 @@ {% block content %}{% endblock content %} - {% block js %}{% endblock js %} diff --git a/ht_booking/src/controllers/calendar.rs b/ht_booking/src/controllers/calendar.rs index 4b4fde1..99e57a3 100644 --- a/ht_booking/src/controllers/calendar.rs +++ b/ht_booking/src/controllers/calendar.rs @@ -6,6 +6,8 @@ //! court. Booked slots are coloured; free slots are blank. The same grid is //! reused by the admin dashboard with `is_admin = true`. +use axum::http::{header, HeaderMap}; +use axum::response::Redirect; use axum_extra::extract::cookie::CookieJar; use chrono::{Datelike, Duration, NaiveDate, Utc}; use loco_rs::prelude::*; @@ -312,6 +314,46 @@ pub async fn index( format::render().view(&v, "calendar/week.html", &page) } -pub fn routes() -> Routes { - Routes::new().add("/", get(index)) +#[derive(Debug, Deserialize)] +pub struct LangForm { + pub lang: String, +} + +/// Switches the UI language. The navbar's language buttons post here; the +/// `lang` cookie is set server-side and the visitor is bounced back to the +/// page they came from. This replaces the old client-side `setLang` script. +#[debug_handler] +pub async fn set_lang(headers: HeaderMap, Form(form): Form) -> Result { + // Only the two supported languages; anything else falls back to Slovak, + // matching `current_lang`. + let lang = if form.lang == "en" { "en" } else { "sk" }; + let cookie = format!("lang={lang}; Path=/; Max-Age=31536000; SameSite=Lax"); + Ok(( + [(header::SET_COOKIE, cookie)], + Redirect::to(&back_path(&headers)), + ) + .into_response()) +} + +/// On-site path of the page that submitted the form, read from `Referer`. +/// Scheme and host are stripped so a stale or foreign header can only ever +/// bounce the visitor to a path on this site, never off it. +fn back_path(headers: &HeaderMap) -> String { + let raw = headers + .get(header::REFERER) + .and_then(|v| v.to_str().ok()) + .unwrap_or("/"); + match raw.split_once("://") { + Some((_, rest)) => match rest.find('/') { + Some(i) => rest[i..].to_string(), + None => "/".to_string(), + }, + None => raw.to_string(), + } +} + +pub fn routes() -> Routes { + Routes::new() + .add("/", get(index)) + .add("/lang", post(set_lang)) }