add initial crates and apps
This commit is contained in:
parent
5c87ba3519
commit
1ca300098f
113 changed files with 28169 additions and 0 deletions
139
crates/chattyness-admin-ui/src/pages/props_detail.rs
Normal file
139
crates/chattyness-admin-ui/src/pages/props_detail.rs
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
//! Prop detail page component.
|
||||
|
||||
use leptos::prelude::*;
|
||||
use leptos_router::hooks::use_params_map;
|
||||
|
||||
use crate::components::{Card, DetailGrid, DetailItem, PageHeader};
|
||||
use crate::hooks::use_fetch_if;
|
||||
use crate::models::PropDetail;
|
||||
|
||||
/// Prop detail page component.
|
||||
#[component]
|
||||
pub fn PropsDetailPage() -> impl IntoView {
|
||||
let params = use_params_map();
|
||||
let prop_id = move || params.get().get("prop_id").unwrap_or_default();
|
||||
let initial_prop_id = params.get_untracked().get("prop_id").unwrap_or_default();
|
||||
|
||||
let prop = use_fetch_if::<PropDetail>(
|
||||
move || !prop_id().is_empty(),
|
||||
move || format!("/api/admin/props/{}", prop_id()),
|
||||
);
|
||||
|
||||
view! {
|
||||
<PageHeader title="Prop Details" subtitle=initial_prop_id>
|
||||
<a href="/admin/props" class="btn btn-secondary">"Back to Props"</a>
|
||||
</PageHeader>
|
||||
|
||||
<Suspense fallback=|| view! { <p>"Loading prop..."</p> }>
|
||||
{move || {
|
||||
prop.get().map(|maybe_prop| {
|
||||
match maybe_prop {
|
||||
Some(p) => view! {
|
||||
<PropDetailView prop=p />
|
||||
}.into_any(),
|
||||
None => view! {
|
||||
<Card>
|
||||
<p class="text-error">"Prop not found or you don't have permission to view."</p>
|
||||
</Card>
|
||||
}.into_any()
|
||||
}
|
||||
})
|
||||
}}
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn PropDetailView(prop: PropDetail) -> impl IntoView {
|
||||
let asset_url = format!("/assets/{}", prop.asset_path);
|
||||
let tags_display = if prop.tags.is_empty() {
|
||||
"None".to_string()
|
||||
} else {
|
||||
prop.tags.join(", ")
|
||||
};
|
||||
|
||||
view! {
|
||||
<Card>
|
||||
<div class="prop-header" style="display: flex; gap: 24px; align-items: flex-start;">
|
||||
<div class="prop-preview" style="flex-shrink: 0;">
|
||||
<img
|
||||
src=asset_url
|
||||
alt=prop.name.clone()
|
||||
style="width: 128px; height: 128px; object-fit: contain; border: 1px solid var(--color-border, #334155); border-radius: 8px; background: var(--color-bg-tertiary, #0f172a);"
|
||||
/>
|
||||
</div>
|
||||
<div class="prop-info" style="flex: 1;">
|
||||
<h2 style="margin: 0 0 8px 0;">{prop.name.clone()}</h2>
|
||||
<p class="text-muted" style="margin: 0;"><code>{prop.slug.clone()}</code></p>
|
||||
{prop.description.clone().map(|desc| view! {
|
||||
<p style="margin-top: 12px; color: var(--color-text-secondary, #94a3b8);">{desc}</p>
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="Details">
|
||||
<DetailGrid>
|
||||
<DetailItem label="Prop ID">
|
||||
<code>{prop.id.clone()}</code>
|
||||
</DetailItem>
|
||||
<DetailItem label="Tags">
|
||||
{tags_display}
|
||||
</DetailItem>
|
||||
<DetailItem label="Default Layer">
|
||||
{prop.default_layer.clone().unwrap_or_else(|| "Not set".to_string())}
|
||||
</DetailItem>
|
||||
<DetailItem label="Default Position">
|
||||
{match prop.default_position {
|
||||
Some(pos) => {
|
||||
let labels = ["Top-Left", "Top-Center", "Top-Right",
|
||||
"Middle-Left", "Center", "Middle-Right",
|
||||
"Bottom-Left", "Bottom-Center", "Bottom-Right"];
|
||||
labels.get(pos as usize).map(|s| s.to_string())
|
||||
.unwrap_or_else(|| format!("{}", pos))
|
||||
},
|
||||
None => "Not set".to_string(),
|
||||
}}
|
||||
</DetailItem>
|
||||
<DetailItem label="Status">
|
||||
{if prop.is_active {
|
||||
view! { <span class="status-badge status-active">"Active"</span> }.into_any()
|
||||
} else {
|
||||
view! { <span class="status-badge status-inactive">"Inactive"</span> }.into_any()
|
||||
}}
|
||||
</DetailItem>
|
||||
</DetailGrid>
|
||||
</Card>
|
||||
|
||||
<Card title="Properties">
|
||||
<DetailGrid>
|
||||
<DetailItem label="Unique">
|
||||
{if prop.is_unique { "Yes" } else { "No" }}
|
||||
</DetailItem>
|
||||
<DetailItem label="Transferable">
|
||||
{if prop.is_transferable { "Yes" } else { "No" }}
|
||||
</DetailItem>
|
||||
<DetailItem label="Portable">
|
||||
{if prop.is_portable { "Yes" } else { "No" }}
|
||||
</DetailItem>
|
||||
</DetailGrid>
|
||||
</Card>
|
||||
|
||||
<Card title="Availability">
|
||||
<DetailGrid>
|
||||
<DetailItem label="Available From">
|
||||
{prop.available_from.clone().unwrap_or_else(|| "Always".to_string())}
|
||||
</DetailItem>
|
||||
<DetailItem label="Available Until">
|
||||
{prop.available_until.clone().unwrap_or_else(|| "No end date".to_string())}
|
||||
</DetailItem>
|
||||
<DetailItem label="Created">
|
||||
{prop.created_at.clone()}
|
||||
</DetailItem>
|
||||
<DetailItem label="Updated">
|
||||
{prop.updated_at.clone()}
|
||||
</DetailItem>
|
||||
</DetailGrid>
|
||||
</Card>
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue