484979ad53
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.
191 lines
7.4 KiB
Rust
191 lines
7.4 KiB
Rust
use dioxus::prelude::*;
|
|
|
|
use crate::api;
|
|
use crate::app::Route;
|
|
use crate::components::plant_card::PlantCard;
|
|
|
|
#[component]
|
|
pub fn SpeciesList() -> Element {
|
|
let mut page = use_signal(|| 1i64);
|
|
let mut search = use_signal(|| String::new());
|
|
let current_page = *page.read();
|
|
let search_str = search.read().clone();
|
|
|
|
let species = use_resource(move || {
|
|
let s = search_str.clone();
|
|
async move {
|
|
let q = if s.is_empty() { None } else { Some(s.as_str()) };
|
|
api::list_species(current_page, None, q).await
|
|
}
|
|
});
|
|
|
|
rsx! {
|
|
div { class: "page",
|
|
h1 { "Species" }
|
|
|
|
div { class: "search-bar",
|
|
input {
|
|
r#type: "text",
|
|
placeholder: "Search species...",
|
|
value: "{search}",
|
|
oninput: move |e| {
|
|
search.set(e.value());
|
|
page.set(1);
|
|
},
|
|
}
|
|
}
|
|
|
|
match &*species.read() {
|
|
None => rsx! { p { "Loading..." } },
|
|
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
|
|
Some(Ok(data)) => rsx! {
|
|
div { class: "card-grid",
|
|
for s in data.data.iter() {
|
|
PlantCard {
|
|
key: "{s.id}",
|
|
slug: s.slug.clone(),
|
|
name: s.name_scientific.clone(),
|
|
name_common: s.name_en.clone(),
|
|
entity_type: "species".to_string(),
|
|
}
|
|
}
|
|
}
|
|
if data.total > data.per_page {
|
|
div { class: "pagination",
|
|
button {
|
|
disabled: current_page <= 1,
|
|
onclick: move |_| page.set(current_page - 1),
|
|
"Previous"
|
|
}
|
|
span { "Page {current_page} of {(data.total + data.per_page - 1) / data.per_page}" }
|
|
button {
|
|
disabled: current_page * data.per_page >= data.total,
|
|
onclick: move |_| page.set(current_page + 1),
|
|
"Next"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
pub fn SpeciesDetail(slug: String) -> Element {
|
|
let slug_clone = slug.clone();
|
|
let species = use_resource(move || {
|
|
let s = slug_clone.clone();
|
|
async move { api::get_species(&s).await }
|
|
});
|
|
|
|
rsx! {
|
|
div { class: "page species-detail",
|
|
match &*species.read() {
|
|
None => rsx! { p { "Loading..." } },
|
|
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
|
|
Some(Ok(s)) => {
|
|
let species_slug = s.slug.clone();
|
|
rsx! {
|
|
h1 { em { "{s.name_scientific}" } }
|
|
if let Some(ref en) = s.name_en {
|
|
p { class: "name-common", "{en}" }
|
|
}
|
|
if let Some(ref de) = s.name_de {
|
|
p { class: "name-common", "{de}" }
|
|
}
|
|
if let Some(ref desc) = s.description {
|
|
div { class: "description", "{desc}" }
|
|
}
|
|
|
|
// Info grid
|
|
div { class: "info-grid",
|
|
if let Some(ref layer) = s.plant_layer {
|
|
div { class: "info-item",
|
|
span { class: "info-label", "Layer" }
|
|
span { class: "info-value", "{layer}" }
|
|
}
|
|
}
|
|
if let Some(ref dt) = s.drought_tolerance {
|
|
div { class: "info-item",
|
|
span { class: "info-label", "Drought Tolerance" }
|
|
span { class: "info-value", "{dt}" }
|
|
}
|
|
}
|
|
if let Some(ref hz) = s.hardiness_zone_usda {
|
|
div { class: "info-item",
|
|
span { class: "info-label", "USDA Zone" }
|
|
span { class: "info-value", "{hz}" }
|
|
}
|
|
}
|
|
if let Some(rating) = s.edibility_rating {
|
|
div { class: "info-item",
|
|
span { class: "info-label", "Edibility" }
|
|
span { class: "info-value", "{rating}/5" }
|
|
}
|
|
}
|
|
if let Some(nf) = s.nitrogen_fixer {
|
|
if nf {
|
|
div { class: "info-item badge",
|
|
"Nitrogen Fixer"
|
|
}
|
|
}
|
|
}
|
|
if let Some(da) = s.dynamic_accumulator {
|
|
if da {
|
|
div { class: "info-item badge",
|
|
"Dynamic Accumulator"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cultivars for this species
|
|
h2 { "Cultivars" }
|
|
CultivarListForSpecies { species_slug: species_slug }
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
fn CultivarListForSpecies(species_slug: String) -> Element {
|
|
let slug = species_slug.clone();
|
|
let cultivars = use_resource(move || {
|
|
let s = slug.clone();
|
|
async move { api::list_cultivars(1, Some(&s), None).await }
|
|
});
|
|
|
|
rsx! {
|
|
match &*cultivars.read() {
|
|
None => rsx! { p { "Loading..." } },
|
|
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
|
|
Some(Ok(data)) => {
|
|
if data.data.is_empty() {
|
|
rsx! { p { class: "empty", "No cultivars yet." } }
|
|
} else {
|
|
rsx! {
|
|
div { class: "card-grid",
|
|
for c in data.data.iter() {
|
|
div { class: "plant-card",
|
|
Link { to: Route::CultivarDetail { slug: c.slug.clone() },
|
|
strong { "{c.name}" }
|
|
}
|
|
if let Some(ref en) = c.name_en {
|
|
p { class: "card-common", "{en}" }
|
|
}
|
|
if c.is_organic {
|
|
span { class: "badge organic", "Organic" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|