use dioxus::prelude::*; use gloo_storage::{LocalStorage, Storage}; use std::collections::HashMap; use uuid::Uuid; use crate::api; use crate::app::{Lang, Route}; use crate::components::plant_card::PlantCard; use crate::components::table_controls::*; use crate::i18n::{pick_desc, pick_name, t, t_val}; const STORAGE_KEY_COLS: &str = "herbapi_species_cols"; const STORAGE_KEY_PP: &str = "herbapi_species_pp"; const STORAGE_KEY_VIEW: &str = "herbapi_species_view"; fn species_columns() -> Vec { vec![ ColumnDef { key: "name_scientific", label: "Scientific Name", default_visible: true }, ColumnDef { key: "common_name", label: "Common Name", default_visible: true }, ColumnDef { key: "name_de", label: "German", default_visible: false }, ColumnDef { key: "name_en", label: "English", default_visible: false }, ColumnDef { key: "family", label: "Family", default_visible: true }, ColumnDef { key: "plant_layer", label: "Layer", default_visible: true }, ColumnDef { key: "nitrogen_fixer", label: "N-Fixer", default_visible: true }, ColumnDef { key: "dynamic_accumulator", label: "Dyn. Accum.", default_visible: true }, ColumnDef { key: "food_uses", label: "Food Uses", default_visible: false }, ColumnDef { key: "edibility_rating", label: "Edibility", default_visible: false }, ColumnDef { key: "drought_tolerance", label: "Drought Tol.", default_visible: false }, ColumnDef { key: "hardiness_zone_usda", label: "USDA Zone", default_visible: false }, ColumnDef { key: "nectar_value", label: "Nectar", default_visible: false }, ColumnDef { key: "wild_bee_count", label: "Wild Bees", default_visible: false }, ColumnDef { key: "native_status", label: "Native Status", default_visible: false }, ] } #[component] pub fn SpeciesList() -> Element { let lang = use_context::().0; let columns = species_columns(); let mut page = use_signal(|| 1i64); let per_page = use_signal(|| load_per_page(STORAGE_KEY_PP, 25)); let mut search = use_signal(|| String::new()); let visible_cols = use_signal(|| load_visible_columns(STORAGE_KEY_COLS, &species_columns())); let mut table_view = use_signal(|| { LocalStorage::get::(STORAGE_KEY_VIEW).unwrap_or(true) }); // Filter signals let mut filter_layer = use_signal(|| String::new()); let mut filter_nfixer = use_signal(|| false); let mut filter_dynacc = use_signal(|| false); let mut filter_drought = use_signal(|| String::new()); // Fetch family map for resolving family_id -> name let family_map = use_resource(move || async move { let mut map = HashMap::::new(); if let Ok(resp) = api::list_families(1, 100, None).await { for f in resp.data { map.insert(f.id, f.name_scientific); } let total_pages = (resp.total + resp.per_page - 1) / resp.per_page; for p in 2..=total_pages { if let Ok(r) = api::list_families(p, 100, None).await { for f in r.data { map.insert(f.id, f.name_scientific); } } } } map }); let species = use_resource(move || { let s = search.read().clone(); let p = *page.read(); let pp = *per_page.read(); let layer = filter_layer.read().clone(); let nfixer = *filter_nfixer.read(); let dynacc = *filter_dynacc.read(); let drought = filter_drought.read().clone(); async move { let filters = api::SpeciesFilters { search: if s.is_empty() { None } else { Some(s) }, plant_layer: if layer.is_empty() { None } else { Some(layer) }, nitrogen_fixer: if nfixer { Some(true) } else { None }, dynamic_accumulator: if dynacc { Some(true) } else { None }, drought_tolerance: if drought.is_empty() { None } else { Some(drought) }, ..Default::default() }; api::list_species_filtered(p, pp, &filters).await } }); let current_page = *page.read(); let is_table = *table_view.read(); let l = lang.read().clone(); rsx! { div { class: "page", h1 { "{t(&l, \"page.species\")}" } div { class: "table-toolbar", div { class: "search-bar", input { r#type: "text", placeholder: "{t(&l, \"search.placeholder_species\")}", value: "{search}", oninput: move |e| { search.set(e.value()); page.set(1); }, } } div { class: "toolbar-right", button { class: "view-toggle-btn", onclick: move |_| { let new_val = !*table_view.read(); table_view.set(new_val); let _ = LocalStorage::set(STORAGE_KEY_VIEW, new_val); }, if is_table { "{t(&l, \"btn.card_view\")}" } else { "{t(&l, \"btn.table_view\")}" } } PerPageSelector { per_page: per_page, page: page, storage_key: STORAGE_KEY_PP.to_string(), } } } // Filter bar div { class: "filter-bar", div { class: "filter-group", label { "{t(&l, \"filter.layer\")}" } select { value: "{filter_layer}", onchange: move |e| { filter_layer.set(e.value()); page.set(1); }, option { value: "", "{t(&l, \"filter.all\")}" } option { value: "canopy", "{t_val(&l, \"canopy\")}" } option { value: "understory", "{t_val(&l, \"understory\")}" } option { value: "shrub", "{t_val(&l, \"shrub\")}" } option { value: "herbaceous", "{t_val(&l, \"herbaceous\")}" } option { value: "ground_cover", "{t_val(&l, \"ground_cover\")}" } option { value: "vine", "{t_val(&l, \"vine\")}" } option { value: "root", "{t_val(&l, \"root\")}" } } } div { class: "filter-group", label { "{t(&l, \"filter.drought_tol\")}" } select { value: "{filter_drought}", onchange: move |e| { filter_drought.set(e.value()); page.set(1); }, option { value: "", "{t(&l, \"filter.all\")}" } option { value: "none", "{t_val(&l, \"none\")}" } option { value: "low", "{t_val(&l, \"low\")}" } option { value: "moderate", "{t_val(&l, \"moderate\")}" } option { value: "high", "{t_val(&l, \"high\")}" } option { value: "very_high", "{t_val(&l, \"very_high\")}" } } } div { class: "filter-group filter-checkbox", label { input { r#type: "checkbox", checked: *filter_nfixer.read(), onchange: move |e| { filter_nfixer.set(e.checked()); page.set(1); }, } "{t(&l, \"field.n_fixer\")}" } } div { class: "filter-group filter-checkbox", label { input { r#type: "checkbox", checked: *filter_dynacc.read(), onchange: move |e| { filter_dynacc.set(e.checked()); page.set(1); }, } "{t(&l, \"field.dyn_accum\")}" } } } if is_table { ColumnToggle { columns: columns.clone(), visible: visible_cols, storage_key: STORAGE_KEY_COLS.to_string(), } } match &*species.read() { None => rsx! { p { "{t(&l, \"loading\")}" } }, Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } }, Some(Ok(data)) => { let fmap_read = family_map.read(); let empty_map: HashMap = HashMap::new(); let fm = match &*fmap_read { Some(m) => m, _ => &empty_map, }; rsx! { p { class: "result-count", "{data.total} {t(&l, \"nav.species\").to_lowercase()}" } if is_table { { let vis = visible_cols.read(); rsx! { div { class: "table-wrap", table { thead { tr { if is_col_visible(&vis, "name_scientific") { th { "{t(&l, \"field.scientific_name\")}" } } if is_col_visible(&vis, "common_name") { th { "{t(&l, \"field.common_name\")}" } } if is_col_visible(&vis, "name_de") { th { "{t(&l, \"field.name_de\")}" } } if is_col_visible(&vis, "name_en") { th { "{t(&l, \"field.name_en\")}" } } if is_col_visible(&vis, "family") { th { "{t(&l, \"field.family\")}" } } if is_col_visible(&vis, "plant_layer") { th { "{t(&l, \"field.layer\")}" } } if is_col_visible(&vis, "nitrogen_fixer") { th { "{t(&l, \"field.n_fixer\")}" } } if is_col_visible(&vis, "dynamic_accumulator") { th { "{t(&l, \"field.dyn_accum\")}" } } if is_col_visible(&vis, "food_uses") { th { "{t(&l, \"field.food_uses\")}" } } if is_col_visible(&vis, "edibility_rating") { th { "{t(&l, \"field.edibility\")}" } } if is_col_visible(&vis, "drought_tolerance") { th { "{t(&l, \"field.drought_tol\")}" } } if is_col_visible(&vis, "hardiness_zone_usda") { th { "{t(&l, \"field.usda_zone\")}" } } if is_col_visible(&vis, "nectar_value") { th { "{t(&l, \"field.nectar\")}" } } if is_col_visible(&vis, "wild_bee_count") { th { "{t(&l, \"field.wild_bees\")}" } } if is_col_visible(&vis, "native_status") { th { "{t(&l, \"field.native_status\")}" } } } } tbody { for s in data.data.iter() { { let family_name: &str = fm.get(&s.family_id).map(String::as_str).unwrap_or("-"); let common = pick_name(&lang.read(), &s.name_de, &s.name_en, &s.name_scientific); rsx! { tr { if is_col_visible(&vis, "name_scientific") { td { Link { to: Route::SpeciesDetail { slug: s.slug.clone() }, em { "{s.name_scientific}" } } } } if is_col_visible(&vis, "common_name") { td { "{common}" } } if is_col_visible(&vis, "name_de") { td { "{s.name_de.as_deref().unwrap_or(\"-\")}" } } if is_col_visible(&vis, "name_en") { td { "{s.name_en.as_deref().unwrap_or(\"-\")}" } } if is_col_visible(&vis, "family") { td { class: "species-name", em { "{family_name}" } } } if is_col_visible(&vis, "plant_layer") { td { "{s.plant_layer.as_deref().map(|v| t_val(&l, v)).unwrap_or(\"-\")}" } } if is_col_visible(&vis, "nitrogen_fixer") { td { match s.nitrogen_fixer { Some(true) => rsx! { span { class: "badge organic", "{t(&l, \"yes\")}" } }, Some(false) => rsx! { "-" }, None => rsx! { "-" }, } } } if is_col_visible(&vis, "dynamic_accumulator") { td { match s.dynamic_accumulator { Some(true) => rsx! { span { class: "badge organic", "{t(&l, \"yes\")}" } }, Some(false) => rsx! { "-" }, None => rsx! { "-" }, } } } if is_col_visible(&vis, "food_uses") { td { class: "cell-truncated", "{s.food_uses.as_deref().map(|u| truncate(u, 60)).unwrap_or_else(|| \"-\".to_string())}" } } if is_col_visible(&vis, "edibility_rating") { td { match s.edibility_rating { Some(r) => rsx! { "{r}/5" }, None => rsx! { "-" }, } } } if is_col_visible(&vis, "drought_tolerance") { td { "{s.drought_tolerance.as_deref().map(|v| t_val(&l, v)).unwrap_or(\"-\")}" } } if is_col_visible(&vis, "hardiness_zone_usda") { td { "{s.hardiness_zone_usda.as_deref().unwrap_or(\"-\")}" } } if is_col_visible(&vis, "nectar_value") { td { match s.nectar_value { Some(v) => rsx! { "{v}/4" }, None => rsx! { "-" }, } } } if is_col_visible(&vis, "wild_bee_count") { td { match s.wild_bee_count { Some(v) => rsx! { "{v}" }, None => rsx! { "-" }, } } } if is_col_visible(&vis, "native_status") { td { match &s.native_status { Some(ns) if !ns.is_empty() => { let badge_class = match ns.as_str() { s if s.contains("Wildform") || s.contains("wildform") || s.contains("heimisch") => "native-badge native-badge-heimisch", s if s.contains("rchäophyt") => "native-badge native-badge-archaeophyt", s if s.contains("eophyt") => "native-badge native-badge-neophyt", _ => "native-badge", }; let translated = t_val(&l, ns); rsx! { span { class: "{badge_class}", "{translated}" } } }, _ => rsx! { "-" }, } } } } } } } } } } } } } else { div { class: "card-grid", for s in data.data.iter() { { let card_common = pick_name(&lang.read(), &s.name_de, &s.name_en, &s.name_scientific); let show_common = if card_common == s.name_scientific { None } else { Some(card_common) }; rsx! { PlantCard { key: "{s.id}", slug: s.slug.clone(), name: s.name_scientific.clone(), name_common: show_common, 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), "{t(&l, \"btn.previous\")}" } span { "{t(&l, \"page\")} {current_page} {t(&l, \"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), "{t(&l, \"btn.next\")}" } } } } }, } } } } #[component] pub fn SpeciesDetail(slug: String) -> Element { let lang = use_context::().0; let slug_clone = slug.clone(); let species = use_resource(move || { let s = slug_clone.clone(); async move { api::get_species(&s).await } }); // Fetch family for family link let family_data = use_resource(move || { let sp = species.read().clone(); async move { if let Some(Ok(s)) = sp { // look up by iterating families let resp = api::list_families(1, 200, None).await.ok(); if let Some(r) = resp { for f in &r.data { if f.id == s.family_id { return Some(f.clone()); } } } } None } }); // Fetch images for this species let species_images = use_resource(move || { let sp = species.read().clone(); async move { if let Some(Ok(s)) = sp { api::get_images("species", s.id).await.ok().unwrap_or_default() } else { vec![] } } }); rsx! { div { class: "page species-detail", match &*species.read() { None => rsx! { p { "{t(&lang.read(), \"loading\")}" } }, Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } }, Some(Ok(s)) => { let species_slug = s.slug.clone(); let current_lang = lang.read().clone(); let l = &*current_lang; // Helper closures to format fields let os = |v: &Option| -> String { match v { Some(x) if !x.is_empty() => x.clone(), _ => "\u{2014}".to_string(), } }; let ob = |v: Option| -> String { match v { Some(true) => t(l, "yes").to_string(), Some(false) => t(l, "no").to_string(), None => "\u{2014}".to_string(), } }; let ovs = |v: &Option>| -> String { match v { Some(x) if !x.is_empty() => x.join(", "), _ => "\u{2014}".to_string(), } }; let em = "\u{2014}"; let common_name = pick_name(¤t_lang, &s.name_de, &s.name_en, &s.name_scientific); let name_en = os(&s.name_en); let name_de = os(&s.name_de); let desc = pick_desc(¤t_lang, &s.description_de, &s.description_en, &s.description); // Uses let food = pick_desc(¤t_lang, &s.food_uses_de, &s.food_uses_en, &s.food_uses); let med = pick_desc(¤t_lang, &s.medicinal_uses_de, &s.medicinal_uses_en, &s.medicinal_uses); let other = pick_desc(¤t_lang, &s.other_uses_de, &s.other_uses_en, &s.other_uses); let edibility = match s.edibility_rating { Some(r) => format!("{r}/5"), None => em.to_string(), }; // Ecology let layer = s.plant_layer.as_deref().map(|v| t_val(l, v).to_string()).unwrap_or_else(|| em.to_string()); let succession = s.succession_stage.as_deref().map(|v| t_val(l, v).to_string()).unwrap_or_else(|| em.to_string()); let wildlife = os(&s.wildlife_value); let native = os(&s.native_range); let pollination = s.pollination_type.as_deref().map(|v| t_val(l, v).to_string()).unwrap_or_else(|| em.to_string()); // Growing requirements let soil_moist = os(&s.soil_moisture); let ph_range = match (s.ph_min, s.ph_max) { (Some(mn), Some(mx)) => format!("{mn} \u{2013} {mx}"), (Some(mn), None) => format!("{mn}+"), (None, Some(mx)) => format!("< {mx}"), _ => em.to_string(), }; let drought = s.drought_tolerance.as_deref().map(|v| t_val(l, v).to_string()).unwrap_or_else(|| em.to_string()); let salt = os(&s.salt_tolerance); let usda = os(&s.hardiness_zone_usda); let at_zone = os(&s.hardiness_zone_at); // Permaculture let n_fixer = ob(s.nitrogen_fixer); let dyn_acc = ob(s.dynamic_accumulator); let pollinators = ob(s.attracts_pollinators); let beneficial = ob(s.attracts_beneficial_insects); let mulch = ob(s.mulch_plant); let gc_quality = os(&s.ground_cover_quality); let allelo = ob(s.allelopathic); let guild = ovs(&s.guild_role); // External links let qid = s.wikidata_qid.clone(); let gbif = s.gbif_id.clone(); let eppo = os(&s.eppo_code); let pfaf = s.pfaf_url.clone(); // Primary image: prefer primary from images API, else primary_image_key let primary_img: Option = { let imgs = species_images.read(); match &*imgs { Some(v) if !v.is_empty() => { v.iter().find(|i| i.is_primary).or(v.first()).cloned() } _ => None, } }; let img_key: Option = primary_img.as_ref().map(|i| i.s3_key.clone()) .or_else(|| s.primary_image_key.clone()); rsx! { h1 { em { "{s.name_scientific}" } } if common_name != s.name_scientific { p { class: "name-common", "{common_name}" } } div { class: "detail-row", // === LEFT COLUMN === div { class: "detail-col", // Card 1: Species Details div { class: "detail-card", div { class: "detail-card-header", "{t(l, \"card.species_details\")}" } table { class: "attr-table", tbody { tr { th { "{t(l, \"field.scientific_name\")}" } td { em { "{s.name_scientific}" } } } tr { th { "{t(l, \"field.common_name\")}" } td { "{common_name}" } } tr { th { "{t(l, \"field.name_en\")}" } td { class: if name_en == em { "placeholder" } else { "" }, "{name_en}" } } tr { th { "{t(l, \"field.name_de\")}" } td { class: if name_de == em { "placeholder" } else { "" }, "{name_de}" } } tr { th { "{t(l, \"field.family\")}" } td { if let Some(Some(ref fam)) = *family_data.read() { Link { to: Route::FamilyDetail { slug: fam.slug.clone() }, em { "{fam.name_scientific}" } } } else { span { class: "placeholder", "\u{2014}" } } } } tr { th { "{t(l, \"field.description\")}" } td { class: if desc == em { "placeholder" } else { "" }, "{desc}" } } } } } // Card 2: Uses div { class: "detail-card", div { class: "detail-card-header", "{t(l, \"card.uses\")}" } table { class: "attr-table", tbody { tr { th { "{t(l, \"field.food_uses\")}" } td { class: if food == em { "placeholder" } else { "" }, "{food}" } } tr { th { "{t(l, \"field.medicinal_uses\")}" } td { class: if med == em { "placeholder" } else { "" }, "{med}" } } tr { th { "{t(l, \"field.other_uses\")}" } td { class: if other == em { "placeholder" } else { "" }, "{other}" } } tr { th { "{t(l, \"field.edibility\")}" } td { class: if edibility == em { "placeholder" } else { "" }, "{edibility}" } } } } } // Card 3: Ecology div { class: "detail-card", div { class: "detail-card-header", "{t(l, \"card.ecology\")}" } table { class: "attr-table", tbody { tr { th { "{t(l, \"field.plant_layer\")}" } td { class: if layer == em { "placeholder" } else { "" }, "{layer}" } } tr { th { "{t(l, \"field.succession\")}" } td { class: if succession == em { "placeholder" } else { "" }, "{succession}" } } tr { th { "{t(l, \"field.wildlife_value\")}" } td { class: if wildlife == em { "placeholder" } else { "" }, "{wildlife}" } } tr { th { "{t(l, \"field.native_range\")}" } td { class: if native == em { "placeholder" } else { "" }, "{native}" } } tr { th { "{t(l, \"field.pollination\")}" } td { class: if pollination == em { "placeholder" } else { "" }, "{pollination}" } } } } } } // === RIGHT COLUMN === div { class: "detail-col", // Image card if let Some(ref key) = img_key { div { class: "detail-card", div { class: "detail-card-header", "{t(l, \"card.image\")}" } div { class: "image-card-body", img { class: "species-image", src: "/img/{key}", alt: "{s.name_scientific}" } if let Some(ref img) = primary_img { div { class: "image-attribution", if let Some(ref caption) = img.caption { span { "{caption}" } } if let Some(ref lic) = img.license { if img.caption.is_some() { " | " } span { "{lic}" } } if let Some(ref url) = img.source_url { if img.caption.is_some() || img.license.is_some() { " | " } a { href: "{url}", target: "_blank", class: "attribution-link", "{t(l, \"field.source\")}" } } } } } } } // Card 4: Growing Requirements div { class: "detail-card", div { class: "detail-card-header", "{t(l, \"card.growing_requirements\")}" } table { class: "attr-table", tbody { tr { th { "{t(l, \"field.soil_moisture\")}" } td { class: if soil_moist == em { "placeholder" } else { "" }, "{soil_moist}" } } tr { th { "{t(l, \"field.ph_range\")}" } td { class: if ph_range == em { "placeholder" } else { "" }, "{ph_range}" } } tr { th { "{t(l, \"field.drought_tolerance\")}" } td { class: if drought == em { "placeholder" } else { "" }, "{drought}" } } tr { th { "{t(l, \"field.salt_tolerance\")}" } td { class: if salt == em { "placeholder" } else { "" }, "{salt}" } } tr { th { "{t(l, \"field.usda_zone\")}" } td { class: if usda == em { "placeholder" } else { "" }, "{usda}" } } tr { th { "{t(l, \"field.at_zone\")}" } td { class: if at_zone == em { "placeholder" } else { "" }, "{at_zone}" } } } } } // Card 5: Permaculture div { class: "detail-card", div { class: "detail-card-header", "{t(l, \"card.permaculture\")}" } table { class: "attr-table", tbody { tr { th { "{t(l, \"field.nitrogen_fixer\")}" } td { class: if n_fixer == em { "placeholder" } else { "" }, "{n_fixer}" } } tr { th { "{t(l, \"field.dynamic_acc\")}" } td { class: if dyn_acc == em { "placeholder" } else { "" }, "{dyn_acc}" } } tr { th { "{t(l, \"field.attracts_pollinators\")}" } td { class: if pollinators == em { "placeholder" } else { "" }, "{pollinators}" } } tr { th { "{t(l, \"field.attracts_beneficial\")}" } td { class: if beneficial == em { "placeholder" } else { "" }, "{beneficial}" } } tr { th { "{t(l, \"field.mulch_plant\")}" } td { class: if mulch == em { "placeholder" } else { "" }, "{mulch}" } } tr { th { "{t(l, \"field.ground_cover_quality\")}" } td { class: if gc_quality == em { "placeholder" } else { "" }, "{gc_quality}" } } tr { th { "{t(l, \"field.allelopathic\")}" } td { class: if allelo == em { "placeholder" } else { "" }, "{allelo}" } } tr { th { "{t(l, \"field.guild_role\")}" } td { class: if guild == em { "placeholder" } else { "" }, "{guild}" } } } } } // Card 6: Wildlife & Ecology div { class: "detail-card", div { class: "detail-card-header", "{t(l, \"card.wildlife\")}" } table { class: "attr-table", tbody { tr { th { "{t(l, \"field.nectar\")}" } td { match s.nectar_value { Some(v) => rsx! { div { class: "wildlife-bar-wrap", div { class: "wildlife-bar wildlife-bar-{v}", style: "width: {v as f64 * 25.0}%", } } span { class: "wildlife-bar-label", "{v}/4" } }, None => rsx! { span { class: "placeholder", "\u{2014}" } }, } } } tr { th { "{t(l, \"field.pollen\")}" } td { match s.pollen_value { Some(v) => rsx! { div { class: "wildlife-bar-wrap", div { class: "wildlife-bar wildlife-bar-{v}", style: "width: {v as f64 * 25.0}%", } } span { class: "wildlife-bar-label", "{v}/4" } }, None => rsx! { span { class: "placeholder", "\u{2014}" } }, } } } tr { th { "{t(l, \"field.wild_bees\")}" } td { match (s.wild_bee_count, s.wild_bee_specialist_count) { (Some(total), Some(spec)) => rsx! { "{total} {t(l, \"species_species\")} ({spec} {t(l, \"specialists\")})" }, (Some(total), None) => rsx! { "{total} {t(l, \"species_species\")}" }, _ => rsx! { span { class: "placeholder", "\u{2014}" } }, } } } tr { th { "{t(l, \"field.butterflies\")}" } td { match (s.butterfly_moth_count, s.caterpillar_host_count) { (Some(bm), Some(ch)) => rsx! { "{bm} {t(l, \"species_species\")} ({ch} {t(l, \"caterpillar_hosts\")})" }, (Some(bm), None) => rsx! { "{bm} {t(l, \"species_species\")}" }, _ => rsx! { span { class: "placeholder", "\u{2014}" } }, } } } if s.caterpillar_specialist_count.is_some() { tr { th { "{t(l, \"field.caterpillar_specialists\")}" } td { "{s.caterpillar_specialist_count.unwrap()}" } } } tr { th { "{t(l, \"field.hoverflies\")}" } td { match s.hoverfly_count { Some(v) => rsx! { "{v} {t(l, \"species_species\")}" }, None => rsx! { span { class: "placeholder", "\u{2014}" } }, } } } tr { th { "{t(l, \"field.beetles\")}" } td { match s.beetle_count { Some(v) => rsx! { "{v} {t(l, \"species_species\")}" }, None => rsx! { span { class: "placeholder", "\u{2014}" } }, } } } tr { th { "{t(l, \"field.birds\")}" } td { match s.bird_count { Some(v) => rsx! { "{v} {t(l, \"species_species\")}" }, None => rsx! { span { class: "placeholder", "\u{2014}" } }, } } } tr { th { "{t(l, \"field.mammals\")}" } td { match s.mammal_count { Some(v) => rsx! { "{v} {t(l, \"species_species\")}" }, None => rsx! { span { class: "placeholder", "\u{2014}" } }, } } } tr { th { "{t(l, \"field.native_status\")}" } td { match &s.native_status { Some(ns) if !ns.is_empty() => { let badge_class = match ns.as_str() { s if s.contains("Wildform") || s.contains("wildform") || s.contains("heimisch") => "native-badge native-badge-heimisch", s if s.contains("rchäophyt") => "native-badge native-badge-archaeophyt", s if s.contains("eophyt") => "native-badge native-badge-neophyt", _ => "native-badge", }; let translated = t_val(l, ns); rsx! { span { class: "{badge_class}", "{translated}" } } }, _ => rsx! { span { class: "placeholder", "\u{2014}" } }, } } } tr { th { "{t(l, \"field.naturadb_tags\")}" } td { match &s.naturadb_tags { Some(tags) if !tags.is_empty() => { let tag_list: Vec<&str> = tags.split(',').map(|t| t.trim()).filter(|t| !t.is_empty()).collect(); rsx! { div { class: "badges", for tag in tag_list { span { class: "badge", "{tag}" } } } } }, _ => rsx! { span { class: "placeholder", "\u{2014}" } }, } } } } } } // Card 7: External Links div { class: "detail-card", div { class: "detail-card-header", "{t(l, \"card.external_links\")}" } table { class: "attr-table", tbody { tr { th { "{t(l, \"field.wikidata_qid\")}" } td { if let Some(ref q) = qid { if !q.is_empty() { a { href: "https://www.wikidata.org/wiki/{q}", target: "_blank", class: "external-link", "{q}" } } else { span { class: "placeholder", "\u{2014}" } } } else { span { class: "placeholder", "\u{2014}" } } } } tr { th { "{t(l, \"field.gbif_id\")}" } td { if let Some(ref g) = gbif { if !g.is_empty() { a { href: "https://www.gbif.org/species/{g}", target: "_blank", class: "external-link", "{g}" } } else { span { class: "placeholder", "\u{2014}" } } } else { span { class: "placeholder", "\u{2014}" } } } } tr { th { "{t(l, \"field.eppo_code\")}" } td { class: if eppo == em { "placeholder" } else { "" }, "{eppo}" } } tr { th { "{t(l, \"field.pfaf_url\")}" } td { if let Some(ref u) = pfaf { if !u.is_empty() { a { href: "{u}", target: "_blank", class: "external-link", "{t(l, \"field.view_on_pfaf\")}" } } else { span { class: "placeholder", "\u{2014}" } } } else { span { class: "placeholder", "\u{2014}" } } } } } } } } } // Cultivars for this species (below the two-column layout) h2 { "{t(l, \"card.cultivars\")}" } CultivarListForSpecies { species_slug: species_slug } } }, } } } } #[component] fn CultivarListForSpecies(species_slug: String) -> Element { let lang = use_context::().0; let slug = species_slug.clone(); let cultivars = use_resource(move || { let s = slug.clone(); async move { api::list_cultivars(1, 100, Some(&s), None).await } }); let l = lang.read().clone(); rsx! { match &*cultivars.read() { None => rsx! { p { "{t(&l, \"loading\")}" } }, Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } }, Some(Ok(data)) => { if data.data.is_empty() { rsx! { p { class: "empty", "{t(&l, \"no_cultivars\")}" } } } else { rsx! { div { class: "card-grid", for c in data.data.iter() { { let cv_common = pick_name(&lang.read(), &c.name_de, &c.name_en, &c.name); let show_common = if cv_common == c.name { None } else { Some(cv_common) }; rsx! { div { class: "plant-card", Link { to: Route::CultivarDetail { slug: c.slug.clone() }, strong { "{c.name}" } } if let Some(ref common) = show_common { p { class: "card-common", "{common}" } } if c.is_organic { span { class: "badge organic", "{t(&l, \"field.organic\")}" } } } } } } } } } }, } } }