//! Login page for realm users. use leptos::ev::SubmitEvent; use leptos::prelude::*; #[cfg(feature = "hydrate")] use leptos::task::spawn_local; #[cfg(feature = "hydrate")] use leptos_router::hooks::use_navigate; use crate::components::{Card, CenteredLayout, ErrorAlert, JoinRealmModal, SubmitButton}; use chattyness_db::models::RealmSummary; /// Main login page component. #[component] pub fn LoginPage() -> impl IntoView { view! {
// Logo and title

"Sign in to explore virtual community spaces"

} } /// Realm login form component. #[component] fn RealmLoginForm() -> impl IntoView { #[cfg(feature = "hydrate")] let navigate = use_navigate(); #[cfg(feature = "hydrate")] let navigate_for_submit = navigate.clone(); #[cfg(feature = "hydrate")] let navigate_for_join = navigate.clone(); #[cfg(feature = "hydrate")] let navigate_for_guest = navigate.clone(); // Form state let (username, set_username) = signal(String::new()); let (password, set_password) = signal(String::new()); let (selected_realm, set_selected_realm) = signal(Option::::None); let (private_realm, set_private_realm) = signal(String::new()); let (error, set_error) = signal(Option::::None); let (pending, set_pending) = signal(false); let (guest_pending, set_guest_pending) = signal(false); // Join modal state let (show_join_modal, set_show_join_modal) = signal(false); let (join_pending, set_join_pending) = signal(false); let (pending_realm, set_pending_realm) = signal(Option::::None); // Fetch public realms let realms = LocalResource::new(move || async move { #[cfg(feature = "hydrate")] { use gloo_net::http::Request; #[derive(serde::Deserialize)] struct ListResponse { realms: Vec, } let response = Request::get("/api/realms?include_nsfw=false&limit=20") .send() .await; match response { Ok(resp) if resp.ok() => resp.json::().await.ok().map(|r| r.realms), _ => None, } } #[cfg(not(feature = "hydrate"))] { None::> } }); // Get the realm slug to use let realm_slug = Signal::derive(move || { if !private_realm.get().is_empty() { Some(private_realm.get()) } else { selected_realm.get() } }); // Handle login submission let on_submit = move |ev: SubmitEvent| { ev.prevent_default(); set_error.set(None); let slug = realm_slug.get(); if slug.is_none() { set_error.set(Some( "Please select a realm or enter a private realm name".to_string(), )); return; } #[cfg(feature = "hydrate")] let slug = slug.unwrap(); let uname = username.get(); let pwd = password.get(); if uname.is_empty() || pwd.is_empty() { set_error.set(Some("Username and password are required".to_string())); return; } set_pending.set(true); #[cfg(feature = "hydrate")] { use chattyness_db::models::{LoginRequest, LoginResponse, LoginType}; use gloo_net::http::Request; let navigate = navigate_for_submit.clone(); spawn_local(async move { let request = LoginRequest { username: uname, password: pwd, login_type: LoginType::Realm, realm_slug: Some(slug), }; let response = Request::post("/api/auth/login") .json(&request) .unwrap() .send() .await; set_pending.set(false); match response { Ok(resp) if resp.ok() => { if let Ok(login_resp) = resp.json::().await { if login_resp.requires_pw_reset { navigate(&login_resp.redirect_url, Default::default()); } else if login_resp.is_member == Some(false) { if let Some(realm) = login_resp.realm { set_pending_realm.set(Some(realm)); set_show_join_modal.set(true); } } else { navigate(&login_resp.redirect_url, Default::default()); } } } Ok(resp) => { let status = resp.status(); if status == 401 { set_error.set(Some("Invalid username or password".to_string())); } else if status == 403 { set_error.set(Some("Your account is suspended or banned".to_string())); } else { set_error.set(Some("Login failed. Please try again.".to_string())); } } Err(_) => { set_error.set(Some( "Network error. Please check your connection.".to_string(), )); } } }); } }; // Handle join confirmation let on_join_confirm = move |_| { if pending_realm.get().is_some() { set_join_pending.set(true); #[cfg(feature = "hydrate")] { use chattyness_db::models::JoinRealmRequest; use gloo_net::http::Request; let realm = pending_realm.get().unwrap(); let navigate = navigate_for_join.clone(); let realm_id = realm.id; let realm_slug = realm.slug.clone(); spawn_local(async move { let request = JoinRealmRequest { realm_id }; let response = Request::post("/api/auth/join-realm") .json(&request) .unwrap() .send() .await; set_join_pending.set(false); set_show_join_modal.set(false); match response { Ok(resp) if resp.ok() => { navigate(&format!("/realms/{}", realm_slug), Default::default()); } Ok(resp) => { let status = resp.status(); if status == 403 { set_error.set(Some("Cannot join this realm".to_string())); } else { set_error.set(Some( "Failed to join realm. Please try again.".to_string(), )); } } Err(_) => { set_error.set(Some( "Network error. Please check your connection.".to_string(), )); } } }); } } }; let on_join_cancel = move |_| { set_show_join_modal.set(false); set_pending_realm.set(None); }; // Handle guest login let on_guest_click = move |_| { set_error.set(None); let slug = realm_slug.get(); if slug.is_none() { set_error.set(Some("Please select a realm first".to_string())); return; } set_guest_pending.set(true); #[cfg(feature = "hydrate")] { use chattyness_db::models::{GuestLoginRequest, GuestLoginResponse}; use gloo_net::http::Request; let navigate = navigate_for_guest.clone(); let realm_slug_val = slug.unwrap(); spawn_local(async move { let request = GuestLoginRequest { realm_slug: realm_slug_val, }; let response = Request::post("/api/auth/guest") .json(&request) .unwrap() .send() .await; set_guest_pending.set(false); match response { Ok(resp) if resp.ok() => { if let Ok(guest_resp) = resp.json::().await { navigate(&guest_resp.redirect_url, Default::default()); } } Ok(resp) => { #[derive(serde::Deserialize)] struct ErrorResp { error: String, } if let Ok(err) = resp.json::().await { set_error.set(Some(err.error)); } else { set_error.set(Some( "Guest access not available for this realm".to_string(), )); } } Err(_) => { set_error.set(Some( "Network error. Please check your connection.".to_string(), )); } } }); } }; view! {
// Realm selection
"Choose a Realm" // Public realm list "Loading realms..."

} }> {move || { realms .get() .map(|maybe_realms: Option>| { match maybe_realms { Some(realms) if !realms.is_empty() => { view! {
{realms .into_iter() .map(|realm| { let slug = realm.slug.clone(); let slug_for_click = slug.clone(); let is_selected = Signal::derive(move || { selected_realm.get() == Some(slug.clone()) }); view! { } }) .collect_view()}
} .into_any() } _ => { view! {

"No public realms available"

} .into_any() } } }) }}
// Private realm input
"/"
// Credentials
// Error message // Submit button // Divider
"or"
// Guest button // Sign up link

"Don't have an account? " "Sign up"

// Join modal { let on_join_confirm = on_join_confirm.clone(); let on_join_cancel = on_join_cancel.clone(); move || { let on_join_confirm = on_join_confirm.clone(); let on_join_cancel = on_join_cancel.clone(); pending_realm .get() .map(|realm| { view! { } }) } } } }