diff --git a/herbapi-ui/assets/herbapi.css b/herbapi-ui/assets/herbapi.css index 24efca2..324201e 100644 --- a/herbapi-ui/assets/herbapi.css +++ b/herbapi-ui/assets/herbapi.css @@ -1197,6 +1197,28 @@ td.placeholder { text-decoration: underline; } +/* Species-fallback estimated values */ + +.estimated-value { + color: var(--accent); + font-style: italic; +} + +.estimated-tag { + font-size: 0.7rem; + color: var(--text-muted); + margin-left: 0.4rem; + opacity: 0.7; +} + +.species-fallback-note { + font-size: 0.8rem; + color: var(--text-muted); + font-style: italic; + margin-top: 1.25rem; + opacity: 0.7; +} + /* Responsive */ @media (max-width: 768px) { diff --git a/herbapi-ui/src/pages/cultivars.rs b/herbapi-ui/src/pages/cultivars.rs index ba48d6a..ee25959 100644 --- a/herbapi-ui/src/pages/cultivars.rs +++ b/herbapi-ui/src/pages/cultivars.rs @@ -70,6 +70,27 @@ fn opt_vec_str(val: &Option>) -> String { } } +/// Infer a light requirement string from a species plant_layer value. +fn infer_light_from_layer(layer: &Option) -> Option { + 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"; @@ -385,6 +406,33 @@ pub fn CultivarDetail(slug: String) -> Element { 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 --- + // When a cultivar field is empty, we show species-level + // data styled as an estimation. + 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.clone() + .filter(|s| !s.is_empty()); + 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: prefer primary from images API, else primary_image_key let sp_primary_img: Option = { let imgs = species_images.read(); @@ -673,7 +721,16 @@ pub fn CultivarDetail(slug: String) -> Element { } tr { th { "Light Requirement" } - td { class: if light_req == "\u{2014}" { "placeholder" } else { "" }, "{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 { "Stratification Required" } @@ -710,11 +767,29 @@ pub fn CultivarDetail(slug: String) -> Element { } tr { th { "Light" } - td { class: if light == "\u{2014}" { "placeholder" } else { "" }, "{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 { "Frost Tolerance" } - td { class: if frost_tol == "\u{2014}" { "placeholder" } else { "" }, "{frost_tol}" } + 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 { "Min Light Hours/Day" } @@ -724,6 +799,24 @@ pub fn CultivarDetail(slug: String) -> Element { th { "Optimal Light Hours/Day" } td { class: if opt_light_h == "\u{2014}" { "placeholder" } else { "" }, "{opt_light_h}" } } + if let Some(ref fb) = sp_drought_fb { + tr { + th { "Drought Tolerance" } + td { + span { class: "estimated-value", "{fb}" } + span { class: "estimated-tag", "~ species" } + } + } + } + if let Some(ref fb) = sp_usda_fb { + tr { + th { "USDA Hardiness Zone" } + td { + span { class: "estimated-value", "{fb}" } + span { class: "estimated-tag", "~ species" } + } + } + } } } } @@ -810,6 +903,14 @@ pub fn CultivarDetail(slug: String) -> Element { } } } + + if has_any_fallback { + p { class: "species-fallback-note", + "Values marked " + span { class: "estimated-tag", "~ species" } + " are estimated from species-level data." + } + } } }, }