From 11ad1d965283c89d7421903b205096848d2c3c92 Mon Sep 17 00:00:00 2001 From: Rivulet Date: Wed, 29 Oct 2025 18:49:52 -0700 Subject: [PATCH] alot of progress --- Cargo.lock | 310 ++++++++++++++++++++++++++++++++- Cargo.toml | 21 +-- src/lib.rs | 469 ++++++++++++++++++-------------------------------- wrangler.toml | 14 +- 4 files changed, 493 insertions(+), 321 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d83e368..b11089c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,6 +46,7 @@ dependencies = [ "tower", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -64,6 +65,30 @@ dependencies = [ "sync_wrapper", "tower-layer", "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96" +dependencies = [ + "axum", + "axum-core", + "bytes", + "cookie", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "serde_core", + "tower-layer", + "tower-service", + "tracing", ] [[package]] @@ -77,6 +102,12 @@ dependencies = [ "syn", ] +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + [[package]] name = "bumpalo" version = "3.19.0" @@ -106,6 +137,26 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -193,6 +244,20 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + [[package]] name = "http" version = "1.3.1" @@ -350,12 +415,27 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + [[package]] name = "litemap" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "matchit" version = "0.7.3" @@ -407,16 +487,24 @@ name = "mis-interpreter" version = "0.1.0" dependencies = [ "axum", - "futures-util", + "axum-extra", + "getrandom", "maud", + "rand", "serde", - "serde_json", + "tower-cookies", "tower-service", - "urlencoding", + "uuid", "worker", "worker-macros", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-traits" version = "0.2.19" @@ -432,6 +520,29 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -479,6 +590,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.103" @@ -509,6 +635,50 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -521,6 +691,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.228" @@ -598,6 +774,12 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "slab" version = "0.4.11" @@ -644,6 +826,37 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.1" @@ -677,6 +890,22 @@ dependencies = [ "tower-service", ] +[[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-layer" version = "0.3.3" @@ -689,6 +918,22 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" + [[package]] name = "unicode-ident" version = "1.0.20" @@ -707,24 +952,39 @@ dependencies = [ "serde", ] -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom", + "js-sys", + "sha1_smol", + "wasm-bindgen", +] + [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.105" @@ -806,6 +1066,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + [[package]] name = "worker" version = "0.6.7" @@ -895,6 +1167,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerofrom" version = "0.1.6" diff --git a/Cargo.toml b/Cargo.toml index 6c5161c..b122b26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,25 +7,22 @@ authors = ["Rivulet "] [lib] crate-type = ["cdylib"] -# (Wrangler will ignore these; fine to keep or remove) [package.metadata.wasm-pack] target = "web" out-dir = "build" out-name = "index" [dependencies] -# Cloudflare Workers (HTTP + Axum adapter) -worker = { version = "0.6", features = ["http", "axum"] } -worker-macros = { version = "0.6", features = ["http"] } - -# Axum *without* tokio/hyper; we don't use its WS, so don't enable it. +worker = { version = "0.6", features = ['http', 'axum'] } +worker-macros = { version = "0.6", features = ['http'] } axum = { version = "0.8", default-features = false, features = ["form", "macros"] } - tower-service = "0.3.3" - maud = { version = "0.27.0", default-features = false } +rand = { version = "0.9.2",default-features = false, features = ["thread_rng"] } +serde = { version = "1.0.228", default-features = false, features = ["derive"] } +tower-cookies = "0.11.0" +axum-extra = { version = "0.10.3", features = ["cookie"] } +uuid = { version = "1.18", features = ["v4", "v7", "v5", "js"] } +[target.'cfg(target_arch = "wasm32")'.dependencies] +getrandom = { version = "0.3", features = ["wasm_js"] } -serde = { version = "1.0.228", features = ["derive"] } -serde_json = "1.0.145" -urlencoding = "2.1.3" -futures-util = "0.3.31" diff --git a/src/lib.rs b/src/lib.rs index 46aadef..c9bba02 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,339 +1,214 @@ -use axum::{ - body::{to_bytes, Body}, - http::{Response as AxumResponse, StatusCode}, - response::Html, - routing::{get, post}, - Form, Router, -}; -use futures_util::StreamExt; -use maud::{html, Markup}; -use serde::Deserialize; +use axum::http::{status, HeaderMap, HeaderValue, StatusCode}; +use axum::response::{Html, Redirect }; +use axum::routing::post; +use axum::{routing::get, Extension, Form, Router}; +use axum::http::header::SET_COOKIE; +use axum_extra::extract::cookie::Cookie; +use axum_extra::extract::CookieJar; +use maud::html; +use tower_cookies::{CookieManagerLayer, Cookies}; use tower_service::Service; +use uuid::Uuid; use worker::*; +use worker::wasm_bindgen_futures::spawn_local; -/* ===================== Router ===================== */ +#[durable_object] +pub struct Room { + state: State, +} + +fn gen_id() -> String { + Uuid::now_v7().to_string() +} + +fn load_jar(req: &Request) -> CookieJar { + let mut jar = CookieJar::new(); + if let Ok(Some(header)) = req.headers().get("Cookie") { + for c in Cookie::split_parse(header) { + if let Ok(c) = c { + jar = jar.add(c); + } + } + } + jar +} + +impl DurableObject for Room { + fn new(state: State, env: Env) -> Self { + Self { state } + } + + async fn fetch(&self, req: Request) -> Result { + let cookie = load_jar(&req); + let url = req.url()?; + match (req.method(), url.path()) { + (Method::Get, "/ws") => { + + let pair = WebSocketPair::new()?; + let server = pair.server; + let client = pair.client; + + server.accept()?; + + Response::from_websocket(client) + } + (Method::Post, "/setname") => { + let name = req.headers().get("Name").unwrap_or(None); + if name.is_some() { + self.state.storage().put("name", name.unwrap()).await.expect("failed to write to storage"); + Response::ok("Ok") + } else { + Response::error("No name provided", StatusCode::BAD_REQUEST.as_u16()) + } + } + _ => todo!(), + } + } +} fn router() -> Router { Router::new() .route("/", get(root)) - .route("/create", post(create_game)) - // /ws is handled in fetch() using CF WebSocketPair. + .route("/create", post(create)) + .route("/lobby", get(lobby)) +} +#[derive(Clone)] +pub struct Params { + env: Env, } - -/* ===================== Cloudflare entry ===================== */ - #[event(fetch)] -async fn fetch(req: HttpRequest, _env: Env, _ctx: Context) -> Result { - // Intercept WebSocket before handing off to Axum - let is_ws = req - .headers() - .get("Upgrade") - .and_then(|v| v.to_str().ok()) - .map(|v| v.eq_ignore_ascii_case("websocket")) - .unwrap_or(false); +async fn fetch( + mut req: HttpRequest, + env: Env, + _ctx: Context, +) -> Result> { - if req.uri().path() == "/ws" && is_ws { - return handle_ws_upgrade(req).await; + let url = req.uri(); + let path = url.path(); + + // Expect: /ws/{name} + if path == "/ws" { + let jar = load_jar(&req.try_into()?); + let name = jar.get("game_id").expect("failed to get game_id").to_string(); + + // Resolve the DO instance deterministically by name + let ns = env.durable_object("ROOM")?; // binding in wrangler.toml + let id = ns.id_from_name(&*name)?; + let stub = id.get_stub()?; + + return Ok(stub.fetch_with_request(Request::new("http://do/ws", Method::Get)?).await.expect("failed to connect").try_into()?); } - // Otherwise hand off to Axum and adapt Axum -> Worker Response - let axum_resp: AxumResponse = router().call(req).await?; - axum_to_worker(axum_resp).await + + req.extensions_mut().insert( Params{ env}); + Ok(router().call(req).await?) } -/* Convert an Axum response into a worker::Response */ -async fn axum_to_worker(ax: AxumResponse) -> Result { - let (parts, body) = ax.into_parts(); - let bytes = to_bytes(body, usize::MAX).await.unwrap_or_default(); +#[derive(serde::Deserialize, Debug, Clone)] +pub struct CreateGameForm { + game_name: Option, // <- String, not &str + password: Option, + player_name: String, +} - let mut resp = Response::from_bytes(bytes.to_vec())?; - resp = resp.with_status(parts.status.as_u16()); - - for (name, value) in parts.headers.iter() { - if let Ok(val) = value.to_str() { - resp.headers_mut().set(name.as_str(), val)?; // <- note: no ? after headers_mut() - } +#[axum::debug_handler] +pub async fn create(Extension(params): Extension, mut jar: CookieJar, Form(form): Form) -> (CookieJar, Redirect) { + let id = gen_id(); + jar = jar.add(Cookie::new("game_id", id.clone())); + jar = jar.add(Cookie::new("player_name", form.player_name.clone())); + if !form.game_name.clone().unwrap_or("".to_string()).is_empty() { + jar = jar.add(Cookie::new("game_name", form.game_name.as_ref().unwrap_or(&"".to_string()).clone())); } - Ok(resp) -} - -/* ===================== Pages ===================== */ - -/// GET / -pub async fn root() -> Html { - // If you want to read cookies here, switch to a custom Response and pass headers in via extractors. - let page = render_page(None); - Html::from(page) -} - -/// POST /create (classic PRG with cookie flash) -#[derive(Deserialize)] -struct CreateForm { - game_name: Option, - password: Option, - player_name: Option, -} - -pub async fn create_game(Form(form): Form) -> AxumResponse { - let msg = flash_msg(&form); - AxumResponse::builder() - .status(StatusCode::SEE_OTHER) - .header("Location", "/") - .header( - "Set-Cookie", - format!( - "flash={}; Path=/; Max-Age=30; HttpOnly; SameSite=Lax", - urlencoding::encode(&msg) - ), - ) - .body(Body::empty()) - .unwrap() -} - -/* ===================== WebSocket (HTMX ws-send) ===================== */ - -async fn handle_ws_upgrade(_req: HttpRequest) -> Result { - // Create the CF WebSocketPair at the worker layer - let pair = WebSocketPair::new()?; - let server = pair.server; - let client = pair.client; - - server.accept()?; - - // Spawn a local task to read messages and push OOB HTML back. - wasm_bindgen_futures::spawn_local(async move { - // Create the borrow-bound event stream *inside* the task and keep the clone alive. - let server_clone = server.clone(); - let mut events = match server_clone.events() { - Ok(e) => e, - Err(_) => return, - }; - - while let Some(Ok(evt)) = events.next().await { - if let WebsocketEvent::Message(m) = evt { - if let Some(txt) = m.text() { - if let Ok(parsed) = serde_json::from_str::(&*txt) { - let msg = flash_msg(&parsed); - let oob = render_flash_oob(&msg); - let _ = server_clone.send_with_str(oob); - } else { - let _ = server_clone.send_with_str(render_log_oob("Unrecognized message")); - } - } - } + if !form.password.clone().unwrap_or("".to_string()).is_empty() { + jar = jar.add(Cookie::new("password", form.password.as_ref().unwrap_or(&"".to_string()).clone())); + } + let ns = params.env.durable_object("ROOM").unwrap(); + let id = ns.id_from_name(&*format!("room-{}",id)).expect("Failed to find game_name"); + let stub = id.get_stub().expect("Failed to find stub"); + let req = Request::new("http://do/setname", Method::Post).unwrap(); + req.headers().set("Name", &*form.game_name.clone().unwrap_or("".to_string())).expect("Failed to set header"); + let name = form.game_name.clone().unwrap_or("".to_string()); + spawn_local(async move { + if let Err(e) = stub.fetch_with_request(req).await { + console_error!("failed to set name: {}",e) } }); - // Return the upgrade response (must be worker::Response) - Response::from_websocket(client) + console_log!("{:?} id:{}", form, id); + (jar, Redirect::to("/lobby")) } - -/* ===================== View / HTML ===================== */ - -fn render_page(flash: Option<&str>) -> String { - (html! { +pub async fn root() -> Html { + Html::from(html! { (maud::DOCTYPE) html lang="en" { head { meta charset="utf-8"; meta name="viewport" content="width=device-width, initial-scale=1"; title { "Mis-Interpreter" } - - // HTMX + WS Extension - script src="https://unpkg.com/htmx.org@2.0.4" {} - script src="https://unpkg.com/htmx-ext-ws@2.0.3/ws.js" {} - style { (PASTEL_CSS) } } body { main class="shell" { section class="card cartoon" { - h1 class="title" { "Mis-Interpreter" } - p class="subtitle" { "Create a game?" } - - // Flash target - div id="flash" { - @if let Some(msg) = flash { - (render_flash_inline(msg)) - } + h1 class="title" { + "Mis-Interpreter" } - - // Live form using htmx WebSocket - div hx-ext="ws" ws-connect="/ws" { - form id="create-form" class="form" ws-send { - label class="sr" for="game_name" { "Game Name" } - input id="game_name" type="text" name="game_name" - placeholder="Game Name (Optional)" class="input"; - - label class="sr" for="password" { "Password" } - input id="password" type="password" name="password" - placeholder="Password (Optional)" class="input"; - - label class="sr" for="player_name" { "Player Name" } - input id="player_name" type="text" name="player_name" - value="John Wick" class="input"; - - button type="submit" class="btn" { "Create (Live)" } + p class="subtitle" { + "Create a game?" + } + form method="post" action="/create" class="form" { + label class="sr" for="game_name" { + "Game Name" + } + input id="game_name" type="text" name="game_name" placeholder="Game Name (Optional)" class="input"; + label class="sr" for="password" { + "Password" + } + input id="password" type="password" name="password" placeholder="Password (Optional)" class="input"; label class="sr" for="player_name" { + "Player Name" + } + input id="player_name" type="text" name="player_name" value="John Wick" class="input"; button type="submit" class="btn" { + "Create" } - div id="ws-log" class="wslog" {} } } } } } - }).into_string() + }.into_string()) } -fn render_flash_inline(msg: &str) -> Markup { - html! { div class="flash cartoon" { (escape(msg)) } } +pub async fn lobby(jar: CookieJar) -> Html { + let title = jar.get("game_name").and_then(|v| Some(v.value())).unwrap_or("Lobby"); + Html::from(html! { + (maud::DOCTYPE) + html lang="en" { + head { + meta charset="utf-8"; + meta name="viewport" content="width=device-width, initial-scale=1"; + script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js" integrity="sha384-/TgkGk7p307TH7EXJDuUlgG3Ce1UVolAOFopFekQkkXihi5u/6OCvVKyz1W+idaz" crossorigin="anonymous" {} + script src="https://unpkg.com/htmx.org@1.9.12/dist/ext/ws.js" {} + title { "Mis-Interpreter - "(title) } + style { (PASTEL_CSS) } + } + body { + main class="shell" { + section class="card cartoon" { + h1 class="title" { + (title) + } + hr; + p class="subtitle" { + "Player List" + } + div { + + } + } + } + } + } + }.into_string()) } -fn render_flash_oob(msg: &str) -> String { - format!( - r#"
{}
"#, - escape(msg) - ) -} - -fn render_log_oob(text: &str) -> String { - format!(r#"
{}
"#, escape(text)) -} - -/* ===================== Utilities ===================== */ - -fn flash_msg(form: &CreateForm) -> String { - let game = form.game_name.as_deref().unwrap_or("Untitled"); - let player = form.player_name.as_deref().unwrap_or("Anonymous"); - let priv_tag = if form.password.as_deref().unwrap_or("").is_empty() { "" } else { " (private)" }; - format!("Created game '{}' for '{}'{priv_tag}", escape(game), escape(player)) -} - -// minimal safe escaping for text nodes -fn escape(s: &str) -> String { - s.replace('&', "&").replace('<', "<").replace('>', ">") -} - -/* ===================== Theme ===================== */ - -const PASTEL_CSS: &str = r#" -/* ===== Pastel Blues, Cartoon Borders ===== */ -:root{ - /* palette */ - --sky-50:#eff6ff; --sky-100:#dbeafe; --sky-200:#bfdbfe; --sky-300:#93c5fd; - --sky-400:#60a5fa; --sky-500:#3b82f6; --indigo-600:#4f46e5; --teal-300:#99f6e4; - - --bg-top: color-mix(in oklab, var(--sky-100) 70%, white); - --bg-bot: color-mix(in oklab, var(--teal-300) 35%, white); - - --card: #f2f7ff; - --ink: #0b1220; - --muted:#44506a; - - /* cartoon outlines */ - --outline: #133a84; /* deep blue stroke */ - --shadow: #0e2250; /* offset shadow for sticker look */ - - --ring: var(--sky-400); - --field-a: #e9f2ff; - --field-b: #e6fffb; - - --btn-a: #a5b4ff; /* indigo pastel */ - --btn-b: #7dd3fc; /* sky pastel */ - - --flash-bg:#c4f1f9; /* cyan-200 */ - --flash-border:#0891b2; -} - -*{box-sizing:border-box} -html,body{height:100%} -body{ - margin:0; - font:17px/1.55 ui-sans-serif, system-ui, -apple-system, Segoe UI, Inter, Roboto, Arial; - color:var(--ink); - background: - radial-gradient(1000px 700px at 15% -10%, var(--sky-200) 0%, transparent 60%), - radial-gradient(1100px 800px at 110% 0%, var(--teal-300) 0%, transparent 60%), - linear-gradient(180deg, var(--bg-top), var(--bg-bot)); -} - -/* ——— cartoon helpers ——— */ -.cartoon{ - border:4px solid var(--outline); - box-shadow: 8px 8px 0 var(--shadow); - border-radius: 26px; -} - -/* layout */ -.shell{min-height:100%; display:grid; place-items:center; padding:3rem 1rem} -.card{ - width:min(740px, 94vw); - background: var(--card); - padding:3rem 2.5rem; -} -.card.cartoon{} /* (keep class for specificity) */ - -.title{ - text-align:center; margin:0 0 .5rem; font-weight:900; letter-spacing:.3px; - font-size: clamp(2.1rem, 3.4vw + 1rem, 3.4rem); - /* shiny blue gradient text */ - background: linear-gradient(90deg, var(--sky-500), var(--indigo-600)); - -webkit-background-clip: text; background-clip: text; color: transparent; - text-shadow: 0 2px 0 #ffffff66; -} -.subtitle{ text-align:center; color:var(--muted); margin:0 0 2rem; font-weight:700 } - -.flash{ - background: var(--flash-bg); - border:4px solid var(--flash-border); - color:#0c4a6e; - padding:.85rem 1rem; border-radius:16px; margin:0 0 1.25rem; - box-shadow: 6px 6px 0 var(--shadow); -} - -/* form */ -.form{ display:grid; grid-template-columns:1fr auto; gap:16px 16px; align-items:center } -.input{ - width:100%; - padding:1rem 1.1rem; - border:4px solid var(--outline); - border-radius:16px; - background: - linear-gradient(0deg, #ffffff, #ffffff) padding-box, - radial-gradient(120% 120% at 0% 0%, var(--field-a) 0%, var(--field-b) 100%) border-box; - color:var(--ink); - box-shadow: 4px 4px 0 var(--shadow); - transition: transform .06s ease, box-shadow .15s ease, border-color .15s ease; -} -.input::placeholder{color:#6b7280} -.input:focus{ - outline:none; transform: translateY(-1px); - border-color: var(--ring); - box-shadow: 6px 6px 0 var(--shadow), 0 0 0 6px color-mix(in oklab, var(--ring) 40%, white); -} - -/* button: chunky pill with dual-tone blue */ -.btn{ - padding:1rem 1.35rem; - font-weight:900; - border:4px solid var(--outline); - border-radius:999px; - color:#06223a; - background: linear-gradient(180deg, - color-mix(in oklab, var(--btn-b) 75%, white), - color-mix(in oklab, var(--btn-a) 85%, white)); - cursor:pointer; - box-shadow: 6px 6px 0 var(--shadow); - transition: transform .07s ease, filter .15s ease, box-shadow .15s ease; -} -.btn:hover{ filter:brightness(1.06) saturate(1.05) } -.btn:active{ transform: translateY(2px); box-shadow: 4px 4px 0 var(--shadow) } - -/* accessibility */ -.sr{ position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0 } - -/* responsive */ -@media (max-width: 560px){ - .form{ grid-template-columns: 1fr } - .btn{ width:100% } -} -"#; +const PASTEL_CSS: &str = r#" /* ===== Pastel Blues, Cartoon Borders ===== */ :root{ /* palette */ --sky-50:#eff6ff; --sky-100:#dbeafe; --sky-200:#bfdbfe; --sky-300:#93c5fd; --sky-400:#60a5fa; --sky-500:#3b82f6; --indigo-600:#4f46e5; --teal-300:#99f6e4; --bg-top: color-mix(in oklab, var(--sky-100) 70%, white); --bg-bot: color-mix(in oklab, var(--teal-300) 35%, white); --card: #f2f7ff; --ink: #0b1220; --muted:#44506a; /* cartoon outlines */ --outline: #133a84; /* deep blue stroke */ --shadow: #0e2250; /* offset shadow for sticker look */ --ring: var(--sky-400); --field-a: #e9f2ff; --field-b: #e6fffb; --btn-a: #a5b4ff; /* indigo pastel */ --btn-b: #7dd3fc; /* sky pastel */ --flash-bg:#c4f1f9; /* cyan-200 */ --flash-border:#0891b2; } *{box-sizing:border-box} html,body{height:100%} body{ margin:0; font:17px/1.55 ui-sans-serif, system-ui, -apple-system, Segoe UI, Inter, Roboto, Arial; color:var(--ink); background: radial-gradient(1000px 700px at 15% -10%, var(--sky-200) 0%, transparent 60%), radial-gradient(1100px 800px at 110% 0%, var(--teal-300) 0%, transparent 60%), linear-gradient(180deg, var(--bg-top), var(--bg-bot)); } /* ——— cartoon helpers ——— */ .cartoon{ border:4px solid var(--outline); box-shadow: 8px 8px 0 var(--shadow); border-radius: 26px; } /* layout */ .shell{min-height:100%; display:grid; place-items:center; padding:3rem 1rem} .card{ width:min(740px, 94vw); background: var(--card); padding:3rem 2.5rem; } .card.cartoon{} /* (keep class for specificity) */ .title{ text-align:center; margin:0 0 .5rem; font-weight:900; letter-spacing:.3px; font-size: clamp(2.1rem, 3.4vw + 1rem, 3.4rem); /* shiny blue gradient text */ background: linear-gradient(90deg, var(--sky-500), var(--indigo-600)); -webkit-background-clip: text; background-clip: text; color: transparent; text-shadow: 0 2px 0 #ffffff66; } .subtitle{ text-align:center; color:var(--muted); margin:0 0 2rem; font-weight:700 } .flash{ background: var(--flash-bg); border:4px solid var(--flash-border); color:#0c4a6e; padding:.85rem 1rem; border-radius:16px; margin:0 0 1.25rem; box-shadow: 6px 6px 0 var(--shadow); } /* form */ .form{ display:grid; grid-template-columns:1fr auto; gap:16px 16px; align-items:center } .input{ width:100%; padding:1rem 1.1rem; border:4px solid var(--outline); border-radius:16px; background: linear-gradient(0deg, #ffffff, #ffffff) padding-box, radial-gradient(120% 120% at 0% 0%, var(--field-a) 0%, var(--field-b) 100%) border-box; color:var(--ink); box-shadow: 4px 4px 0 var(--shadow); transition: transform .06s ease, box-shadow .15s ease, border-color .15s ease; } .input::placeholder{color:#6b7280} .input:focus{ outline:none; transform: translateY(-1px); border-color: var(--ring); box-shadow: 6px 6px 0 var(--shadow), 0 0 0 6px color-mix(in oklab, var(--ring) 40%, white); } /* button: chunky pill with dual-tone blue */ .btn{ padding:1rem 1.35rem; font-weight:900; border:4px solid var(--outline); border-radius:999px; color:#06223a; background: linear-gradient(180deg, color-mix(in oklab, var(--btn-b) 75%, white), color-mix(in oklab, var(--btn-a) 85%, white)); cursor:pointer; box-shadow: 6px 6px 0 var(--shadow); transition: transform .07s ease, filter .15s ease, box-shadow .15s ease; } .btn:hover{ filter:brightness(1.06) saturate(1.05) } .btn:active{ transform: translateY(2px); box-shadow: 4px 4px 0 var(--shadow) } /* accessibility */ .sr{ position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0 } /* responsive */ @media (max-width: 560px){ .form{ grid-template-columns: 1fr } .btn{ width:100% } } "#; diff --git a/wrangler.toml b/wrangler.toml index 24c3db1..57249ba 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -1,7 +1,15 @@ name = "mis-interpreter" -workers_dev = true -compatibility_date = "2025-10-27" main = "build/worker/shim.mjs" +compatibility_date = "2024-11-06" [build] -command = "cargo install -q worker-build && worker-build --release" # required \ No newline at end of file +command = "cargo install -q worker-build && worker-build --release" + +[durable_objects] +bindings = [ + { name = "ROOM", class_name = "Room" } +] + +[[migrations]] +tag = "v1" +new_classes = ["Room"]