use serde::{Deserialize, Serialize}; #[cfg(debug_assertions)] pub const HTTP_BASE: &str = "http://localhost:8080"; #[cfg(not(debug_assertions))] pub const HTTP_BASE: &str = ""; fn url(path: &str) -> String { format!("{HTTP_BASE}{path}") } // ── Response types ──────────────────────────────────────────────────────────── #[derive(Clone, Debug, Deserialize)] pub struct MeResponse { pub id: i64, pub username: String, #[serde(default)] pub email_verified: bool, } #[derive(Clone, Debug, Deserialize)] pub struct UserProfile { pub id: i64, pub username: String, pub created_at: i64, pub total_games: i64, pub wins: i64, pub losses: i64, pub draws: i64, } #[derive(Clone, Debug, Deserialize)] pub struct GameSummary { pub id: i64, pub game_id: String, pub room_code: String, pub started_at: i64, pub ended_at: Option, pub result: Option, pub outcome: Option, } #[derive(Clone, Debug, Deserialize)] pub struct GamesResponse { pub games: Vec, } #[derive(Clone, Debug, Deserialize)] pub struct Participant { pub player_id: i64, pub outcome: Option, pub username: Option, } #[derive(Clone, Debug, Deserialize)] pub struct GameDetail { pub id: i64, pub game_id: String, pub room_code: String, pub started_at: i64, pub ended_at: Option, pub result: Option, pub participants: Vec, } #[derive(Clone, Debug, Deserialize)] pub struct PageContent { pub title: String, pub content: String, } // ── Request bodies ──────────────────────────────────────────────────────────── #[derive(Serialize)] pub struct RegisterBody<'a> { pub username: &'a str, pub email: &'a str, pub password: &'a str, } #[derive(Serialize)] pub struct LoginBody<'a> { pub username: &'a str, pub password: &'a str, } // ── Fetch helpers ───────────────────────────────────────────────────────────── pub async fn get_me() -> Result { let resp = gloo_net::http::Request::get(&url("/auth/me")) .credentials(web_sys::RequestCredentials::Include) .send() .await .map_err(|e| e.to_string())?; if resp.status() == 200 { resp.json::().await.map_err(|e| e.to_string()) } else { Err(format!("status {}", resp.status())) } } pub async fn post_login(username: &str, password: &str) -> Result { let body = LoginBody { username, password }; let resp = gloo_net::http::Request::post(&url("/auth/login")) .credentials(web_sys::RequestCredentials::Include) .json(&body) .map_err(|e| e.to_string())? .send() .await .map_err(|e| e.to_string())?; if resp.status() == 200 { resp.json::().await.map_err(|e| e.to_string()) } else { let text = resp.text().await.unwrap_or_default(); Err(text) } } pub async fn post_register(username: &str, email: &str, password: &str) -> Result { let body = RegisterBody { username, email, password }; let resp = gloo_net::http::Request::post(&url("/auth/register")) .credentials(web_sys::RequestCredentials::Include) .json(&body) .map_err(|e| e.to_string())? .send() .await .map_err(|e| e.to_string())?; if resp.status() == 201 { resp.json::().await.map_err(|e| e.to_string()) } else { let text = resp.text().await.unwrap_or_default(); Err(text) } } pub async fn post_logout() -> Result<(), String> { let resp = gloo_net::http::Request::post(&url("/auth/logout")) .credentials(web_sys::RequestCredentials::Include) .send() .await .map_err(|e| e.to_string())?; if resp.status() == 204 { Ok(()) } else { Err(format!("status {}", resp.status())) } } pub async fn delete_account() -> Result<(), String> { let resp = gloo_net::http::Request::delete(&url("/auth/account")) .credentials(web_sys::RequestCredentials::Include) .send() .await .map_err(|e| e.to_string())?; if resp.status() == 204 { Ok(()) } else { Err(format!("status {}", resp.status())) } } pub async fn get_user_profile(username: &str) -> Result { let resp = gloo_net::http::Request::get(&url(&format!("/users/{username}"))) .credentials(web_sys::RequestCredentials::Include) .send() .await .map_err(|e| e.to_string())?; if resp.status() == 200 { resp.json::().await.map_err(|e| e.to_string()) } else { Err(format!("status {}", resp.status())) } } pub async fn get_user_games(username: &str, page: i64) -> Result { let resp = gloo_net::http::Request::get(&url(&format!( "/users/{username}/games?page={page}&per_page=20" ))) .credentials(web_sys::RequestCredentials::Include) .send() .await .map_err(|e| e.to_string())?; if resp.status() == 200 { resp.json::().await.map_err(|e| e.to_string()) } else { Err(format!("status {}", resp.status())) } } pub async fn get_game_detail(id: i64) -> Result { let resp = gloo_net::http::Request::get(&url(&format!("/games/{id}"))) .credentials(web_sys::RequestCredentials::Include) .send() .await .map_err(|e| e.to_string())?; if resp.status() == 200 { resp.json::().await.map_err(|e| e.to_string()) } else { Err(format!("status {}", resp.status())) } } pub async fn get_verify_email(token: &str) -> Result<(), String> { let resp = gloo_net::http::Request::get(&url(&format!("/auth/verify-email?token={token}"))) .credentials(web_sys::RequestCredentials::Include) .send() .await .map_err(|e| e.to_string())?; if resp.status() == 200 { Ok(()) } else { let text = resp.text().await.unwrap_or_default(); Err(text) } } pub async fn post_resend_verification() -> Result<(), String> { let resp = gloo_net::http::Request::post(&url("/auth/resend-verification")) .credentials(web_sys::RequestCredentials::Include) .send() .await .map_err(|e| e.to_string())?; if resp.status() == 200 { Ok(()) } else { Err(format!("status {}", resp.status())) } } pub async fn post_forgot_password(email: &str) -> Result<(), String> { let body = serde_json::json!({ "email": email }); let resp = gloo_net::http::Request::post(&url("/auth/forgot-password")) .credentials(web_sys::RequestCredentials::Include) .json(&body) .map_err(|e| e.to_string())? .send() .await .map_err(|e| e.to_string())?; if resp.status() == 200 { Ok(()) } else { Err(format!("status {}", resp.status())) } } pub async fn post_reset_password(token: &str, new_password: &str) -> Result<(), String> { let body = serde_json::json!({ "token": token, "new_password": new_password }); let resp = gloo_net::http::Request::post(&url("/auth/reset-password")) .credentials(web_sys::RequestCredentials::Include) .json(&body) .map_err(|e| e.to_string())? .send() .await .map_err(|e| e.to_string())?; if resp.status() == 200 { Ok(()) } else { let text = resp.text().await.unwrap_or_default(); Err(text) } } pub async fn get_page(slug: &str, lang: &str) -> Result { let resp = gloo_net::http::Request::get(&url(&format!("/pages/{slug}?lang={lang}"))) .send() .await .map_err(|e| e.to_string())?; if resp.status() == 200 { resp.json::().await.map_err(|e| e.to_string()) } else { Err(format!("status {}", resp.status())) } } // ── Utilities ───────────────────────────────────────────────────────────────── /// Maps to the `Intl.DateTimeFormat` options object accepted by `Date.toLocaleString`. /// `Default` passes no options (browser default: full date + time). pub struct DateFormatOptions { /// "full" | "long" | "medium" | "short" — omit to suppress date part pub date_style: Option<&'static str>, /// "full" | "long" | "medium" | "short" — omit to suppress time part pub time_style: Option<&'static str>, } impl Default for DateFormatOptions { fn default() -> Self { Self { date_style: None, time_style: None } } } impl DateFormatOptions { pub fn date_only() -> Self { Self { date_style: Some("short"), time_style: None } } pub fn time_only() -> Self { Self { date_style: None, time_style: Some("short") } } pub fn date_time() -> Self { Self { date_style: Some("short"), time_style: Some("short") } } fn to_js_value(&self) -> wasm_bindgen::JsValue { if self.date_style.is_none() && self.time_style.is_none() { return wasm_bindgen::JsValue::UNDEFINED; } let obj = js_sys::Object::new(); if let Some(v) = self.date_style { let _ = js_sys::Reflect::set(&obj, &"dateStyle".into(), &v.into()); } if let Some(v) = self.time_style { let _ = js_sys::Reflect::set(&obj, &"timeStyle".into(), &v.into()); } obj.into() } } pub fn format_ts(ts: i64, locale: &str, opts: &DateFormatOptions) -> String { let ms = (ts * 1000) as f64; let date = js_sys::Date::new(&wasm_bindgen::JsValue::from_f64(ms)); date.to_locale_string(locale, &opts.to_js_value()) .as_string() .unwrap_or_default() }