Files
herbapi/herbapi-ui/src/pages/cultivars.rs
T

924 lines
54 KiB
Rust

use dioxus::prelude::*;
use std::collections::HashMap;
use uuid::Uuid;
use crate::api;
use crate::app::{Lang, Route};
use crate::components::planting_calendar::PlantingCalendar;
use crate::components::table_controls::*;
use crate::i18n::{pick_desc, pick_name, t, t_val};
/// Convert a month-number array (1=Jan..12=Dec) to a comma-separated string of abbreviations.
fn months_display(months: &Option<Vec<i32>>) -> String {
const NAMES: [&str; 12] = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
match months {
Some(v) if !v.is_empty() => v
.iter()
.filter_map(|&m| NAMES.get((m - 1) as usize).copied())
.collect::<Vec<_>>()
.join(", "),
_ => "\u{2014}".to_string(), // em dash
}
}
/// Format an Option<String> for display: value or em dash placeholder.
fn opt_str(val: &Option<String>) -> String {
match val {
Some(s) if !s.is_empty() => s.clone(),
_ => "\u{2014}".to_string(),
}
}
/// Format an Option<f64> with a suffix.
fn opt_f64_suffix(val: Option<f64>, suffix: &str) -> String {
match val {
Some(v) => format!("{v}{suffix}"),
None => "\u{2014}".to_string(),
}
}
/// Format an Option<i32> with a suffix.
fn opt_i32_suffix(val: Option<i32>, suffix: &str) -> String {
match val {
Some(v) => format!("{v}{suffix}"),
None => "\u{2014}".to_string(),
}
}
/// Format an Option<bool> as Yes / No / em dash (translated).
fn opt_bool_t(val: Option<bool>, lang: &str) -> String {
match val {
Some(true) => t(lang, "yes").to_string(),
Some(false) => t(lang, "no").to_string(),
None => "\u{2014}".to_string(),
}
}
/// Bool field: always present (non-Option), translated.
fn bool_display_t(val: bool, lang: &str) -> String {
if val { t(lang, "yes").to_string() } else { t(lang, "no").to_string() }
}
/// Format an Option<Vec<String>> as comma-separated or em dash.
fn opt_vec_str(val: &Option<Vec<String>>) -> String {
match val {
Some(v) if !v.is_empty() => v.join(", "),
_ => "\u{2014}".to_string(),
}
}
/// Infer a light requirement string from a species plant_layer value.
fn infer_light_from_layer(layer: &Option<String>) -> Option<String> {
match layer.as_deref() {
Some(l) => {
let lower = l.to_lowercase();
if lower.contains("canopy") || lower.contains("tall tree") {
Some("Full sun".to_string())
} else if lower.contains("understory") || lower.contains("small tree") || lower.contains("shrub") {
Some("Partial shade".to_string())
} else if lower.contains("ground") || lower.contains("creep") {
Some("Shade tolerant".to_string())
} else if lower.contains("herb") || lower.contains("climber") || lower.contains("vine") {
Some("Full sun to partial shade".to_string())
} else {
None
}
}
None => None,
}
}
const STORAGE_KEY_COLS: &str = "herbapi_cultivars_cols";
const STORAGE_KEY_PP: &str = "herbapi_cultivars_pp";
fn cultivar_columns() -> Vec<ColumnDef> {
vec![
ColumnDef { key: "name", label: "Name", default_visible: true },
ColumnDef { key: "species", label: "Species", default_visible: true },
ColumnDef { key: "organic", label: "Organic", default_visible: true },
ColumnDef { key: "perennial", label: "Perennial", default_visible: true },
ColumnDef { key: "description", label: "Description", default_visible: false },
ColumnDef { key: "frost_tolerance", label: "Frost Tol.", default_visible: false },
ColumnDef { key: "growing_time", label: "Growing Time", default_visible: false },
ColumnDef { key: "days_germ", label: "Days to Germ.", default_visible: false },
]
}
#[component]
pub fn CultivarList() -> Element {
let lang = use_context::<Lang>().0;
let columns = cultivar_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, &cultivar_columns()));
// Fetch species list once to map species_id -> name
let species_map = use_resource(move || async move {
let mut map = HashMap::<Uuid, String>::new();
if let Ok(resp) = api::list_species(1, 100, None, None).await {
for s in resp.data {
map.insert(s.id, s.name_scientific);
}
// Fetch remaining pages if needed
let total_pages = (resp.total + resp.per_page - 1) / resp.per_page;
for p in 2..=total_pages {
if let Ok(r) = api::list_species(p, 100, None, None).await {
for s in r.data {
map.insert(s.id, s.name_scientific);
}
}
}
}
map
});
let cultivars = use_resource(move || {
let p = *page.read();
let pp = *per_page.read();
let s = search.read().clone();
async move {
let q = if s.is_empty() { None } else { Some(s.as_str()) };
api::list_cultivars(p, pp, None, q).await
}
});
let current_page = *page.read();
let l = lang.read().clone();
rsx! {
div { class: "page",
h1 { "{t(&l, \"page.cultivars\")}" }
div { class: "table-toolbar",
div { class: "search-bar",
input {
r#type: "text",
placeholder: "{t(&l, \"search.placeholder_cultivars\")}",
value: "{search}",
oninput: move |e| {
search.set(e.value());
page.set(1);
},
}
}
PerPageSelector {
per_page: per_page,
page: page,
storage_key: STORAGE_KEY_PP.to_string(),
}
}
ColumnToggle {
columns: columns.clone(),
visible: visible_cols,
storage_key: STORAGE_KEY_COLS.to_string(),
}
match &*cultivars.read() {
None => rsx! { p { "{t(&l, \"loading\")}" } },
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
Some(Ok(data)) => {
let smap = species_map.read();
let empty_map: HashMap<Uuid, String> = HashMap::new();
let sm = match &*smap {
Some(m) => m,
_ => &empty_map,
};
let vis = visible_cols.read();
rsx! {
p { class: "result-count", "{data.total} {t(&l, \"nav.cultivars\").to_lowercase()}" }
div { class: "table-wrap",
table {
thead {
tr {
if is_col_visible(&vis, "name") {
th { "{t(&l, \"field.name\")}" }
}
if is_col_visible(&vis, "species") {
th { "{t(&l, \"field.species\")}" }
}
if is_col_visible(&vis, "organic") {
th { "{t(&l, \"field.organic\")}" }
}
if is_col_visible(&vis, "perennial") {
th { "{t(&l, \"field.perennial\")}" }
}
if is_col_visible(&vis, "description") {
th { "{t(&l, \"field.description\")}" }
}
if is_col_visible(&vis, "frost_tolerance") {
th { "{t(&l, \"field.frost_tol\")}" }
}
if is_col_visible(&vis, "growing_time") {
th { "{t(&l, \"field.growing_time\")}" }
}
if is_col_visible(&vis, "days_germ") {
th { "{t(&l, \"field.days_germ\")}" }
}
}
}
tbody {
for c in data.data.iter() {
{
let species_name: &str = sm.get(&c.species_id).map(String::as_str).unwrap_or("-");
rsx! {
tr {
if is_col_visible(&vis, "name") {
td {
Link { to: Route::CultivarDetail { slug: c.slug.clone() },
strong { "{c.name}" }
}
}
}
if is_col_visible(&vis, "species") {
td { class: "species-name", em { "{species_name}" } }
}
if is_col_visible(&vis, "organic") {
td {
if c.is_organic {
span { class: "badge organic", "{t(&l, \"yes\")}" }
} else {
"-"
}
}
}
if is_col_visible(&vis, "perennial") {
td { if c.perennial { "{t(&l, \"yes\")}" } else { "{t(&l, \"annual\")}" } }
}
if is_col_visible(&vis, "description") {
td { class: "cell-truncated",
"{c.description.as_deref().map(|d| truncate(d, 60)).unwrap_or_else(|| \"-\".to_string())}"
}
}
if is_col_visible(&vis, "frost_tolerance") {
td { "{c.frost_tolerance.as_deref().unwrap_or(\"-\")}" }
}
if is_col_visible(&vis, "growing_time") {
td {
match c.growing_time_days {
Some(d) => rsx! { "{d} days" },
None => rsx! { "-" },
}
}
}
if is_col_visible(&vis, "days_germ") {
td {
match c.days_to_germination {
Some(d) => rsx! { "{d}" },
None => rsx! { "-" },
}
}
}
}
}
}
}
}
}
}
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 CultivarDetail(slug: String) -> Element {
let lang = use_context::<Lang>().0;
let slug_clone = slug.clone();
let cultivar = use_resource(move || {
let s = slug_clone.clone();
async move { api::get_cultivar(&s).await }
});
// Fetch species info once we have the cultivar
let species_data = use_resource(move || {
let cv = cultivar.read().clone();
async move {
if let Some(Ok(c)) = cv {
let resp = api::list_species(1, 200, None, None).await.ok();
if let Some(r) = resp {
for s in &r.data {
if s.id == c.species_id {
return Some(s.clone());
}
}
}
}
None
}
});
// Fetch supplier links once we have the cultivar
let suppliers_data = use_resource(move || {
let cv = cultivar.read().clone();
async move {
if let Some(Ok(c)) = cv {
api::get_cultivar_suppliers(c.id).await.ok()
} else {
None
}
}
});
// Fetch all suppliers for name lookup
let all_suppliers = use_resource(move || async move {
api::list_suppliers().await.ok().unwrap_or_default()
});
// Fetch species images (to show species image on cultivar page)
let species_images = use_resource(move || {
let cv = cultivar.read().clone();
async move {
if let Some(Ok(c)) = cv {
api::get_images("species", c.species_id).await.ok().unwrap_or_default()
} else {
vec![]
}
}
});
rsx! {
div { class: "page cultivar-detail",
match &*cultivar.read() {
None => rsx! { p { "{t(&lang.read(), \"loading\")}" } },
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
Some(Ok(c)) => {
let current_lang = lang.read().clone();
let l = &*current_lang;
// Pre-compute display strings outside of rsx
let common_name = pick_name(&current_lang, &c.name_de, &c.name_en, &c.name);
let name_en = opt_str(&c.name_en);
let name_de = opt_str(&c.name_de);
let name_sci = opt_str(&c.name_scientific);
let desc = pick_desc(&current_lang, &c.description_de, &c.description_en, &c.description);
let organic = bool_display_t(c.is_organic, l);
let perennial = bool_display_t(c.perennial, l);
// Planting schedule
let indoor = months_display(&c.indoor_sowing_months);
let direct = months_display(&c.direct_sowing_months);
let transplant = months_display(&c.transplanting_months);
let glasshouse = months_display(&c.glasshouse_months);
let harvest = months_display(&c.harvesting_months);
let succession = opt_i32_suffix(c.succession_planting_days, " days");
let planting_notes = opt_str(&c.planting_notes);
// Growing information
let growing_time = opt_i32_suffix(c.growing_time_days, " days");
let planting_depth = opt_f64_suffix(c.planting_depth_cm, " cm");
let row_spacing = opt_f64_suffix(c.row_spacing_cm, " cm");
let plant_spacing = opt_f64_suffix(c.plant_spacing_cm, " cm");
let propagation = opt_vec_str(&c.propagation_methods);
// Germination
let days_germ = opt_i32_suffix(c.days_to_germination, "");
let germ_temp = opt_f64_suffix(c.germination_temp_c, " \u{00b0}C");
let light_req = opt_str(&c.light_requirement);
let strat_req = opt_bool_t(c.stratification_required, l);
let strat_days = opt_i32_suffix(c.stratification_days, "");
let scar_req = opt_bool_t(c.scarification_required, l);
// Climate
let min_temp = opt_f64_suffix(c.min_temp, " \u{00b0}C");
let max_temp = opt_f64_suffix(c.max_temp, " \u{00b0}C");
let humidity = opt_str(&c.humidity);
let light = opt_str(&c.light);
let frost_tol = opt_str(&c.frost_tolerance);
let min_light_h = opt_f64_suffix(c.min_light_hours_day, " h");
let opt_light_h = opt_f64_suffix(c.optimal_light_hours_day, " h");
// --- Species fallback values ---
let (sp_frost_fb, sp_light_fb, sp_drought_fb, sp_usda_fb) = {
let sp_read = species_data.read();
match &*sp_read {
Some(Some(sp)) => {
let frost = sp.hardiness_zone_usda.as_ref()
.filter(|s| !s.is_empty())
.map(|z| format!("USDA Zone {z}"));
let light = infer_light_from_layer(&sp.plant_layer);
let drought = sp.drought_tolerance.as_deref()
.filter(|s| !s.is_empty())
.map(|v| t_val(l, v).to_string());
let usda = sp.hardiness_zone_usda.clone()
.filter(|s| !s.is_empty());
(frost, light, drought, usda)
}
_ => (None, None, None, None),
}
};
let has_any_fallback =
(frost_tol == "\u{2014}" && sp_frost_fb.is_some())
|| (light_req == "\u{2014}" && sp_light_fb.is_some())
|| (light == "\u{2014}" && sp_light_fb.is_some())
|| sp_drought_fb.is_some()
|| sp_usda_fb.is_some();
// Species image
let sp_primary_img: Option<crate::types::Image> = {
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 sp_img_key: Option<String> = sp_primary_img.as_ref().map(|i| i.s3_key.clone())
.or_else(|| {
let sp_read = species_data.read();
match &*sp_read {
Some(Some(sp)) => sp.primary_image_key.clone(),
_ => None,
}
});
rsx! {
h1 { "{c.name}" }
if common_name != c.name {
p { class: "name-common", "{common_name}" }
}
// Week-based planting calendar (full width)
div { class: "detail-card", style: "margin-top: 1.25rem;",
div { class: "detail-card-header", "{t(l, \"card.planting_calendar\")}" }
div { style: "padding: 0.75rem;",
PlantingCalendar {
indoor_sowing_months: c.indoor_sowing_months.clone(),
direct_sowing_months: c.direct_sowing_months.clone(),
transplanting_months: c.transplanting_months.clone(),
glasshouse_months: c.glasshouse_months.clone(),
harvesting_months: c.harvesting_months.clone(),
indoor_sowing_weeks: c.indoor_sowing_weeks.clone(),
direct_sowing_weeks: c.direct_sowing_weeks.clone(),
transplanting_weeks: c.transplanting_weeks.clone(),
glasshouse_weeks: c.glasshouse_weeks.clone(),
harvesting_weeks: c.harvesting_weeks.clone(),
}
}
}
div { class: "detail-row",
// === LEFT COLUMN ===
div { class: "detail-col",
// Card 1: Cultivar Details
div { class: "detail-card",
div { class: "detail-card-header", "{t(l, \"card.cultivar_details\")}" }
table { class: "attr-table",
tbody {
tr {
th { "{t(l, \"field.name\")}" }
td { "{c.name}" }
}
tr {
th { "{t(l, \"field.common_name\")}" }
td { "{common_name}" }
}
tr {
th { "{t(l, \"field.name_en\")}" }
td { class: if name_en == "\u{2014}" { "placeholder" } else { "" }, "{name_en}" }
}
tr {
th { "{t(l, \"field.name_de\")}" }
td { class: if name_de == "\u{2014}" { "placeholder" } else { "" }, "{name_de}" }
}
tr {
th { "{t(l, \"field.scientific_name\")}" }
td { class: if name_sci == "\u{2014}" { "placeholder" } else { "" },
if name_sci != "\u{2014}" {
em { "{name_sci}" }
} else {
span { "\u{2014}" }
}
}
}
tr {
th { "{t(l, \"field.species\")}" }
td {
if let Some(Some(ref sp)) = *species_data.read() {
Link { to: Route::SpeciesDetail { slug: sp.slug.clone() },
em { "{sp.name_scientific}" }
}
if let Some(ref de) = sp.name_de {
if !de.is_empty() {
" ({de})"
}
}
} else {
span { class: "placeholder", "\u{2014}" }
}
}
}
tr {
th { "{t(l, \"field.description\")}" }
td { class: if desc == "\u{2014}" { "placeholder" } else { "" }, "{desc}" }
}
tr {
th { "{t(l, \"field.organic\")}" }
td { "{organic}" }
}
tr {
th { "{t(l, \"field.perennial\")}" }
td { "{perennial}" }
}
}
}
}
// Card 2: Planting Schedule
div { class: "detail-card",
div { class: "detail-card-header", "{t(l, \"card.planting_schedule\")}" }
table { class: "attr-table",
tbody {
tr {
th { "{t(l, \"cal.indoor_sowing\")}" }
td { class: if indoor == "\u{2014}" { "placeholder" } else { "" }, "{indoor}" }
}
tr {
th { "{t(l, \"cal.direct_sowing\")}" }
td { class: if direct == "\u{2014}" { "placeholder" } else { "" }, "{direct}" }
}
tr {
th { "{t(l, \"cal.transplanting\")}" }
td { class: if transplant == "\u{2014}" { "placeholder" } else { "" }, "{transplant}" }
}
tr {
th { "{t(l, \"cal.glasshouse\")}" }
td { class: if glasshouse == "\u{2014}" { "placeholder" } else { "" }, "{glasshouse}" }
}
tr {
th { "{t(l, \"cal.harvesting\")}" }
td { class: if harvest == "\u{2014}" { "placeholder" } else { "" }, "{harvest}" }
}
tr {
th { "{t(l, \"field.succession_planting\")}" }
td { class: if succession == "\u{2014}" { "placeholder" } else { "" }, "{succession}" }
}
tr {
th { "{t(l, \"field.planting_notes\")}" }
td { class: if planting_notes == "\u{2014}" { "placeholder" } else { "" }, "{planting_notes}" }
}
}
}
}
// Card 3: Where to Buy
div { class: "detail-card",
div { class: "detail-card-header", "{t(l, \"card.where_to_buy\")}" }
{
let sup_links = suppliers_data.read();
let sups = all_suppliers.read();
let sup_list: Vec<crate::types::Supplier> = match &*sups {
Some(v) => v.clone(),
None => vec![],
};
match &*sup_links {
Some(Some(links)) if !links.is_empty() => {
rsx! {
table { class: "attr-table",
thead {
tr {
th { "{t(l, \"field.supplier\")}" }
th { "{t(l, \"field.sku\")}" }
th { "{t(l, \"field.price\")}" }
th { "{t(l, \"field.link\")}" }
}
}
tbody {
for link in links.iter() {
{
let sup_name = sup_list.iter()
.find(|s| s.id == link.supplier_id)
.map(|s| s.name.clone())
.unwrap_or_else(|| "\u{2014}".to_string());
let sku = link.article_number.as_deref().unwrap_or("\u{2014}").to_string();
let price = match link.price_eur {
Some(p) if p > 0.0 => format!("EUR {p:.2}"),
_ => "\u{2014}".to_string(),
};
let url = link.product_url.clone();
rsx! {
tr {
td { "{sup_name}" }
td { class: if sku == "\u{2014}" { "placeholder" } else { "" }, "{sku}" }
td { class: if price == "\u{2014}" { "placeholder" } else { "" }, "{price}" }
td {
if let Some(ref u) = url {
if !u.is_empty() {
a { href: "{u}", target: "_blank", class: "external-link", "{t(l, \"btn.view\")}" }
} else {
span { class: "placeholder", "\u{2014}" }
}
} else {
span { class: "placeholder", "\u{2014}" }
}
}
}
}
}
}
}
}
}
},
_ => rsx! {
p { class: "placeholder detail-card-empty", "\u{2014} {t(l, \"no_suppliers_linked\")}" }
},
}
}
}
}
// === RIGHT COLUMN ===
div { class: "detail-col",
// Image card
if let Some(ref key) = sp_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: "{c.name}" }
if let Some(ref img) = sp_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 Information
div { class: "detail-card",
div { class: "detail-card-header", "{t(l, \"card.growing_info\")}" }
table { class: "attr-table",
tbody {
tr {
th { "{t(l, \"field.growing_time\")}" }
td { class: if growing_time == "\u{2014}" { "placeholder" } else { "" }, "{growing_time}" }
}
tr {
th { "{t(l, \"field.planting_depth\")}" }
td { class: if planting_depth == "\u{2014}" { "placeholder" } else { "" }, "{planting_depth}" }
}
tr {
th { "{t(l, \"field.row_spacing\")}" }
td { class: if row_spacing == "\u{2014}" { "placeholder" } else { "" }, "{row_spacing}" }
}
tr {
th { "{t(l, \"field.plant_spacing\")}" }
td { class: if plant_spacing == "\u{2014}" { "placeholder" } else { "" }, "{plant_spacing}" }
}
tr {
th { "{t(l, \"field.propagation\")}" }
td { class: if propagation == "\u{2014}" { "placeholder" } else { "" }, "{propagation}" }
}
}
}
}
// Card 5: Germination
div { class: "detail-card",
div { class: "detail-card-header", "{t(l, \"card.germination\")}" }
table { class: "attr-table",
tbody {
tr {
th { "{t(l, \"field.days_to_germ\")}" }
td { class: if days_germ == "\u{2014}" { "placeholder" } else { "" }, "{days_germ}" }
}
tr {
th { "{t(l, \"field.germ_temp\")}" }
td { class: if germ_temp == "\u{2014}" { "placeholder" } else { "" }, "{germ_temp}" }
}
tr {
th { "{t(l, \"field.light_req\")}" }
if light_req != "\u{2014}" {
td { "{light_req}" }
} else if let Some(ref fb) = sp_light_fb {
td {
span { class: "estimated-value", "{fb}" }
span { class: "estimated-tag", "~ species" }
}
} else {
td { class: "placeholder", "\u{2014}" }
}
}
tr {
th { "{t(l, \"field.stratification\")}" }
td { class: if strat_req == "\u{2014}" { "placeholder" } else { "" }, "{strat_req}" }
}
tr {
th { "{t(l, \"field.stratification_days\")}" }
td { class: if strat_days == "\u{2014}" { "placeholder" } else { "" }, "{strat_days}" }
}
tr {
th { "{t(l, \"field.scarification\")}" }
td { class: if scar_req == "\u{2014}" { "placeholder" } else { "" }, "{scar_req}" }
}
}
}
}
// Card 6: Climate & Environment
div { class: "detail-card",
div { class: "detail-card-header", "{t(l, \"card.climate\")}" }
table { class: "attr-table",
tbody {
tr {
th { "{t(l, \"field.min_temp\")}" }
td { class: if min_temp == "\u{2014}" { "placeholder" } else { "" }, "{min_temp}" }
}
tr {
th { "{t(l, \"field.max_temp\")}" }
td { class: if max_temp == "\u{2014}" { "placeholder" } else { "" }, "{max_temp}" }
}
tr {
th { "{t(l, \"field.humidity\")}" }
td { class: if humidity == "\u{2014}" { "placeholder" } else { "" }, "{humidity}" }
}
tr {
th { "{t(l, \"field.light\")}" }
if light != "\u{2014}" {
td { "{light}" }
} else if let Some(ref fb) = sp_light_fb {
td {
span { class: "estimated-value", "{fb}" }
span { class: "estimated-tag", "~ species" }
}
} else {
td { class: "placeholder", "\u{2014}" }
}
}
tr {
th { "{t(l, \"field.frost_tolerance\")}" }
if frost_tol != "\u{2014}" {
td { "{frost_tol}" }
} else if let Some(ref fb) = sp_frost_fb {
td {
span { class: "estimated-value", "{fb}" }
span { class: "estimated-tag", "~ species" }
}
} else {
td { class: "placeholder", "\u{2014}" }
}
}
tr {
th { "{t(l, \"field.min_light_hours\")}" }
td { class: if min_light_h == "\u{2014}" { "placeholder" } else { "" }, "{min_light_h}" }
}
tr {
th { "{t(l, \"field.opt_light_hours\")}" }
td { class: if opt_light_h == "\u{2014}" { "placeholder" } else { "" }, "{opt_light_h}" }
}
if let Some(ref fb) = sp_drought_fb {
tr {
th { "{t(l, \"field.drought_tolerance\")}" }
td {
span { class: "estimated-value", "{fb}" }
span { class: "estimated-tag", "~ species" }
}
}
}
if let Some(ref fb) = sp_usda_fb {
tr {
th { "{t(l, \"field.usda_hardiness_zone\")}" }
td {
span { class: "estimated-value", "{fb}" }
span { class: "estimated-tag", "~ species" }
}
}
}
}
}
}
// Card 7: Species Information
div { class: "detail-card",
div { class: "detail-card-header", "{t(l, \"card.species_info\")}" }
{
let sp_read = species_data.read();
match &*sp_read {
Some(Some(sp)) => {
let ph_range = match (sp.ph_min, sp.ph_max) {
(Some(mn), Some(mx)) => format!("{mn} \u{2013} {mx}"),
(Some(mn), None) => format!("{mn}+"),
(None, Some(mx)) => format!("< {mx}"),
_ => "\u{2014}".to_string(),
};
let sp_layer = sp.plant_layer.as_deref().map(|v| t_val(l, v).to_string()).unwrap_or_else(|| "\u{2014}".to_string());
let sp_drought = sp.drought_tolerance.as_deref().map(|v| t_val(l, v).to_string()).unwrap_or_else(|| "\u{2014}".to_string());
let sp_usda = opt_str(&sp.hardiness_zone_usda);
let sp_nfix = opt_bool_t(sp.nitrogen_fixer, l);
let sp_dynacc = opt_bool_t(sp.dynamic_accumulator, l);
let sp_food = pick_desc(&current_lang, &sp.food_uses_de, &sp.food_uses_en, &sp.food_uses);
let sp_med = pick_desc(&current_lang, &sp.medicinal_uses_de, &sp.medicinal_uses_en, &sp.medicinal_uses);
let sp_other = pick_desc(&current_lang, &sp.other_uses_de, &sp.other_uses_en, &sp.other_uses);
let sp_wildlife = opt_str(&sp.wildlife_value);
let sp_native = opt_str(&sp.native_range);
rsx! {
table { class: "attr-table",
tbody {
tr {
th { "{t(l, \"field.plant_layer\")}" }
td { class: if sp_layer == "\u{2014}" { "placeholder" } else { "" }, "{sp_layer}" }
}
tr {
th { "{t(l, \"field.drought_tolerance\")}" }
td { class: if sp_drought == "\u{2014}" { "placeholder" } else { "" }, "{sp_drought}" }
}
tr {
th { "{t(l, \"field.usda_zone\")}" }
td { class: if sp_usda == "\u{2014}" { "placeholder" } else { "" }, "{sp_usda}" }
}
tr {
th { "{t(l, \"field.ph_range\")}" }
td { class: if ph_range == "\u{2014}" { "placeholder" } else { "" }, "{ph_range}" }
}
tr {
th { "{t(l, \"field.nitrogen_fixer\")}" }
td { class: if sp_nfix == "\u{2014}" { "placeholder" } else { "" }, "{sp_nfix}" }
}
tr {
th { "{t(l, \"field.dynamic_acc\")}" }
td { class: if sp_dynacc == "\u{2014}" { "placeholder" } else { "" }, "{sp_dynacc}" }
}
tr {
th { "{t(l, \"field.food_uses\")}" }
td { class: if sp_food == "\u{2014}" { "placeholder" } else { "" }, "{sp_food}" }
}
tr {
th { "{t(l, \"field.medicinal_uses\")}" }
td { class: if sp_med == "\u{2014}" { "placeholder" } else { "" }, "{sp_med}" }
}
tr {
th { "{t(l, \"field.other_uses\")}" }
td { class: if sp_other == "\u{2014}" { "placeholder" } else { "" }, "{sp_other}" }
}
tr {
th { "{t(l, \"field.wildlife_value\")}" }
td { class: if sp_wildlife == "\u{2014}" { "placeholder" } else { "" }, "{sp_wildlife}" }
}
tr {
th { "{t(l, \"field.native_range\")}" }
td { class: if sp_native == "\u{2014}" { "placeholder" } else { "" }, "{sp_native}" }
}
}
}
}
},
_ => rsx! {
p { class: "placeholder detail-card-empty", "{t(l, \"loading_species_data\")}" }
},
}
}
}
}
}
if has_any_fallback {
p { class: "species-fallback-note",
"{t(l, \"estimated_note\")}"
}
}
}
},
}
}
}
}