Add species-level data fallback on cultivar detail page
When cultivar fields are empty, show species-level data as fallback with italic styling and a "~ species" indicator. Applied to: frost tolerance (from USDA zone), light requirement (inferred from plant layer), and climate card (drought tolerance, USDA hardiness zone). Includes a footer note explaining estimated values.
This commit is contained in:
@@ -1197,6 +1197,28 @@ td.placeholder {
|
|||||||
text-decoration: underline;
|
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 */
|
/* Responsive */
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
|||||||
@@ -70,6 +70,27 @@ fn opt_vec_str(val: &Option<Vec<String>>) -> 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_COLS: &str = "herbapi_cultivars_cols";
|
||||||
const STORAGE_KEY_PP: &str = "herbapi_cultivars_pp";
|
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 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");
|
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
|
// Species image: prefer primary from images API, else primary_image_key
|
||||||
let sp_primary_img: Option<crate::types::Image> = {
|
let sp_primary_img: Option<crate::types::Image> = {
|
||||||
let imgs = species_images.read();
|
let imgs = species_images.read();
|
||||||
@@ -673,7 +721,16 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Light Requirement" }
|
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 {
|
tr {
|
||||||
th { "Stratification Required" }
|
th { "Stratification Required" }
|
||||||
@@ -710,11 +767,29 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Light" }
|
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 {
|
tr {
|
||||||
th { "Frost Tolerance" }
|
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 {
|
tr {
|
||||||
th { "Min Light Hours/Day" }
|
th { "Min Light Hours/Day" }
|
||||||
@@ -724,6 +799,24 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
th { "Optimal Light Hours/Day" }
|
th { "Optimal Light Hours/Day" }
|
||||||
td { class: if opt_light_h == "\u{2014}" { "placeholder" } else { "" }, "{opt_light_h}" }
|
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."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user