//! 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
"Chattyness"
"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! {
}
}>
{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
// 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! {
}
})
}
}
}
}