312 lines
12 KiB
Rust
312 lines
12 KiB
Rust
//! Realm detail/edit page component.
|
|
|
|
use leptos::prelude::*;
|
|
#[cfg(feature = "hydrate")]
|
|
use leptos::task::spawn_local;
|
|
use leptos_router::hooks::use_params_map;
|
|
|
|
use crate::components::{
|
|
Card, DetailGrid, DetailItem, MessageAlert, NsfwBadge, PageHeader, PrivacyBadge,
|
|
};
|
|
use crate::hooks::use_fetch_if;
|
|
use crate::models::RealmDetail;
|
|
use crate::utils::get_api_base;
|
|
|
|
/// Realm detail page component.
|
|
#[component]
|
|
pub fn RealmDetailPage() -> impl IntoView {
|
|
let params = use_params_map();
|
|
let slug = move || params.get().get("slug").unwrap_or_default();
|
|
let initial_slug = params.get_untracked().get("slug").unwrap_or_default();
|
|
|
|
let (message, set_message) = signal(Option::<(String, bool)>::None);
|
|
|
|
let realm = use_fetch_if::<RealmDetail>(
|
|
move || !slug().is_empty(),
|
|
move || format!("{}/realms/{}", get_api_base(), slug()),
|
|
);
|
|
|
|
let slug_for_scenes = initial_slug.clone();
|
|
|
|
view! {
|
|
<PageHeader title="Realm Details" subtitle=format!("/{}", initial_slug)>
|
|
<a href=format!("/admin/realms/{}/scenes", slug_for_scenes) class="btn btn-primary">"Manage Scenes"</a>
|
|
<a href="/admin/realms" class="btn btn-secondary">"Back to Realms"</a>
|
|
</PageHeader>
|
|
|
|
<Suspense fallback=|| view! { <p>"Loading realm..."</p> }>
|
|
{move || {
|
|
realm.get().map(|maybe_realm| {
|
|
match maybe_realm {
|
|
Some(r) => view! {
|
|
<RealmDetailView realm=r message=message set_message=set_message />
|
|
}.into_any(),
|
|
None => view! {
|
|
<Card>
|
|
<p class="text-error">"Realm not found or you don't have permission to view."</p>
|
|
</Card>
|
|
}.into_any()
|
|
}
|
|
})
|
|
}}
|
|
</Suspense>
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
#[allow(unused_variables)]
|
|
fn RealmDetailView(
|
|
realm: RealmDetail,
|
|
message: ReadSignal<Option<(String, bool)>>,
|
|
set_message: WriteSignal<Option<(String, bool)>>,
|
|
) -> impl IntoView {
|
|
#[cfg(feature = "hydrate")]
|
|
let slug = realm.slug.clone();
|
|
let slug_display = realm.slug.clone();
|
|
let (pending, set_pending) = signal(false);
|
|
|
|
// Form state
|
|
let (name, set_name) = signal(realm.name.clone());
|
|
let (tagline, set_tagline) = signal(realm.tagline.clone().unwrap_or_default());
|
|
let (description, set_description) = signal(realm.description.clone().unwrap_or_default());
|
|
let (privacy, set_privacy) = signal(realm.privacy.clone());
|
|
let (max_users, set_max_users) = signal(realm.max_users);
|
|
let (is_nsfw, set_is_nsfw) = signal(realm.is_nsfw);
|
|
let (allow_guest_access, set_allow_guest_access) = signal(realm.allow_guest_access);
|
|
let (allow_user_teleport, set_allow_user_teleport) = signal(realm.allow_user_teleport);
|
|
let (theme_color, set_theme_color) = signal(
|
|
realm
|
|
.theme_color
|
|
.clone()
|
|
.unwrap_or_else(|| "#7c3aed".to_string()),
|
|
);
|
|
|
|
let on_submit = move |ev: leptos::ev::SubmitEvent| {
|
|
ev.prevent_default();
|
|
set_pending.set(true);
|
|
set_message.set(None);
|
|
|
|
#[cfg(feature = "hydrate")]
|
|
{
|
|
use gloo_net::http::Request;
|
|
|
|
let api_base = get_api_base();
|
|
let slug = slug.clone();
|
|
let data = serde_json::json!({
|
|
"name": name.get(),
|
|
"description": if description.get().is_empty() { None::<String> } else { Some(description.get()) },
|
|
"tagline": if tagline.get().is_empty() { None::<String> } else { Some(tagline.get()) },
|
|
"privacy": privacy.get(),
|
|
"is_nsfw": is_nsfw.get(),
|
|
"max_users": max_users.get(),
|
|
"allow_guest_access": allow_guest_access.get(),
|
|
"allow_user_teleport": allow_user_teleport.get(),
|
|
"theme_color": theme_color.get()
|
|
});
|
|
|
|
spawn_local(async move {
|
|
let response = Request::put(&format!("{}/realms/{}", api_base, slug))
|
|
.json(&data)
|
|
.unwrap()
|
|
.send()
|
|
.await;
|
|
|
|
set_pending.set(false);
|
|
|
|
match response {
|
|
Ok(resp) if resp.ok() => {
|
|
set_message.set(Some(("Realm updated successfully!".to_string(), true)));
|
|
}
|
|
Ok(_) => {
|
|
set_message.set(Some(("Failed to update realm".to_string(), false)));
|
|
}
|
|
Err(_) => {
|
|
set_message.set(Some(("Network error".to_string(), false)));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
view! {
|
|
<Card>
|
|
<div class="realm-header">
|
|
<div class="realm-info">
|
|
<h2>{realm.name.clone()}</h2>
|
|
<p class="text-muted">{realm.tagline.clone().unwrap_or_default()}</p>
|
|
</div>
|
|
<div class="realm-badges">
|
|
<PrivacyBadge privacy=realm.privacy.clone() />
|
|
{if realm.is_nsfw {
|
|
view! { <NsfwBadge /> }.into_any()
|
|
} else {
|
|
view! {}.into_any()
|
|
}}
|
|
</div>
|
|
</div>
|
|
|
|
<DetailGrid>
|
|
<DetailItem label="Owner">
|
|
<a href=format!("/admin/users/{}", realm.owner_id) class="table-link">
|
|
{realm.owner_display_name.clone()} " (@" {realm.owner_username.clone()} ")"
|
|
</a>
|
|
</DetailItem>
|
|
<DetailItem label="Members">
|
|
{realm.member_count.to_string()}
|
|
</DetailItem>
|
|
<DetailItem label="Current Users">
|
|
{realm.current_user_count.to_string()}
|
|
</DetailItem>
|
|
<DetailItem label="Max Users">
|
|
{realm.max_users.to_string()}
|
|
</DetailItem>
|
|
<DetailItem label="Created">
|
|
{realm.created_at.clone()}
|
|
</DetailItem>
|
|
<DetailItem label="Updated">
|
|
{realm.updated_at.clone()}
|
|
</DetailItem>
|
|
</DetailGrid>
|
|
</Card>
|
|
|
|
<Card title="Edit Realm Settings">
|
|
<form on:submit=on_submit>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="name" class="form-label">"Realm Name"</label>
|
|
<input
|
|
type="text"
|
|
id="name"
|
|
required=true
|
|
class="form-input"
|
|
prop:value=move || name.get()
|
|
on:input=move |ev| set_name.set(event_target_value(&ev))
|
|
/>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">"Slug (URL)"</label>
|
|
<input
|
|
type="text"
|
|
value=slug_display
|
|
class="form-input"
|
|
disabled=true
|
|
/>
|
|
<small class="form-help">"Slug cannot be changed"</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="tagline" class="form-label">"Tagline"</label>
|
|
<input
|
|
type="text"
|
|
id="tagline"
|
|
class="form-input"
|
|
placeholder="A short description"
|
|
prop:value=move || tagline.get()
|
|
on:input=move |ev| set_tagline.set(event_target_value(&ev))
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="description" class="form-label">"Description"</label>
|
|
<textarea
|
|
id="description"
|
|
class="form-textarea"
|
|
prop:value=move || description.get()
|
|
on:input=move |ev| set_description.set(event_target_value(&ev))
|
|
></textarea>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="privacy" class="form-label">"Privacy"</label>
|
|
<select
|
|
id="privacy"
|
|
class="form-select"
|
|
on:change=move |ev| set_privacy.set(event_target_value(&ev))
|
|
>
|
|
<option value="public" selected=move || privacy.get() == "public">"Public"</option>
|
|
<option value="unlisted" selected=move || privacy.get() == "unlisted">"Unlisted"</option>
|
|
<option value="private" selected=move || privacy.get() == "private">"Private"</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="max_users" class="form-label">"Max Users"</label>
|
|
<input
|
|
type="number"
|
|
id="max_users"
|
|
min=1
|
|
max=10000
|
|
class="form-input"
|
|
prop:value=move || max_users.get()
|
|
on:input=move |ev| {
|
|
if let Ok(v) = event_target_value(&ev).parse() {
|
|
set_max_users.set(v);
|
|
}
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="checkbox-group">
|
|
<label class="checkbox-label">
|
|
<input
|
|
type="checkbox"
|
|
class="form-checkbox"
|
|
prop:checked=move || is_nsfw.get()
|
|
on:change=move |ev| set_is_nsfw.set(event_target_checked(&ev))
|
|
/>
|
|
"NSFW Content"
|
|
</label>
|
|
</div>
|
|
<div class="checkbox-group">
|
|
<label class="checkbox-label">
|
|
<input
|
|
type="checkbox"
|
|
class="form-checkbox"
|
|
prop:checked=move || allow_guest_access.get()
|
|
on:change=move |ev| set_allow_guest_access.set(event_target_checked(&ev))
|
|
/>
|
|
"Allow Guest Access"
|
|
</label>
|
|
</div>
|
|
<div class="checkbox-group">
|
|
<label class="checkbox-label">
|
|
<input
|
|
type="checkbox"
|
|
class="form-checkbox"
|
|
prop:checked=move || allow_user_teleport.get()
|
|
on:change=move |ev| set_allow_user_teleport.set(event_target_checked(&ev))
|
|
/>
|
|
"Allow User Teleport"
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="theme_color" class="form-label">"Theme Color"</label>
|
|
<input
|
|
type="color"
|
|
id="theme_color"
|
|
class="form-color"
|
|
prop:value=move || theme_color.get()
|
|
on:input=move |ev| set_theme_color.set(event_target_value(&ev))
|
|
/>
|
|
</div>
|
|
|
|
<MessageAlert message=message />
|
|
|
|
<div class="form-actions">
|
|
<button
|
|
type="submit"
|
|
class="btn btn-primary"
|
|
disabled=move || pending.get()
|
|
>
|
|
{move || if pending.get() { "Saving..." } else { "Save Changes" }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</Card>
|
|
}
|
|
}
|