diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index d8c14ad..f374377 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -11,12 +11,11 @@
-
-
-
-
-
+
+
+
+
@@ -91,7 +90,7 @@
1761613870150
-
+
@@ -101,7 +100,15 @@
1761613910534
-
+
+
+ 1761616087329
+
+
+
+ 1761616087329
+
+
@@ -109,6 +116,7 @@
-
+
+
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index e1e46de..d83e368 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -26,7 +26,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871"
dependencies = [
"axum-core",
+ "axum-macros",
"bytes",
+ "form_urlencoded",
"futures-util",
"http",
"http-body",
@@ -38,6 +40,8 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"serde_core",
+ "serde_path_to_error",
+ "serde_urlencoded",
"sync_wrapper",
"tower",
"tower-layer",
@@ -62,6 +66,17 @@ dependencies = [
"tower-service",
]
+[[package]]
+name = "axum-macros"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "bumpalo"
version = "3.19.0"
@@ -353,6 +368,28 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
+[[package]]
+name = "maud"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8156733e27020ea5c684db5beac5d1d611e1272ab17901a49466294b84fc217e"
+dependencies = [
+ "itoa",
+ "maud_macros",
+]
+
+[[package]]
+name = "maud_macros"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7261b00f3952f617899bc012e3dbd56e4f0110a038175929fa5d18e5a19913ca"
+dependencies = [
+ "proc-macro2",
+ "proc-macro2-diagnostics",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "memchr"
version = "2.7.6"
@@ -370,7 +407,12 @@ name = "mis-interpreter"
version = "0.1.0"
dependencies = [
"axum",
+ "futures-util",
+ "maud",
+ "serde",
+ "serde_json",
"tower-service",
+ "urlencoding",
"worker",
"worker-macros",
]
@@ -446,6 +488,18 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "proc-macro2-diagnostics"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+]
+
[[package]]
name = "quote"
version = "1.0.41"
@@ -521,6 +575,17 @@ dependencies = [
"serde_core",
]
+[[package]]
+name = "serde_path_to_error"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
+dependencies = [
+ "itoa",
+ "serde",
+ "serde_core",
+]
+
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@@ -642,12 +707,24 @@ 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 = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
[[package]]
name = "wasm-bindgen"
version = "0.2.105"
diff --git a/Cargo.toml b/Cargo.toml
index 10530af..6c5161c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,13 +7,25 @@ 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]
-worker = { version = "0.6", features = ['http', 'axum'] }
-worker-macros = { version = "0.6", features = ['http'] }
-axum = { version = "0.8", default-features = false }
+# 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.
+axum = { version = "0.8", default-features = false, features = ["form", "macros"] }
+
tower-service = "0.3.3"
+
+maud = { version = "0.27.0", default-features = false }
+
+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 f80ee2b..46aadef 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,20 +1,339 @@
-use axum::{routing::get, Router};
+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 tower_service::Service;
use worker::*;
+/* ===================== Router ===================== */
+
fn router() -> Router {
- Router::new().route("/", get(root))
+ Router::new()
+ .route("/", get(root))
+ .route("/create", post(create_game))
+ // /ws is handled in fetch() using CF WebSocketPair.
}
+/* ===================== Cloudflare entry ===================== */
+
#[event(fetch)]
-async fn fetch(
- req: HttpRequest,
- _env: Env,
- _ctx: Context,
-) -> Result> {
- Ok(router().call(req).await?)
+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);
+
+ if req.uri().path() == "/ws" && is_ws {
+ return handle_ws_upgrade(req).await;
+ }
+
+ // Otherwise hand off to Axum and adapt Axum -> Worker Response
+ let axum_resp: AxumResponse = router().call(req).await?;
+ axum_to_worker(axum_resp).await
}
-pub async fn root() -> &'static str {
- "Hello Axum!"
+/* 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();
+
+ 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()
+ }
+ }
+ 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"));
+ }
+ }
+ }
+ }
+ });
+
+ // Return the upgrade response (must be worker::Response)
+ Response::from_websocket(client)
+}
+
+/* ===================== View / HTML ===================== */
+
+fn render_page(flash: Option<&str>) -> String {
+ (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))
+ }
+ }
+
+ // 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)" }
+ }
+ div id="ws-log" class="wslog" {}
+ }
+ }
+ }
+ }
+ }
+ }).into_string()
+}
+
+fn render_flash_inline(msg: &str) -> Markup {
+ html! { div class="flash cartoon" { (escape(msg)) } }
+}
+
+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% }
+}
+"#;
diff --git a/wrangler.toml b/wrangler.toml
index afee802..24c3db1 100644
--- a/wrangler.toml
+++ b/wrangler.toml
@@ -1,12 +1,7 @@
name = "mis-interpreter"
workers_dev = true
-compatibility_date = "2025-10-28"
+compatibility_date = "2025-10-27"
main = "build/worker/shim.mjs"
-
-[[rules]]
-globs = [ "**/*.wasm" ]
-type = "CompiledWasm"
-
[build]
-command = "cargo install -q worker-build --version ^0.0.8 && worker-build --release" # required
\ No newline at end of file
+command = "cargo install -q worker-build && worker-build --release" # required
\ No newline at end of file