388 lines
17 KiB
Rust
388 lines
17 KiB
Rust
//! Create new realm page component.
|
|
|
|
use leptos::prelude::*;
|
|
#[cfg(feature = "hydrate")]
|
|
use leptos::task::spawn_local;
|
|
|
|
use crate::components::{Card, PageHeader};
|
|
|
|
/// Realm new page component.
|
|
#[component]
|
|
pub fn RealmNewPage() -> impl IntoView {
|
|
// Form state
|
|
let (name, set_name) = signal(String::new());
|
|
let (slug, set_slug) = signal(String::new());
|
|
let (tagline, set_tagline) = signal(String::new());
|
|
let (description, set_description) = signal(String::new());
|
|
let (privacy, set_privacy) = signal("public".to_string());
|
|
let (max_users, set_max_users) = signal(100i32);
|
|
let (is_nsfw, set_is_nsfw) = signal(false);
|
|
let (allow_guest_access, set_allow_guest_access) = signal(false);
|
|
let (theme_color, set_theme_color) = signal("#7c3aed".to_string());
|
|
|
|
// Owner selection
|
|
let (owner_mode, set_owner_mode) = signal("existing".to_string());
|
|
let (owner_id, set_owner_id) = signal(String::new());
|
|
let (new_username, set_new_username) = signal(String::new());
|
|
let (new_email, set_new_email) = signal(String::new());
|
|
let (new_display_name, set_new_display_name) = signal(String::new());
|
|
|
|
// UI state
|
|
let (message, set_message) = signal(Option::<(String, bool)>::None);
|
|
let (pending, set_pending) = signal(false);
|
|
let (created_slug, _set_created_slug) = signal(Option::<String>::None);
|
|
let (temp_password, _set_temp_password) = signal(Option::<String>::None);
|
|
#[cfg(feature = "hydrate")]
|
|
let (set_created_slug, set_temp_password) = (_set_created_slug, _set_temp_password);
|
|
let (slug_auto, set_slug_auto) = signal(true);
|
|
|
|
let update_name = move |ev: leptos::ev::Event| {
|
|
let new_name = event_target_value(&ev);
|
|
set_name.set(new_name.clone());
|
|
if slug_auto.get() {
|
|
let new_slug = new_name
|
|
.to_lowercase()
|
|
.chars()
|
|
.map(|c| if c.is_alphanumeric() { c } else { '-' })
|
|
.collect::<String>()
|
|
.trim_matches('-')
|
|
.to_string();
|
|
set_slug.set(new_slug);
|
|
}
|
|
};
|
|
|
|
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 mut data = serde_json::json!({
|
|
"name": name.get(),
|
|
"slug": slug.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(),
|
|
"theme_color": theme_color.get()
|
|
});
|
|
|
|
if owner_mode.get() == "existing" {
|
|
if owner_id.get().is_empty() {
|
|
set_message.set(Some(("Please enter an owner User ID".to_string(), false)));
|
|
set_pending.set(false);
|
|
return;
|
|
}
|
|
data["owner_id"] = serde_json::json!(owner_id.get());
|
|
} else {
|
|
if new_username.get().is_empty() || new_email.get().is_empty() || new_display_name.get().is_empty() {
|
|
set_message.set(Some(("Please fill in all new owner fields".to_string(), false)));
|
|
set_pending.set(false);
|
|
return;
|
|
}
|
|
data["new_owner"] = serde_json::json!({
|
|
"username": new_username.get(),
|
|
"email": new_email.get(),
|
|
"display_name": new_display_name.get()
|
|
});
|
|
}
|
|
|
|
spawn_local(async move {
|
|
let response = Request::post("/api/admin/realms")
|
|
.json(&data)
|
|
.unwrap()
|
|
.send()
|
|
.await;
|
|
|
|
set_pending.set(false);
|
|
|
|
match response {
|
|
Ok(resp) if resp.ok() => {
|
|
#[derive(serde::Deserialize)]
|
|
struct CreateResponse {
|
|
slug: String,
|
|
owner_temporary_password: Option<String>,
|
|
}
|
|
if let Ok(result) = resp.json::<CreateResponse>().await {
|
|
set_created_slug.set(Some(result.slug));
|
|
set_temp_password.set(result.owner_temporary_password);
|
|
set_message.set(Some(("Realm created successfully!".to_string(), true)));
|
|
}
|
|
}
|
|
Ok(resp) => {
|
|
#[derive(serde::Deserialize)]
|
|
struct ErrorResp {
|
|
error: String,
|
|
}
|
|
if let Ok(err) = resp.json::<ErrorResp>().await {
|
|
set_message.set(Some((err.error, false)));
|
|
} else {
|
|
set_message.set(Some(("Failed to create realm".to_string(), false)));
|
|
}
|
|
}
|
|
Err(_) => {
|
|
set_message.set(Some(("Network error".to_string(), false)));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
view! {
|
|
<PageHeader title="Create New Realm" subtitle="Create a new realm space">
|
|
<a href="/admin/realms" class="btn btn-secondary">"Back to Realms"</a>
|
|
</PageHeader>
|
|
|
|
<Card>
|
|
<form on:submit=on_submit>
|
|
<h3 class="section-title">"Realm Details"</h3>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="name" class="form-label">
|
|
"Realm Name" <span class="required">"*"</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="name"
|
|
required=true
|
|
class="form-input"
|
|
placeholder="My Awesome Realm"
|
|
prop:value=move || name.get()
|
|
on:input=update_name
|
|
/>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="slug" class="form-label">
|
|
"Slug (URL)" <span class="required">"*"</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
id="slug"
|
|
required=true
|
|
pattern="[a-z0-9][a-z0-9\\-]*[a-z0-9]|[a-z0-9]"
|
|
class="form-input"
|
|
placeholder="my-realm"
|
|
prop:value=move || slug.get()
|
|
on:input=move |ev| {
|
|
set_slug_auto.set(false);
|
|
set_slug.set(event_target_value(&ev));
|
|
}
|
|
/>
|
|
<small class="form-help">"Lowercase letters, numbers, hyphens only"</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"
|
|
placeholder="Detailed description of the realm"
|
|
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>
|
|
|
|
<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>
|
|
|
|
<h3 class="section-title">"Realm Owner"</h3>
|
|
|
|
<div class="tab-buttons">
|
|
<button
|
|
type="button"
|
|
class=move || if owner_mode.get() == "existing" { "btn btn-primary" } else { "btn btn-secondary" }
|
|
on:click=move |_| set_owner_mode.set("existing".to_string())
|
|
>
|
|
"Existing User"
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class=move || if owner_mode.get() == "new" { "btn btn-primary" } else { "btn btn-secondary" }
|
|
on:click=move |_| set_owner_mode.set("new".to_string())
|
|
>
|
|
"Create New User"
|
|
</button>
|
|
</div>
|
|
|
|
<Show when=move || owner_mode.get() == "existing">
|
|
<div class="form-group">
|
|
<label for="owner_id" class="form-label">"Owner User ID"</label>
|
|
<input
|
|
type="text"
|
|
id="owner_id"
|
|
class="form-input"
|
|
placeholder="UUID of existing user"
|
|
prop:value=move || owner_id.get()
|
|
on:input=move |ev| set_owner_id.set(event_target_value(&ev))
|
|
/>
|
|
</div>
|
|
</Show>
|
|
|
|
<Show when=move || owner_mode.get() == "new">
|
|
<p class="text-muted">"A random temporary password will be generated for the new owner."</p>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="new_username" class="form-label">"Username"</label>
|
|
<input
|
|
type="text"
|
|
id="new_username"
|
|
minlength=3
|
|
maxlength=32
|
|
class="form-input"
|
|
placeholder="username"
|
|
prop:value=move || new_username.get()
|
|
on:input=move |ev| set_new_username.set(event_target_value(&ev))
|
|
/>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="new_email" class="form-label">"Email"</label>
|
|
<input
|
|
type="email"
|
|
id="new_email"
|
|
class="form-input"
|
|
placeholder="user@example.com"
|
|
prop:value=move || new_email.get()
|
|
on:input=move |ev| set_new_email.set(event_target_value(&ev))
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="new_display_name" class="form-label">"Display Name"</label>
|
|
<input
|
|
type="text"
|
|
id="new_display_name"
|
|
class="form-input"
|
|
placeholder="Display Name"
|
|
prop:value=move || new_display_name.get()
|
|
on:input=move |ev| set_new_display_name.set(event_target_value(&ev))
|
|
/>
|
|
</div>
|
|
</Show>
|
|
|
|
<Show when=move || message.get().is_some()>
|
|
{move || {
|
|
let (msg, is_success) = message.get().unwrap_or_default();
|
|
let class = if is_success { "alert alert-success" } else { "alert alert-error" };
|
|
view! {
|
|
<div class=class role="alert">
|
|
<p>{msg}</p>
|
|
</div>
|
|
}
|
|
}}
|
|
</Show>
|
|
|
|
<Show when=move || created_slug.get().is_some()>
|
|
<div class="alert alert-info">
|
|
<p>
|
|
<a href=format!("/admin/realms/{}", created_slug.get().unwrap_or_default())>
|
|
"View realm"
|
|
</a>
|
|
</p>
|
|
</div>
|
|
</Show>
|
|
|
|
<Show when=move || temp_password.get().is_some()>
|
|
<div class="alert alert-warning">
|
|
<p><strong>"New Owner Temporary Password:"</strong></p>
|
|
<code class="temp-password">{move || temp_password.get().unwrap_or_default()}</code>
|
|
<p class="text-muted">"Copy this password now - it will not be shown again!"</p>
|
|
</div>
|
|
</Show>
|
|
|
|
<div class="form-actions">
|
|
<button
|
|
type="submit"
|
|
class="btn btn-primary"
|
|
disabled=move || pending.get()
|
|
>
|
|
{move || if pending.get() { "Creating..." } else { "Create Realm" }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</Card>
|
|
}
|
|
}
|