use futures::channel::mpsc::UnboundedSender; use leptos::prelude::*; use leptos_router::hooks::use_query_map; use crate::app::{NetCommand, Screen}; use crate::i18n::*; // ── Room code generation ────────────────────────────────────────────────────── fn generate_room_code() -> String { use rand::Rng; let mut rng = rand::rng(); const CHARS: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789"; (0..6) .map(|_| CHARS[rng.random_range(0..CHARS.len())] as char) .collect() } // ── QR code SVG rendering ───────────────────────────────────────────────────── pub(crate) fn qr_svg(text: &str) -> String { use qrcodegen::{QrCode, QrCodeEcc}; let qr = match QrCode::encode_text(text, QrCodeEcc::Medium) { Ok(q) => q, Err(_) => return String::new(), }; let size = qr.size(); let border = 2; let total = size + 2 * border; let mut svg = format!( "", t = total, ); svg.push_str(""); for y in 0..size { for x in 0..size { if qr.get_module(x, y) { svg.push_str(&format!( "", x + border, y + border, )); } } } svg.push_str(""); svg } // ── Share URL helper ────────────────────────────────────────────────────────── #[cfg(target_arch = "wasm32")] pub(crate) fn room_url(code: &str) -> String { let origin = web_sys::window() .and_then(|w| w.location().origin().ok()) .unwrap_or_default(); format!("{}/?room={}", origin, code) } #[cfg(not(target_arch = "wasm32"))] pub(crate) fn room_url(code: &str) -> String { format!("http://localhost:9091/?room={}", code) } // ── Lobby component ─────────────────────────────────────────────────────────── #[derive(Clone)] enum LobbyView { Idle, Waiting { code: String }, } #[component] pub fn LobbyPage() -> impl IntoView { let screen = use_context::>().expect("Screen context not found"); let cmd_tx = use_context::>() .expect("UnboundedSender not found in context"); let query = use_query_map(); let view_state: RwSignal = RwSignal::new(LobbyView::Idle); // Auto-join when the URL contains ?room=CODE let cmd_tx_query = cmd_tx.clone(); Effect::new(move |_| { if let Some(code) = query.read().get("room") { if !code.is_empty() { cmd_tx_query .unbounded_send(NetCommand::JoinRoom { room: code }) .ok(); } } }); let error = move || match screen.get() { Screen::Login { error } => error, _ => None, }; let cmd_tx_idle = cmd_tx; view! {
} } // ── Idle card: Create + vs Bot + hidden join-by-code ───────────────────────── #[component] fn IdleCard( on_create: impl Fn(leptos::ev::MouseEvent) + 'static, cmd_tx_bot: UnboundedSender, ) -> impl IntoView { let i18n = use_i18n(); let join_open = RwSignal::new(false); let join_code = RwSignal::new(String::new()); let cmd_tx_join = cmd_tx_bot.clone(); view! { // Hidden "join by code" fallback
{move || { // Clone the sender on each reactive run to keep the outer closure FnMut let cmd = cmd_tx_join.clone(); join_open.get().then(|| view! {
}) }}
} } // ── Waiting card: URL + copy + QR ──────────────────────────────────────────── #[component] fn WaitingCard(code: String) -> impl IntoView { let i18n = use_i18n(); let url = room_url(&code); let svg = qr_svg(&url); let copied = RwSignal::new(false); let on_copy = { let url = url.clone(); move |_| { #[cfg(target_arch = "wasm32")] { let url = url.clone(); wasm_bindgen_futures::spawn_local(async move { if let Some(clipboard) = web_sys::window() .map(|w| w.navigator().clipboard()) { let _ = wasm_bindgen_futures::JsFuture::from( clipboard.write_text(&url) ).await; copied.set(true); gloo_timers::future::TimeoutFuture::new(2000).await; copied.set(false); } }); } } }; view! {

{t!(i18n, waiting_for_opponent)}

{t!(i18n, share_link)}

{t!(i18n, scan_qr)}

} }