From 811d29b9ec5c1c7ffc36b8a5f2c51ea31e6fcbb6 Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Sun, 3 Aug 2025 12:11:02 +0200 Subject: [PATCH] add error/succ messages --- Cargo.lock | 124 +++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + src/game.rs | 133 +++++++++++++++++++++++++++------------------------ src/index.rs | 5 +- src/main.rs | 20 ++++++-- src/page.rs | 104 +++++++++++++++++++++++----------------- 6 files changed, 272 insertions(+), 116 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7339c59..0781b8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -53,6 +53,17 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atoi" version = "2.0.0" @@ -145,6 +156,22 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum-messages" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d67ce6e7bc1e1e71f2a4e86d418045a29c63c4ebb631f3d9bb2f81c4958ea391" +dependencies = [ + "axum-core", + "http", + "parking_lot", + "serde", + "serde_json", + "tower", + "tower-sessions-core", + "tracing", +] + [[package]] name = "backtrace" version = "0.3.75" @@ -381,6 +408,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -481,6 +509,20 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -525,6 +567,17 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -545,6 +598,7 @@ checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1007,6 +1061,7 @@ checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", + "serde", ] [[package]] @@ -2135,6 +2190,22 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-cookies" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "151b5a3e3c45df17466454bb74e9ecedecc955269bdedbf4d150dfa393b55a36" +dependencies = [ + "axum-core", + "cookie", + "futures-util", + "http", + "parking_lot", + "pin-project-lite", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-http" version = "0.6.6" @@ -2173,6 +2244,57 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +[[package]] +name = "tower-sessions" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a05911f23e8fae446005fe9b7b97e66d95b6db589dc1c4d59f6a2d4d4927d3" +dependencies = [ + "async-trait", + "http", + "time", + "tokio", + "tower-cookies", + "tower-layer", + "tower-service", + "tower-sessions-core", + "tower-sessions-memory-store", + "tracing", +] + +[[package]] +name = "tower-sessions-core" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce8cce604865576b7751b7a6bc3058f754569a60d689328bb74c52b1d87e355b" +dependencies = [ + "async-trait", + "axum-core", + "base64", + "futures", + "http", + "parking_lot", + "rand", + "serde", + "serde_json", + "thiserror", + "time", + "tokio", + "tracing", +] + +[[package]] +name = "tower-sessions-memory-store" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb05909f2e1420135a831dd5df9f5596d69196d0a64c3499ca474c4bd3d33242" +dependencies = [ + "async-trait", + "time", + "tokio", + "tower-sessions-core", +] + [[package]] name = "tracing" version = "0.1.41" @@ -2421,6 +2543,7 @@ version = "0.1.0" dependencies = [ "axum", "axum-extra", + "axum-messages", "chrono", "maud", "rust-i18n", @@ -2428,6 +2551,7 @@ dependencies = [ "sqlx", "tokio", "tower-http", + "tower-sessions", "uuid", ] diff --git a/Cargo.toml b/Cargo.toml index d299c94..d9d2587 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] axum = "0.8" axum-extra = { version = "0.10", features = ["cookie"] } +axum-messages = "0.8.0" chrono = { version = "0.4.41", features = ["serde"] } maud = { version = "0.27", features = ["axum"] } rust-i18n = "3.1.5" @@ -13,4 +14,5 @@ serde = { version = "1", features = ["derive"] } sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite", "macros", "chrono"] } tokio = { version = "1", features = ["macros", "rt-multi-thread"] } tower-http = { version = "0.6", features = ["fs"] } +tower-sessions = "0.14.0" uuid = { version = "1.17", features = ["v4", "serde"] } diff --git a/src/game.rs b/src/game.rs index 8867b57..27db6e7 100644 --- a/src/game.rs +++ b/src/game.rs @@ -1,4 +1,4 @@ -use crate::{language::language, page::new, Backend}; +use crate::{language::language, page::Page, Backend, NameUpdateError}; use axum::{ extract::{Path, State}, http::HeaderMap, @@ -7,6 +7,7 @@ use axum::{ Form, Router, }; use axum_extra::extract::CookieJar; +use axum_messages::Messages; use maud::{html, Markup, PreEscaped}; use serde::Deserialize; use std::sync::Arc; @@ -15,6 +16,7 @@ use uuid::Uuid; async fn index( State(backend): State>, cookies: CookieJar, + messages: Messages, headers: HeaderMap, ) -> Response { let (cookies, req) = backend.client_full(cookies, &headers).await; @@ -24,66 +26,65 @@ async fn index( let amount_total_cameras = backend.amount_total_cameras().await; let highscore = backend.highscore().await; - let markup = new( - html! { - hgroup { - h1 { "Who finds the most cameras?" } - } - p { - mark { "TODO: Explanation of AEF / digital shadows / search game" } - } + let mut page = Page::new(req.lang); + page.messages(messages); + let markup = page.content(html! { + hgroup { + h1 { "Who finds the most cameras?" } + } + p { + mark { "TODO: Explanation of AEF / digital shadows / search game" } + } - div.mb-sm { - (client.get_display_name()) - ", do you want to be named something different? No worries, change here 👇" - } + div.mb-sm { + (client.get_display_name()) + ", do you want to be named something different? No worries, change here 👇" + } - form action="/name" method="post" { - fieldset role="group" { - input - name="name" - placeholder="✨ Your new name starts here ✨" - aria-label="Name"; - input type="submit" value="Save"; + form action="/name" method="post" { + fieldset role="group" { + input + name="name" + placeholder="✨ Your new name starts here ✨" + aria-label="Name"; + input type="submit" value="Save"; + } + } + + p.mb-0 { + "You have found " + (sightings.len()) + "/" + (amount_total_cameras) + " cameras:" + progress value=(sightings.len()) max=(amount_total_cameras); + } + + ol.flex.small { + @for sighting in &sightings { + li { + (sighting.camera.name) } } + } - p.mb-0 { - "You have found " - (sightings.len()) - "/" - (amount_total_cameras) - " cameras:" - progress value=(sightings.len()) max=(amount_total_cameras); - } - - ol.flex.small { - @for (idx, sighting) in sightings.iter().enumerate() { - li { - (sighting.camera.name) - } - } - } - - p { - h2 { "Highscore" } - ul.iterated { - @for rank in highscore { - li.card { - span { - span.font-headline.rank.text-muted { (rank.rank) "." } - @if rank.client == client { (PreEscaped("")) } - (rank.client.get_display_name()) - @if rank.client == client { (PreEscaped("")) } - } - span.font-headline.font-lg { (rank.amount) (PreEscaped(" ")) "📸" } + p { + h2 { "Highscore" } + ul.iterated { + @for rank in highscore { + li.card { + span { + span.font-headline.rank.text-muted { (rank.rank) "." } + @if rank.client == client { (PreEscaped("")) } + (rank.client.get_display_name()) + @if rank.client == client { (PreEscaped("")) } } + span.font-headline.font-lg { (rank.amount) (PreEscaped(" ")) "📸" } } } } - }, - req.lang, - ); + } + }); (cookies, markup).into_response() } @@ -92,6 +93,7 @@ async fn game( State(backend): State>, cookies: CookieJar, headers: HeaderMap, + messages: Messages, Path(uuid): Path, ) -> Result { let (cookies, client) = backend.client(cookies).await; @@ -104,20 +106,22 @@ async fn game( return Err(not_found(cookies, headers).await.into_response()); }; - let succ = backend.client_found_camera(&client, &camera).await; - // TODO: show succ/err based on succ + if backend.client_found_camera(&client, &camera).await { + messages.info(format!("found-cam|{}", camera.name)); + } else { + messages.info(format!( + "err|Try to find a new camera!|You have already collected this camera.|" + )); + } Ok(Redirect::to("/game")) } async fn not_found(cookies: CookieJar, headers: HeaderMap) -> Markup { let lang = language(&cookies, &headers); - new( - html! { - h1 { "uups" } - }, - lang, - ) + Page::new(lang).content(html! { + h1 { "uups" } + }) } #[derive(Deserialize)] @@ -128,13 +132,16 @@ struct NameForm { async fn set_name( State(backend): State>, cookies: CookieJar, + messages: Messages, Form(form): Form, ) -> Response { let (cookies, client) = backend.client(cookies).await; - // Update the client's name in the backend - // TODO: handle succ/err msg - let _ = backend.set_client_name(&client, &form.name).await; + match backend.set_client_name(&client, &form.name).await { + Ok(()) => messages.info("set-name-succ"), + Err(NameUpdateError::TooShort(expected, actual)) => messages.info(format!("err|That's too little!|We need more information about you. Give us at least {expected} characters for you new name!|You sent us {actual} characters.")), + Err(NameUpdateError::TooLong(expected, actual)) => messages.info(format!("err|That's too much!|We only live in (20)25, so please use less than {expected} characters for your new name.|You sent us {actual} characters.")), + }; // Redirect back to the game page (cookies, Redirect::to("/game")).into_response() diff --git a/src/index.rs b/src/index.rs index 6d8b21c..8b03d37 100644 --- a/src/index.rs +++ b/src/index.rs @@ -8,10 +8,7 @@ pub(super) async fn index(cookies: CookieJar, headers: HeaderMap) -> Markup { rust_i18n::set_locale(lang.to_locale()); - let mut page = Page::new(lang); - page.succ("Yeah! That worked!".into()); - page.err("Damn...".into()); - + let page = Page::new(lang); page.content( html! { h1 { (t!("digital_shadows")) } diff --git a/src/main.rs b/src/main.rs index 8409c0c..4401b3a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,11 @@ use crate::model::client::Client; use axum::{http::HeaderMap, routing::get, Router}; use axum_extra::extract::{cookie::Cookie, CookieJar}; +use axum_messages::MessagesManagerLayer; use sqlx::{pool::PoolOptions, sqlite::SqliteConnectOptions, SqlitePool}; use std::{fmt::Display, str::FromStr, sync::Arc}; use tower_http::services::ServeDir; +use tower_sessions::{MemoryStore, SessionManagerLayer}; use uuid::Uuid; #[macro_use] @@ -68,6 +70,11 @@ struct Req { lang: Language, } +pub(crate) enum NameUpdateError { + TooLong(usize, usize), + TooShort(usize, usize), +} + impl Backend { async fn client(&self, cookies: CookieJar) -> (CookieJar, Client) { let existing_uuid = cookies @@ -91,12 +98,12 @@ impl Backend { (cookies, Req { client, lang }) } - async fn set_client_name(&self, client: &Client, name: &str) -> Result<(), String> { + async fn set_client_name(&self, client: &Client, name: &str) -> Result<(), NameUpdateError> { if name.len() > 25 { - return Err("Maximum 25 chars are allowed".into()); + return Err(NameUpdateError::TooLong(25, name.len())); } if name.len() < 3 { - return Err("Minimum of 3 chars needed".into()); + return Err(NameUpdateError::TooShort(3, name.len())); } match self { @@ -118,6 +125,9 @@ impl Backend { #[tokio::main] async fn main() { + let session_store = MemoryStore::default(); + let session_layer = SessionManagerLayer::new(session_store).with_secure(false); + let connection_options = SqliteConnectOptions::from_str("sqlite://db.sqlite").unwrap(); let db: SqlitePool = PoolOptions::new() .connect_with(connection_options) @@ -128,7 +138,9 @@ async fn main() { .route("/", get(index::index)) .nest_service("/static", ServeDir::new("./static/serve")) .merge(game::routes()) - .with_state(Arc::new(Backend::Sqlite(db))); + .with_state(Arc::new(Backend::Sqlite(db))) + .layer(MessagesManagerLayer) + .layer(session_layer); // run our app with hyper, listening globally on port 3000 let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); diff --git a/src/page.rs b/src/page.rs index 65a00de..658b4c2 100644 --- a/src/page.rs +++ b/src/page.rs @@ -1,40 +1,49 @@ use crate::Language; +use axum_messages::Messages; use maud::{html, Markup, DOCTYPE}; // TODO: set dynamic meta lang attribute -// TODO: remove function -#[deprecated] -pub fn new(content: Markup, lang: Language) -> Markup { - let page = Page::new(lang); - page.content(content) -} - pub(crate) struct Page { lang: Language, - succ: Option, - err: Option, + found_camera: Option, + new_name: bool, + err: Option<(String, String, String)>, } impl Page { pub fn new(lang: Language) -> Self { Self { lang, - succ: None, + found_camera: None, + new_name: false, err: None, } } - pub fn succ(&mut self, msg: String) { - self.succ = Some(msg); - } - - pub fn err(&mut self, msg: String) { - self.err = Some(msg); - } - - fn has_msg(&self) -> bool { - self.succ.is_some() || self.err.is_some() + pub fn messages(&mut self, messages: Messages) { + for message in messages { + let text = &message.to_string()[..]; + match (message.level, text) { + (_, "set-name-succ") => { + self.new_name = true; + } + (_, msg) if msg.starts_with("found-cam|") => { + let (_, name) = msg.split_once('|').expect("we just checked |"); + self.found_camera = Some(name.into()); + } + (_, msg) if msg.starts_with("err|") => { + let mut parts = msg.splitn(4, '|'); + let _ = parts.next().expect("just checked |"); + if let (Some(title), Some(body), Some(footer)) = + (parts.next(), parts.next(), parts.next()) + { + self.err = Some((title.into(), body.into(), footer.into())); + } + } + (_, _) => {} + } + } } pub fn content(self, content: Markup) -> Markup { @@ -83,39 +92,44 @@ impl Page { } main.container { - article class="succ w-full" { - header { - "Camera found" - } - "✨ You are a star ✨ Star dust sprinkle, sprinkle. " - a href="#ranking" { - "See your ranking" - } - footer { - "Rear Exit Cam" + @if let Some(found_camera) = &self.found_camera { + article class="succ w-full" { + header { + "Camera " + (found_camera) + " found" + } + "✨ You are a star ✨ Star dust sprinkle, sprinkle. " + a href="#ranking" { + "See your ranking" + } + footer { + "Rear Exit Cam" + } } } - article class="name w-full" { - header { - "Camera found" - } - "text" - footer { - "Name of Camera" + @if self.new_name { + article class="name w-full" { + header { + "New name!" + } + "Welcome" } } - article class="error w-full" { - header { - "Camera found" - } - "text" - footer { - "Name of Camera" + @if let Some(err) = &self.err { + article class="error w-full" { + header { + (err.0) + } + (err.1) + footer { + (err.2) + } } } - + section { (content) } }