add initial crates and apps

This commit is contained in:
Evan Carroll 2026-01-12 15:34:40 -06:00
parent 5c87ba3519
commit 1ca300098f
113 changed files with 28169 additions and 0 deletions

View 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>
}
}