CI / Build and push docker image (push) Successful in 1m54s
Add a web app manifest and an install-only service worker (no offline caching: live state always comes from the server and hashed asset names change every build), registered from the WASM bundle. The worker must live at the site root for its scope to cover the app, and Granian only serves /static and /assets, so tavolo/static.py answers /manifest.webmanifest and /sw.js from STATIC_DIR with the content types browsers require; index.html gains the manifest link, icons, theme-color and Apple standalone meta tags. Icons are committed under web/assets/icons/ with vector sources and a make-icons.sh regeneration script, mirroring the card art workflow.
98 lines
3.1 KiB
Rust
98 lines
3.1 KiB
Rust
//! Application entry point.
|
|
mod api;
|
|
mod auth;
|
|
mod components;
|
|
mod model;
|
|
mod pages;
|
|
mod pwa;
|
|
mod ws;
|
|
|
|
use sycamore::prelude::*;
|
|
use sycamore_router::{navigate, HistoryIntegration, Route, Router};
|
|
use wasm_bindgen_futures::spawn_local;
|
|
|
|
use pages::game::GamePage;
|
|
use pages::history::HistoryPage;
|
|
use pages::leaderboard::LeaderboardPage;
|
|
use pages::lobby::LobbyPage;
|
|
use pages::login::LoginPage;
|
|
|
|
#[derive(Route, Clone)]
|
|
enum AppRoutes {
|
|
#[to("/")]
|
|
Lobby,
|
|
#[to("/login")]
|
|
Login,
|
|
#[to("/game/<id>")]
|
|
Game { id: String },
|
|
#[to("/history")]
|
|
History,
|
|
#[to("/leaderboard")]
|
|
Leaderboard,
|
|
#[not_found]
|
|
NotFound,
|
|
}
|
|
|
|
/// After the OIDC callback lands the SPA back on the configured
|
|
/// post-login redirect, forward the user to the page they were on before
|
|
/// being sent to the login page (see [`auth`]) — but only once a session
|
|
/// exists, so a cancelled login never bounces into a stale page.
|
|
fn restore_page_after_login() {
|
|
let Some(path) = auth::saved_return_to() else {
|
|
return;
|
|
};
|
|
spawn_local(async move {
|
|
match api::me().await {
|
|
Ok(Some(_)) => {
|
|
auth::clear_return_to();
|
|
if auth::current_path() != path {
|
|
navigate(&path);
|
|
}
|
|
}
|
|
// Not logged in: keep the target for the next successful
|
|
// login (a reload of the login page must not lose it).
|
|
Ok(None) => {}
|
|
// The probe failed (offline?): keep the target, too.
|
|
Err(_) => {}
|
|
}
|
|
});
|
|
}
|
|
|
|
fn main() {
|
|
console_error_panic_hook::set_once();
|
|
sycamore::render(|| {
|
|
view! {
|
|
Router(
|
|
integration=HistoryIntegration::new(),
|
|
view=|route: ReadSignal<AppRoutes>| {
|
|
view! {
|
|
div(class="app") {
|
|
(match route.get_clone() {
|
|
AppRoutes::Lobby => view! { LobbyPage() },
|
|
AppRoutes::Login => view! { LoginPage() },
|
|
AppRoutes::Game { id } => view! { GamePage(id=id) },
|
|
AppRoutes::History => view! { HistoryPage() },
|
|
AppRoutes::Leaderboard => view! { LeaderboardPage() },
|
|
AppRoutes::NotFound => view! {
|
|
div(class="status-card") {
|
|
div(class="card") {
|
|
div(class="card-body p-4") {
|
|
h1(class="h4 mb-3") { "Page not found" }
|
|
a(class="btn btn-warning", href="/") {
|
|
"Back to lobby"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
})
|
|
}
|
|
}
|
|
}
|
|
)
|
|
}
|
|
});
|
|
pwa::register();
|
|
restore_page_after_login();
|
|
}
|