add initial crates and apps
This commit is contained in:
parent
5c87ba3519
commit
1ca300098f
113 changed files with 28169 additions and 0 deletions
388
crates/chattyness-admin-ui/src/pages/realm_new.rs
Normal file
388
crates/chattyness-admin-ui/src/pages/realm_new.rs
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
//! 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>
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue