Files
herbapi/herbapi-ui/src/app.rs
T
florian.berthold 484979ad53 Initial HerbAPI implementation
Rust/Axum REST API (herbapi-api) with PostgreSQL, S3/Garage, OIDC auth.
Dioxus 0.7 WASM frontend (herbapi-ui) with sidebar layout and botanical reference style.
9 SQL migrations covering families, species, cultivars, suppliers, companions, images, users, API tokens.
2026-03-14 00:02:29 +01:00

109 lines
3.5 KiB
Rust

use dioxus::prelude::*;
use crate::api;
use crate::types::MeResponse;
#[derive(Routable, Clone, Debug, PartialEq)]
#[rustfmt::skip]
pub enum Route {
#[layout(Layout)]
#[route("/")]
Home {},
#[route("/families")]
FamilyList {},
#[route("/families/:slug")]
FamilyDetail { slug: String },
#[route("/species")]
SpeciesList {},
#[route("/species/:slug")]
SpeciesDetail { slug: String },
#[route("/cultivars")]
CultivarList {},
#[route("/cultivars/:slug")]
CultivarDetail { slug: String },
#[route("/suppliers")]
SupplierList {},
#[route("/suppliers/:slug")]
SupplierDetail { slug: String },
#[route("/search")]
SearchPage {},
#[end_layout]
#[route("/:..segments")]
NotFound { segments: Vec<String> },
}
#[component]
pub fn App() -> Element {
rsx! {
Router::<Route> {}
}
}
#[component]
fn Layout() -> Element {
// Try to get current user (may be None for public access)
let auth = use_resource(|| async { api::get_current_user().await.ok() });
let user: Option<MeResponse> = auth.read().as_ref().and_then(|r| r.clone());
rsx! {
div { class: "app-layout",
nav { class: "sidebar",
div { class: "sidebar-brand",
span { class: "brand-icon", "\u{1F33F}" }
div { class: "brand-text-group",
span { class: "brand-text", "HerbAPI" }
span { class: "brand-sub", "Plant Database" }
}
}
div { class: "sidebar-nav",
NavLink { to: Route::Home {}, label: "Home" }
NavLink { to: Route::FamilyList {}, label: "Families" }
NavLink { to: Route::SpeciesList {}, label: "Species" }
NavLink { to: Route::CultivarList {}, label: "Cultivars" }
NavLink { to: Route::SupplierList {}, label: "Suppliers" }
NavLink { to: Route::SearchPage {}, label: "Search" }
}
div { class: "sidebar-user",
if let Some(ref u) = user {
span { class: "user-name", "{u.nickname.as_deref().or(u.name.as_deref()).unwrap_or(&u.email)}" }
a { class: "logout-link", href: "/auth/oidc/logout", "Logout" }
} else {
a { class: "login-link", href: "/auth/oidc/login", "Login" }
}
}
}
main { class: "content",
Outlet::<Route> {}
}
}
}
}
#[component]
fn NavLink(to: Route, label: &'static str) -> Element {
rsx! {
Link { to: to, class: "nav-link",
span { class: "nav-label", "{label}" }
}
}
}
#[component]
fn NotFound(segments: Vec<String>) -> Element {
rsx! {
div { class: "not-found",
h1 { "404" }
p { "Page not found: /{segments.join(\"/\")}" }
Link { to: Route::Home {}, "Back to Home" }
}
}
}
// Re-export page components for the router
pub use crate::pages::cultivars::{CultivarDetail, CultivarList};
pub use crate::pages::families::{FamilyDetail, FamilyList};
pub use crate::pages::home::Home;
pub use crate::pages::search::SearchPage;
pub use crate::pages::species::{SpeciesDetail, SpeciesList};
pub use crate::pages::suppliers::{SupplierDetail, SupplierList};