Add full DE/EN UI label translations across all pages
Add t() translation function to i18n.rs with ~200 key/value pairs covering navigation, card headers, field labels, buttons, filters, planting calendar rows, and general UI text in both German and English. Updated all 11 source files to use t(&lang, "key") instead of hardcoded English strings: app.rs (sidebar nav), all 7 page files (species, cultivars, families, suppliers, home, search, sources), and all 3 component files (planting_calendar, table_controls, plant_card). Boolean values (Yes/No/Annual) are also translated.
This commit is contained in:
+13
-10
@@ -2,6 +2,7 @@ use dioxus::prelude::*;
|
|||||||
use gloo_storage::{LocalStorage, Storage};
|
use gloo_storage::{LocalStorage, Storage};
|
||||||
|
|
||||||
use crate::api;
|
use crate::api;
|
||||||
|
use crate::i18n::t;
|
||||||
use crate::types::MeResponse;
|
use crate::types::MeResponse;
|
||||||
|
|
||||||
/// Global language signal shared via context. Values: "de" or "en".
|
/// Global language signal shared via context. Values: "de" or "en".
|
||||||
@@ -61,6 +62,8 @@ fn Layout() -> Element {
|
|||||||
let auth = use_resource(|| async { api::get_current_user().await.ok() });
|
let auth = use_resource(|| async { api::get_current_user().await.ok() });
|
||||||
let user: Option<MeResponse> = auth.read().as_ref().and_then(|r| r.clone());
|
let user: Option<MeResponse> = auth.read().as_ref().and_then(|r| r.clone());
|
||||||
|
|
||||||
|
let l = &*current_lang;
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "app-layout",
|
div { class: "app-layout",
|
||||||
nav { class: "sidebar",
|
nav { class: "sidebar",
|
||||||
@@ -68,17 +71,17 @@ fn Layout() -> Element {
|
|||||||
span { class: "brand-icon", "\u{1F33F}" }
|
span { class: "brand-icon", "\u{1F33F}" }
|
||||||
div { class: "brand-text-group",
|
div { class: "brand-text-group",
|
||||||
span { class: "brand-text", "HerbAPI" }
|
span { class: "brand-text", "HerbAPI" }
|
||||||
span { class: "brand-sub", "Plant Database" }
|
span { class: "brand-sub", "{t(l, \"brand.sub\")}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div { class: "sidebar-nav",
|
div { class: "sidebar-nav",
|
||||||
NavLink { to: Route::Home {}, label: "Home" }
|
NavLink { to: Route::Home {}, label: t(l, "nav.home") }
|
||||||
NavLink { to: Route::FamilyList {}, label: "Families" }
|
NavLink { to: Route::FamilyList {}, label: t(l, "nav.families") }
|
||||||
NavLink { to: Route::SpeciesList {}, label: "Species" }
|
NavLink { to: Route::SpeciesList {}, label: t(l, "nav.species") }
|
||||||
NavLink { to: Route::CultivarList {}, label: "Cultivars" }
|
NavLink { to: Route::CultivarList {}, label: t(l, "nav.cultivars") }
|
||||||
NavLink { to: Route::SupplierList {}, label: "Suppliers" }
|
NavLink { to: Route::SupplierList {}, label: t(l, "nav.suppliers") }
|
||||||
NavLink { to: Route::SearchPage {}, label: "Search" }
|
NavLink { to: Route::SearchPage {}, label: t(l, "nav.search") }
|
||||||
NavLink { to: Route::Sources {}, label: "Sources" }
|
NavLink { to: Route::Sources {}, label: t(l, "nav.sources") }
|
||||||
}
|
}
|
||||||
div { class: "sidebar-lang",
|
div { class: "sidebar-lang",
|
||||||
div { class: "lang-toggle",
|
div { class: "lang-toggle",
|
||||||
@@ -103,9 +106,9 @@ fn Layout() -> Element {
|
|||||||
div { class: "sidebar-user",
|
div { class: "sidebar-user",
|
||||||
if let Some(ref u) = user {
|
if let Some(ref u) = user {
|
||||||
span { class: "user-name", "{u.nickname.as_deref().or(u.name.as_deref()).unwrap_or(&u.email)}" }
|
span { class: "user-name", "{u.nickname.as_deref().or(u.name.as_deref()).unwrap_or(&u.email)}" }
|
||||||
a { class: "logout-link", href: "/auth/oidc/logout", "Logout" }
|
a { class: "logout-link", href: "/auth/oidc/logout", "{t(l, \"btn.logout\")}" }
|
||||||
} else {
|
} else {
|
||||||
a { class: "login-link", href: "/auth/oidc/login", "Login" }
|
a { class: "login-link", href: "/auth/oidc/login", "{t(l, \"btn.login\")}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
use crate::app::Lang;
|
||||||
|
use crate::i18n::t;
|
||||||
|
|
||||||
/// Month boundaries as week ranges (approximate ISO weeks).
|
/// Month boundaries as week ranges (approximate ISO weeks).
|
||||||
/// Jan=1-4, Feb=5-8, Mar=9-13, Apr=14-17, May=18-22, Jun=23-26,
|
/// Jan=1-4, Feb=5-8, Mar=9-13, Apr=14-17, May=18-22, Jun=23-26,
|
||||||
/// Jul=27-30, Aug=31-35, Sep=36-39, Oct=40-44, Nov=45-48, Dec=49-52
|
/// Jul=27-30, Aug=31-35, Sep=36-39, Oct=40-44, Nov=45-48, Dec=49-52
|
||||||
@@ -54,17 +57,22 @@ pub fn PlantingCalendar(
|
|||||||
glasshouse_weeks: Option<Vec<i32>>,
|
glasshouse_weeks: Option<Vec<i32>>,
|
||||||
harvesting_weeks: Option<Vec<i32>>,
|
harvesting_weeks: Option<Vec<i32>>,
|
||||||
) -> Element {
|
) -> Element {
|
||||||
let rows: Vec<(&str, &str, Option<Vec<i32>>)> = vec![
|
let lang = use_context::<Lang>().0;
|
||||||
("Indoor Sowing", "cal-indoor", resolve_weeks(&indoor_sowing_weeks, &indoor_sowing_months)),
|
let l = lang.read().clone();
|
||||||
("Direct Sowing", "cal-direct", resolve_weeks(&direct_sowing_weeks, &direct_sowing_months)),
|
|
||||||
("Transplanting", "cal-transplant", resolve_weeks(&transplanting_weeks, &transplanting_months)),
|
let row_indoor = (t(&l, "cal.indoor_sowing").to_string(), "cal-indoor".to_string(), resolve_weeks(&indoor_sowing_weeks, &indoor_sowing_months));
|
||||||
("Glasshouse", "cal-glass", resolve_weeks(&glasshouse_weeks, &glasshouse_months)),
|
let row_direct = (t(&l, "cal.direct_sowing").to_string(), "cal-direct".to_string(), resolve_weeks(&direct_sowing_weeks, &direct_sowing_months));
|
||||||
("Harvesting", "cal-harvest", resolve_weeks(&harvesting_weeks, &harvesting_months)),
|
let row_transplant = (t(&l, "cal.transplanting").to_string(), "cal-transplant".to_string(), resolve_weeks(&transplanting_weeks, &transplanting_months));
|
||||||
|
let row_glass = (t(&l, "cal.glasshouse").to_string(), "cal-glass".to_string(), resolve_weeks(&glasshouse_weeks, &glasshouse_months));
|
||||||
|
let row_harvest = (t(&l, "cal.harvesting").to_string(), "cal-harvest".to_string(), resolve_weeks(&harvesting_weeks, &harvesting_months));
|
||||||
|
|
||||||
|
let rows: Vec<(String, String, Option<Vec<i32>>)> = vec![
|
||||||
|
row_indoor, row_direct, row_transplant, row_glass, row_harvest,
|
||||||
];
|
];
|
||||||
|
|
||||||
let has_data = rows.iter().any(|(_, _, w)| w.is_some());
|
let has_data = rows.iter().any(|(_, _, w)| w.is_some());
|
||||||
if !has_data {
|
if !has_data {
|
||||||
return rsx! { p { class: "empty", "No planting calendar data." } };
|
return rsx! { p { class: "empty", "{t(&l, \"no_planting_data\")}" } };
|
||||||
}
|
}
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
@@ -111,23 +119,23 @@ pub fn PlantingCalendar(
|
|||||||
div { class: "wcal-legend",
|
div { class: "wcal-legend",
|
||||||
div { class: "wcal-legend-item",
|
div { class: "wcal-legend-item",
|
||||||
div { class: "wcal-legend-swatch cal-indoor" }
|
div { class: "wcal-legend-swatch cal-indoor" }
|
||||||
span { "Indoor Sowing" }
|
span { "{t(&l, \"cal.indoor_sowing\")}" }
|
||||||
}
|
}
|
||||||
div { class: "wcal-legend-item",
|
div { class: "wcal-legend-item",
|
||||||
div { class: "wcal-legend-swatch cal-direct" }
|
div { class: "wcal-legend-swatch cal-direct" }
|
||||||
span { "Direct Sowing" }
|
span { "{t(&l, \"cal.direct_sowing\")}" }
|
||||||
}
|
}
|
||||||
div { class: "wcal-legend-item",
|
div { class: "wcal-legend-item",
|
||||||
div { class: "wcal-legend-swatch cal-transplant" }
|
div { class: "wcal-legend-swatch cal-transplant" }
|
||||||
span { "Transplanting" }
|
span { "{t(&l, \"cal.transplanting\")}" }
|
||||||
}
|
}
|
||||||
div { class: "wcal-legend-item",
|
div { class: "wcal-legend-item",
|
||||||
div { class: "wcal-legend-swatch cal-glass" }
|
div { class: "wcal-legend-swatch cal-glass" }
|
||||||
span { "Glasshouse" }
|
span { "{t(&l, \"cal.glasshouse\")}" }
|
||||||
}
|
}
|
||||||
div { class: "wcal-legend-item",
|
div { class: "wcal-legend-item",
|
||||||
div { class: "wcal-legend-swatch cal-harvest" }
|
div { class: "wcal-legend-swatch cal-harvest" }
|
||||||
span { "Harvesting" }
|
span { "{t(&l, \"cal.harvesting\")}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ use dioxus::prelude::*;
|
|||||||
use gloo_storage::{LocalStorage, Storage};
|
use gloo_storage::{LocalStorage, Storage};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use crate::app::Lang;
|
||||||
|
use crate::i18n::t;
|
||||||
|
|
||||||
/// Column definition for configurable tables
|
/// Column definition for configurable tables
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct ColumnDef {
|
pub struct ColumnDef {
|
||||||
@@ -36,9 +39,12 @@ pub fn ColumnToggle(
|
|||||||
visible: Signal<HashSet<String>>,
|
visible: Signal<HashSet<String>>,
|
||||||
storage_key: String,
|
storage_key: String,
|
||||||
) -> Element {
|
) -> Element {
|
||||||
|
let lang = use_context::<Lang>().0;
|
||||||
|
let l = lang.read().clone();
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "column-toggle",
|
div { class: "column-toggle",
|
||||||
span { class: "column-toggle-label", "Columns:" }
|
span { class: "column-toggle-label", "{t(&l, \"table.columns\")}" }
|
||||||
for col in columns.iter() {
|
for col in columns.iter() {
|
||||||
{
|
{
|
||||||
let key = col.key.to_string();
|
let key = col.key.to_string();
|
||||||
@@ -74,11 +80,13 @@ pub fn PerPageSelector(
|
|||||||
page: Signal<i64>,
|
page: Signal<i64>,
|
||||||
storage_key: String,
|
storage_key: String,
|
||||||
) -> Element {
|
) -> Element {
|
||||||
|
let lang = use_context::<Lang>().0;
|
||||||
|
let l = lang.read().clone();
|
||||||
let current = *per_page.read();
|
let current = *per_page.read();
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "per-page-selector",
|
div { class: "per-page-selector",
|
||||||
label { "Show " }
|
label { "{t(&l, \"table.show\")} " }
|
||||||
select {
|
select {
|
||||||
value: "{current}",
|
value: "{current}",
|
||||||
onchange: move |e| {
|
onchange: move |e| {
|
||||||
@@ -93,7 +101,7 @@ pub fn PerPageSelector(
|
|||||||
option { value: "50", "50" }
|
option { value: "50", "50" }
|
||||||
option { value: "100", "100" }
|
option { value: "100", "100" }
|
||||||
}
|
}
|
||||||
label { " per page" }
|
label { " {t(&l, \"table.per_page\")}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,3 +23,369 @@ pub fn pick_name(lang: &str, de: &Option<String>, en: &Option<String>, scientifi
|
|||||||
.unwrap_or(scientific).to_string(),
|
.unwrap_or(scientific).to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Translate a UI label key into the appropriate language.
|
||||||
|
/// All keys should be covered; unknown keys return "???".
|
||||||
|
pub fn t(lang: &str, key: &str) -> &'static str {
|
||||||
|
match (lang, key) {
|
||||||
|
// Navigation
|
||||||
|
("de", "nav.home") => "Startseite",
|
||||||
|
("de", "nav.families") => "Familien",
|
||||||
|
("de", "nav.species") => "Arten",
|
||||||
|
("de", "nav.cultivars") => "Sorten",
|
||||||
|
("de", "nav.suppliers") => "Lieferanten",
|
||||||
|
("de", "nav.search") => "Suche",
|
||||||
|
("de", "nav.sources") => "Quellen",
|
||||||
|
|
||||||
|
// Sidebar
|
||||||
|
("de", "brand.sub") => "Pflanzendatenbank",
|
||||||
|
|
||||||
|
// Page headings
|
||||||
|
("de", "page.species") => "Arten",
|
||||||
|
("de", "page.cultivars") => "Sorten",
|
||||||
|
("de", "page.families") => "Pflanzenfamilien",
|
||||||
|
("de", "page.suppliers") => "Lieferanten",
|
||||||
|
("de", "page.search") => "Suche",
|
||||||
|
("de", "page.sources") => "Datenquellen",
|
||||||
|
("de", "page.recent_species") => "Neueste Arten",
|
||||||
|
("de", "page.species_in_family") => "Arten in dieser Familie",
|
||||||
|
|
||||||
|
// Card headers
|
||||||
|
("de", "card.species_details") => "Artdetails",
|
||||||
|
("de", "card.cultivar_details") => "Sortendetails",
|
||||||
|
("de", "card.growing_info") => "Anbauinformationen",
|
||||||
|
("de", "card.growing_requirements") => "Anbauanforderungen",
|
||||||
|
("de", "card.planting_schedule") => "Pflanzkalender",
|
||||||
|
("de", "card.planting_calendar") => "Pflanzkalender",
|
||||||
|
("de", "card.germination") => "Keimung",
|
||||||
|
("de", "card.climate") => "Klima & Umgebung",
|
||||||
|
("de", "card.where_to_buy") => "Bezugsquellen",
|
||||||
|
("de", "card.uses") => "Nutzung",
|
||||||
|
("de", "card.ecology") => "\u{00d6}kologie",
|
||||||
|
("de", "card.permaculture") => "Permakultur",
|
||||||
|
("de", "card.external_links") => "Externe Links",
|
||||||
|
("de", "card.wildlife") => "Wildtiere & \u{00d6}kologie",
|
||||||
|
("de", "card.image") => "Bild",
|
||||||
|
("de", "card.species_info") => "Artinformationen",
|
||||||
|
("de", "card.cultivars") => "Sorten",
|
||||||
|
|
||||||
|
// Field labels
|
||||||
|
("de", "field.scientific_name") => "Wissenschaftlicher Name",
|
||||||
|
("de", "field.common_name") => "Allgemeiner Name",
|
||||||
|
("de", "field.name") => "Name",
|
||||||
|
("de", "field.name_en") => "Name EN",
|
||||||
|
("de", "field.name_de") => "Name DE",
|
||||||
|
("de", "field.family") => "Familie",
|
||||||
|
("de", "field.description") => "Beschreibung",
|
||||||
|
("de", "field.organic") => "Bio",
|
||||||
|
("de", "field.perennial") => "Mehrj\u{00e4}hrig",
|
||||||
|
("de", "field.growing_time") => "Kulturzeit",
|
||||||
|
("de", "field.planting_depth") => "Pflanztiefe",
|
||||||
|
("de", "field.row_spacing") => "Reihenabstand",
|
||||||
|
("de", "field.plant_spacing") => "Pflanzabstand",
|
||||||
|
("de", "field.propagation") => "Vermehrung",
|
||||||
|
("de", "field.days_to_germ") => "Tage bis Keimung",
|
||||||
|
("de", "field.germ_temp") => "Keimtemperatur",
|
||||||
|
("de", "field.light_req") => "Lichtbedarf",
|
||||||
|
("de", "field.stratification") => "Stratifikation erforderlich",
|
||||||
|
("de", "field.stratification_days") => "Stratifikationstage",
|
||||||
|
("de", "field.scarification") => "Skarifizierung erforderlich",
|
||||||
|
("de", "field.min_temp") => "Min. Temperatur",
|
||||||
|
("de", "field.max_temp") => "Max. Temperatur",
|
||||||
|
("de", "field.humidity") => "Luftfeuchtigkeit",
|
||||||
|
("de", "field.light") => "Licht",
|
||||||
|
("de", "field.frost_tolerance") => "Frosttoleranz",
|
||||||
|
("de", "field.drought_tolerance") => "Trockenheitstoleranz",
|
||||||
|
("de", "field.usda_zone") => "USDA-Zone",
|
||||||
|
("de", "field.at_zone") => "AT-Zone",
|
||||||
|
("de", "field.ph_range") => "pH-Bereich",
|
||||||
|
("de", "field.soil_moisture") => "Bodenfeuchte",
|
||||||
|
("de", "field.salt_tolerance") => "Salztoleranz",
|
||||||
|
("de", "field.plant_layer") => "Pflanzenschicht",
|
||||||
|
("de", "field.nitrogen_fixer") => "Stickstoffbinder",
|
||||||
|
("de", "field.dynamic_acc") => "Dynamischer N\u{00e4}hrstoffsammler",
|
||||||
|
("de", "field.food_uses") => "Nahrungsnutzung",
|
||||||
|
("de", "field.medicinal_uses") => "Medizinische Nutzung",
|
||||||
|
("de", "field.other_uses") => "Sonstige Nutzung",
|
||||||
|
("de", "field.wildlife_value") => "Wildtierwert",
|
||||||
|
("de", "field.native_range") => "Herkunftsgebiet",
|
||||||
|
("de", "field.edibility") => "Essbarkeit",
|
||||||
|
("de", "field.pollination") => "Best\u{00e4}ubung",
|
||||||
|
("de", "field.species") => "Art",
|
||||||
|
("de", "field.supplier") => "Lieferant",
|
||||||
|
("de", "field.sku") => "Art.-Nr.",
|
||||||
|
("de", "field.price") => "Preis",
|
||||||
|
("de", "field.link") => "Link",
|
||||||
|
("de", "field.nectar") => "Nektar",
|
||||||
|
("de", "field.pollen") => "Pollen",
|
||||||
|
("de", "field.wild_bees") => "Wildbienen",
|
||||||
|
("de", "field.butterflies") => "Schmetterlinge & Falter",
|
||||||
|
("de", "field.native_status") => "Heimatstatus",
|
||||||
|
("de", "field.succession") => "Sukzessionsstufe",
|
||||||
|
("de", "field.country") => "Land",
|
||||||
|
("de", "field.website") => "Website",
|
||||||
|
("de", "field.notes") => "Anmerkungen",
|
||||||
|
("de", "field.attracts_pollinators") => "Zieht Best\u{00e4}uber an",
|
||||||
|
("de", "field.attracts_beneficial") => "Zieht N\u{00fc}tzlinge an",
|
||||||
|
("de", "field.mulch_plant") => "Mulchpflanze",
|
||||||
|
("de", "field.ground_cover_quality") => "Bodendeckerqualit\u{00e4}t",
|
||||||
|
("de", "field.allelopathic") => "Allelopathisch",
|
||||||
|
("de", "field.guild_role") => "Gildenrolle",
|
||||||
|
("de", "field.wikidata_qid") => "Wikidata QID",
|
||||||
|
("de", "field.gbif_id") => "GBIF ID",
|
||||||
|
("de", "field.eppo_code") => "EPPO-Code",
|
||||||
|
("de", "field.pfaf_url") => "PFAF URL",
|
||||||
|
("de", "field.hoverflies") => "Schwebfliegen",
|
||||||
|
("de", "field.beetles") => "K\u{00e4}fer",
|
||||||
|
("de", "field.birds") => "V\u{00f6}gel",
|
||||||
|
("de", "field.mammals") => "S\u{00e4}ugetiere",
|
||||||
|
("de", "field.naturadb_tags") => "NaturaDB Tags",
|
||||||
|
("de", "field.caterpillar_specialists") => "Raupen-Spezialisten",
|
||||||
|
("de", "field.succession_planting") => "Staffelpflanzung",
|
||||||
|
("de", "field.planting_notes") => "Pflanzhinweise",
|
||||||
|
("de", "field.min_light_hours") => "Min. Lichtstunden/Tag",
|
||||||
|
("de", "field.opt_light_hours") => "Opt. Lichtstunden/Tag",
|
||||||
|
("de", "field.usda_hardiness_zone") => "USDA-Winterh\u{00e4}rtezone",
|
||||||
|
("de", "field.n_fixer") => "N-Binder",
|
||||||
|
("de", "field.dyn_accum") => "Dyn. Sammler",
|
||||||
|
("de", "field.layer") => "Schicht",
|
||||||
|
("de", "field.drought_tol") => "Trockentol.",
|
||||||
|
("de", "field.frost_tol") => "Frosttol.",
|
||||||
|
("de", "field.days_germ") => "Tage Keimung",
|
||||||
|
("de", "field.data_used") => "Verwendete Daten",
|
||||||
|
("de", "field.license") => "Lizenz",
|
||||||
|
("de", "field.view_on_pfaf") => "Auf PFAF ansehen",
|
||||||
|
("de", "field.source") => "Quelle",
|
||||||
|
|
||||||
|
// Planting calendar
|
||||||
|
("de", "cal.indoor_sowing") => "Voranzucht",
|
||||||
|
("de", "cal.direct_sowing") => "Direktsaat",
|
||||||
|
("de", "cal.transplanting") => "Auspflanzen",
|
||||||
|
("de", "cal.glasshouse") => "Gew\u{00e4}chshaus",
|
||||||
|
("de", "cal.harvesting") => "Ernte",
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
("de", "btn.previous") => "Zur\u{00fc}ck",
|
||||||
|
("de", "btn.next") => "Weiter",
|
||||||
|
("de", "btn.view") => "Ansehen",
|
||||||
|
("de", "btn.search") => "Suchen",
|
||||||
|
("de", "btn.table_view") => "Tabellenansicht",
|
||||||
|
("de", "btn.card_view") => "Kartenansicht",
|
||||||
|
("de", "btn.logout") => "Abmelden",
|
||||||
|
("de", "btn.login") => "Anmelden",
|
||||||
|
|
||||||
|
// Table controls
|
||||||
|
("de", "table.columns") => "Spalten:",
|
||||||
|
("de", "table.show") => "Zeige",
|
||||||
|
("de", "table.per_page") => "pro Seite",
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
("de", "filter.layer") => "Schicht",
|
||||||
|
("de", "filter.drought_tol") => "Trockentol.",
|
||||||
|
("de", "filter.all") => "Alle",
|
||||||
|
|
||||||
|
// General
|
||||||
|
("de", "yes") => "Ja",
|
||||||
|
("de", "no") => "Nein",
|
||||||
|
("de", "annual") => "Einj\u{00e4}hrig",
|
||||||
|
("de", "loading") => "Laden\u{2026}",
|
||||||
|
("de", "estimated_note") => "Mit ~ markierte Werte sind von der Art gesch\u{00e4}tzt",
|
||||||
|
("de", "page") => "Seite",
|
||||||
|
("de", "of") => "von",
|
||||||
|
("de", "no_cultivars") => "Noch keine Sorten.",
|
||||||
|
("de", "no_suppliers_linked") => "Keine Lieferanten verkn\u{00fc}pft",
|
||||||
|
("de", "no_planting_data") => "Keine Pflanzkalenderdaten.",
|
||||||
|
("de", "results") => "Ergebnisse",
|
||||||
|
("de", "search.placeholder") => "Pflanzen suchen\u{2026}",
|
||||||
|
("de", "search.placeholder_species") => "Arten suchen\u{2026}",
|
||||||
|
("de", "search.placeholder_cultivars") => "Sorten suchen\u{2026}",
|
||||||
|
("de", "search.placeholder_families") => "Familien suchen\u{2026}",
|
||||||
|
("de", "search.placeholder_full") => "Pflanzen, Familien, Sorten suchen\u{2026}",
|
||||||
|
("de", "subtitle") => "Dreisprachige Pflanzen-Referenzdatenbank",
|
||||||
|
("de", "sources.intro") => "HerbAPI aggregiert Pflanzendaten aus mehreren offenen Quellen. Wir danken diesen Projekten, dass sie botanisches Wissen frei zug\u{00e4}nglich machen.",
|
||||||
|
("de", "species_species") => "Arten",
|
||||||
|
("de", "loading_species_data") => "Lade Artdaten\u{2026}",
|
||||||
|
|
||||||
|
// ── English defaults ──────────────────────────────────────
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
(_, "nav.home") => "Home",
|
||||||
|
(_, "nav.families") => "Families",
|
||||||
|
(_, "nav.species") => "Species",
|
||||||
|
(_, "nav.cultivars") => "Cultivars",
|
||||||
|
(_, "nav.suppliers") => "Suppliers",
|
||||||
|
(_, "nav.search") => "Search",
|
||||||
|
(_, "nav.sources") => "Sources",
|
||||||
|
|
||||||
|
// Sidebar
|
||||||
|
(_, "brand.sub") => "Plant Database",
|
||||||
|
|
||||||
|
// Page headings
|
||||||
|
(_, "page.species") => "Species",
|
||||||
|
(_, "page.cultivars") => "Cultivars",
|
||||||
|
(_, "page.families") => "Plant Families",
|
||||||
|
(_, "page.suppliers") => "Suppliers",
|
||||||
|
(_, "page.search") => "Search",
|
||||||
|
(_, "page.sources") => "Data Sources",
|
||||||
|
(_, "page.recent_species") => "Recent Species",
|
||||||
|
(_, "page.species_in_family") => "Species in this Family",
|
||||||
|
|
||||||
|
// Card headers
|
||||||
|
(_, "card.species_details") => "Species Details",
|
||||||
|
(_, "card.cultivar_details") => "Cultivar Details",
|
||||||
|
(_, "card.growing_info") => "Growing Information",
|
||||||
|
(_, "card.growing_requirements") => "Growing Requirements",
|
||||||
|
(_, "card.planting_schedule") => "Planting Schedule",
|
||||||
|
(_, "card.planting_calendar") => "Planting Calendar",
|
||||||
|
(_, "card.germination") => "Germination",
|
||||||
|
(_, "card.climate") => "Climate & Environment",
|
||||||
|
(_, "card.where_to_buy") => "Where to Buy",
|
||||||
|
(_, "card.uses") => "Uses",
|
||||||
|
(_, "card.ecology") => "Ecology",
|
||||||
|
(_, "card.permaculture") => "Permaculture",
|
||||||
|
(_, "card.external_links") => "External Links",
|
||||||
|
(_, "card.wildlife") => "Wildlife & Ecology",
|
||||||
|
(_, "card.image") => "Image",
|
||||||
|
(_, "card.species_info") => "Species Information",
|
||||||
|
(_, "card.cultivars") => "Cultivars",
|
||||||
|
|
||||||
|
// Field labels
|
||||||
|
(_, "field.scientific_name") => "Scientific Name",
|
||||||
|
(_, "field.common_name") => "Common Name",
|
||||||
|
(_, "field.name") => "Name",
|
||||||
|
(_, "field.name_en") => "Name EN",
|
||||||
|
(_, "field.name_de") => "Name DE",
|
||||||
|
(_, "field.family") => "Family",
|
||||||
|
(_, "field.description") => "Description",
|
||||||
|
(_, "field.organic") => "Organic",
|
||||||
|
(_, "field.perennial") => "Perennial",
|
||||||
|
(_, "field.growing_time") => "Growing Time",
|
||||||
|
(_, "field.planting_depth") => "Planting Depth",
|
||||||
|
(_, "field.row_spacing") => "Row Spacing",
|
||||||
|
(_, "field.plant_spacing") => "Plant Spacing",
|
||||||
|
(_, "field.propagation") => "Propagation Methods",
|
||||||
|
(_, "field.days_to_germ") => "Days to Germination",
|
||||||
|
(_, "field.germ_temp") => "Germination Temp",
|
||||||
|
(_, "field.light_req") => "Light Requirement",
|
||||||
|
(_, "field.stratification") => "Stratification Required",
|
||||||
|
(_, "field.stratification_days") => "Stratification Days",
|
||||||
|
(_, "field.scarification") => "Scarification Required",
|
||||||
|
(_, "field.min_temp") => "Min Temp",
|
||||||
|
(_, "field.max_temp") => "Max Temp",
|
||||||
|
(_, "field.humidity") => "Humidity",
|
||||||
|
(_, "field.light") => "Light",
|
||||||
|
(_, "field.frost_tolerance") => "Frost Tolerance",
|
||||||
|
(_, "field.drought_tolerance") => "Drought Tolerance",
|
||||||
|
(_, "field.usda_zone") => "USDA Zone",
|
||||||
|
(_, "field.at_zone") => "AT Zone",
|
||||||
|
(_, "field.ph_range") => "pH Range",
|
||||||
|
(_, "field.soil_moisture") => "Soil Moisture",
|
||||||
|
(_, "field.salt_tolerance") => "Salt Tolerance",
|
||||||
|
(_, "field.plant_layer") => "Plant Layer",
|
||||||
|
(_, "field.nitrogen_fixer") => "Nitrogen Fixer",
|
||||||
|
(_, "field.dynamic_acc") => "Dynamic Accumulator",
|
||||||
|
(_, "field.food_uses") => "Food Uses",
|
||||||
|
(_, "field.medicinal_uses") => "Medicinal Uses",
|
||||||
|
(_, "field.other_uses") => "Other Uses",
|
||||||
|
(_, "field.wildlife_value") => "Wildlife Value",
|
||||||
|
(_, "field.native_range") => "Native Range",
|
||||||
|
(_, "field.edibility") => "Edibility Rating",
|
||||||
|
(_, "field.pollination") => "Pollination Type",
|
||||||
|
(_, "field.species") => "Species",
|
||||||
|
(_, "field.supplier") => "Supplier",
|
||||||
|
(_, "field.sku") => "SKU",
|
||||||
|
(_, "field.price") => "Price",
|
||||||
|
(_, "field.link") => "Link",
|
||||||
|
(_, "field.nectar") => "Nectar Value",
|
||||||
|
(_, "field.pollen") => "Pollen Value",
|
||||||
|
(_, "field.wild_bees") => "Wild Bees",
|
||||||
|
(_, "field.butterflies") => "Butterflies & Moths",
|
||||||
|
(_, "field.native_status") => "Native Status",
|
||||||
|
(_, "field.succession") => "Succession Stage",
|
||||||
|
(_, "field.country") => "Country",
|
||||||
|
(_, "field.website") => "Website",
|
||||||
|
(_, "field.notes") => "Notes",
|
||||||
|
(_, "field.attracts_pollinators") => "Attracts Pollinators",
|
||||||
|
(_, "field.attracts_beneficial") => "Attracts Beneficial Insects",
|
||||||
|
(_, "field.mulch_plant") => "Mulch Plant",
|
||||||
|
(_, "field.ground_cover_quality") => "Ground Cover Quality",
|
||||||
|
(_, "field.allelopathic") => "Allelopathic",
|
||||||
|
(_, "field.guild_role") => "Guild Role",
|
||||||
|
(_, "field.wikidata_qid") => "Wikidata QID",
|
||||||
|
(_, "field.gbif_id") => "GBIF ID",
|
||||||
|
(_, "field.eppo_code") => "EPPO Code",
|
||||||
|
(_, "field.pfaf_url") => "PFAF URL",
|
||||||
|
(_, "field.hoverflies") => "Hoverflies",
|
||||||
|
(_, "field.beetles") => "Beetles",
|
||||||
|
(_, "field.birds") => "Birds",
|
||||||
|
(_, "field.mammals") => "Mammals",
|
||||||
|
(_, "field.naturadb_tags") => "NaturaDB Tags",
|
||||||
|
(_, "field.caterpillar_specialists") => "Caterpillar Specialists",
|
||||||
|
(_, "field.succession_planting") => "Succession Planting",
|
||||||
|
(_, "field.planting_notes") => "Planting Notes",
|
||||||
|
(_, "field.min_light_hours") => "Min Light Hours/Day",
|
||||||
|
(_, "field.opt_light_hours") => "Optimal Light Hours/Day",
|
||||||
|
(_, "field.usda_hardiness_zone") => "USDA Hardiness Zone",
|
||||||
|
(_, "field.n_fixer") => "N-Fixer",
|
||||||
|
(_, "field.dyn_accum") => "Dyn. Accum.",
|
||||||
|
(_, "field.layer") => "Layer",
|
||||||
|
(_, "field.drought_tol") => "Drought Tol.",
|
||||||
|
(_, "field.frost_tol") => "Frost Tol.",
|
||||||
|
(_, "field.days_germ") => "Days to Germ.",
|
||||||
|
(_, "field.data_used") => "Data used",
|
||||||
|
(_, "field.license") => "License",
|
||||||
|
(_, "field.view_on_pfaf") => "View on PFAF",
|
||||||
|
(_, "field.source") => "Source",
|
||||||
|
|
||||||
|
// Planting calendar
|
||||||
|
(_, "cal.indoor_sowing") => "Indoor Sowing",
|
||||||
|
(_, "cal.direct_sowing") => "Direct Sowing",
|
||||||
|
(_, "cal.transplanting") => "Transplanting",
|
||||||
|
(_, "cal.glasshouse") => "Glasshouse",
|
||||||
|
(_, "cal.harvesting") => "Harvesting",
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
(_, "btn.previous") => "Previous",
|
||||||
|
(_, "btn.next") => "Next",
|
||||||
|
(_, "btn.view") => "View",
|
||||||
|
(_, "btn.search") => "Search",
|
||||||
|
(_, "btn.table_view") => "Table View",
|
||||||
|
(_, "btn.card_view") => "Card View",
|
||||||
|
(_, "btn.logout") => "Logout",
|
||||||
|
(_, "btn.login") => "Login",
|
||||||
|
|
||||||
|
// Table controls
|
||||||
|
(_, "table.columns") => "Columns:",
|
||||||
|
(_, "table.show") => "Show",
|
||||||
|
(_, "table.per_page") => "per page",
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
(_, "filter.layer") => "Layer",
|
||||||
|
(_, "filter.drought_tol") => "Drought Tol.",
|
||||||
|
(_, "filter.all") => "All",
|
||||||
|
|
||||||
|
// General
|
||||||
|
(_, "yes") => "Yes",
|
||||||
|
(_, "no") => "No",
|
||||||
|
(_, "annual") => "Annual",
|
||||||
|
(_, "loading") => "Loading...",
|
||||||
|
(_, "estimated_note") => "Values marked ~ species are estimated from species-level data",
|
||||||
|
(_, "page") => "Page",
|
||||||
|
(_, "of") => "of",
|
||||||
|
(_, "no_cultivars") => "No cultivars yet.",
|
||||||
|
(_, "no_suppliers_linked") => "No suppliers linked",
|
||||||
|
(_, "no_planting_data") => "No planting calendar data.",
|
||||||
|
(_, "results") => "results",
|
||||||
|
(_, "search.placeholder") => "Search plants...",
|
||||||
|
(_, "search.placeholder_species") => "Search species...",
|
||||||
|
(_, "search.placeholder_cultivars") => "Search cultivars...",
|
||||||
|
(_, "search.placeholder_families") => "Search families...",
|
||||||
|
(_, "search.placeholder_full") => "Search plants, families, cultivars...",
|
||||||
|
(_, "subtitle") => "Trilingual plant reference database",
|
||||||
|
(_, "sources.intro") => "HerbAPI aggregates plant data from multiple open sources. We are grateful to these projects for making botanical knowledge freely available.",
|
||||||
|
(_, "species_species") => "species",
|
||||||
|
(_, "loading_species_data") => "Loading species data\u{2026}",
|
||||||
|
|
||||||
|
_ => "???",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+101
-102
@@ -6,7 +6,7 @@ use crate::api;
|
|||||||
use crate::app::{Lang, Route};
|
use crate::app::{Lang, Route};
|
||||||
use crate::components::planting_calendar::PlantingCalendar;
|
use crate::components::planting_calendar::PlantingCalendar;
|
||||||
use crate::components::table_controls::*;
|
use crate::components::table_controls::*;
|
||||||
use crate::i18n::{pick_desc, pick_name};
|
use crate::i18n::{pick_desc, pick_name, t};
|
||||||
|
|
||||||
/// Convert a month-number array (1=Jan..12=Dec) to a comma-separated string of abbreviations.
|
/// Convert a month-number array (1=Jan..12=Dec) to a comma-separated string of abbreviations.
|
||||||
fn months_display(months: &Option<Vec<i32>>) -> String {
|
fn months_display(months: &Option<Vec<i32>>) -> String {
|
||||||
@@ -48,18 +48,18 @@ fn opt_i32_suffix(val: Option<i32>, suffix: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format an Option<bool> as Yes / No / em dash.
|
/// Format an Option<bool> as Yes / No / em dash (translated).
|
||||||
fn opt_bool(val: Option<bool>) -> String {
|
fn opt_bool_t(val: Option<bool>, lang: &str) -> String {
|
||||||
match val {
|
match val {
|
||||||
Some(true) => "Yes".to_string(),
|
Some(true) => t(lang, "yes").to_string(),
|
||||||
Some(false) => "No".to_string(),
|
Some(false) => t(lang, "no").to_string(),
|
||||||
None => "\u{2014}".to_string(),
|
None => "\u{2014}".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bool field: always present (non-Option).
|
/// Bool field: always present (non-Option), translated.
|
||||||
fn bool_display(val: bool) -> &'static str {
|
fn bool_display_t(val: bool, lang: &str) -> String {
|
||||||
if val { "Yes" } else { "No" }
|
if val { t(lang, "yes").to_string() } else { t(lang, "no").to_string() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format an Option<Vec<String>> as comma-separated or em dash.
|
/// Format an Option<Vec<String>> as comma-separated or em dash.
|
||||||
@@ -109,6 +109,7 @@ fn cultivar_columns() -> Vec<ColumnDef> {
|
|||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn CultivarList() -> Element {
|
pub fn CultivarList() -> Element {
|
||||||
|
let lang = use_context::<Lang>().0;
|
||||||
let columns = cultivar_columns();
|
let columns = cultivar_columns();
|
||||||
let mut page = use_signal(|| 1i64);
|
let mut page = use_signal(|| 1i64);
|
||||||
let per_page = use_signal(|| load_per_page(STORAGE_KEY_PP, 25));
|
let per_page = use_signal(|| load_per_page(STORAGE_KEY_PP, 25));
|
||||||
@@ -146,16 +147,17 @@ pub fn CultivarList() -> Element {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let current_page = *page.read();
|
let current_page = *page.read();
|
||||||
|
let l = lang.read().clone();
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "page",
|
div { class: "page",
|
||||||
h1 { "Cultivars" }
|
h1 { "{t(&l, \"page.cultivars\")}" }
|
||||||
|
|
||||||
div { class: "table-toolbar",
|
div { class: "table-toolbar",
|
||||||
div { class: "search-bar",
|
div { class: "search-bar",
|
||||||
input {
|
input {
|
||||||
r#type: "text",
|
r#type: "text",
|
||||||
placeholder: "Search cultivars...",
|
placeholder: "{t(&l, \"search.placeholder_cultivars\")}",
|
||||||
value: "{search}",
|
value: "{search}",
|
||||||
oninput: move |e| {
|
oninput: move |e| {
|
||||||
search.set(e.value());
|
search.set(e.value());
|
||||||
@@ -177,7 +179,7 @@ pub fn CultivarList() -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match &*cultivars.read() {
|
match &*cultivars.read() {
|
||||||
None => rsx! { p { "Loading..." } },
|
None => rsx! { p { "{t(&l, \"loading\")}" } },
|
||||||
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
|
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
|
||||||
Some(Ok(data)) => {
|
Some(Ok(data)) => {
|
||||||
let smap = species_map.read();
|
let smap = species_map.read();
|
||||||
@@ -188,34 +190,34 @@ pub fn CultivarList() -> Element {
|
|||||||
};
|
};
|
||||||
let vis = visible_cols.read();
|
let vis = visible_cols.read();
|
||||||
rsx! {
|
rsx! {
|
||||||
p { class: "result-count", "{data.total} cultivars" }
|
p { class: "result-count", "{data.total} {t(&l, \"nav.cultivars\").to_lowercase()}" }
|
||||||
div { class: "table-wrap",
|
div { class: "table-wrap",
|
||||||
table {
|
table {
|
||||||
thead {
|
thead {
|
||||||
tr {
|
tr {
|
||||||
if is_col_visible(&vis, "name") {
|
if is_col_visible(&vis, "name") {
|
||||||
th { "Name" }
|
th { "{t(&l, \"field.name\")}" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "species") {
|
if is_col_visible(&vis, "species") {
|
||||||
th { "Species" }
|
th { "{t(&l, \"field.species\")}" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "organic") {
|
if is_col_visible(&vis, "organic") {
|
||||||
th { "Organic" }
|
th { "{t(&l, \"field.organic\")}" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "perennial") {
|
if is_col_visible(&vis, "perennial") {
|
||||||
th { "Perennial" }
|
th { "{t(&l, \"field.perennial\")}" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "description") {
|
if is_col_visible(&vis, "description") {
|
||||||
th { "Description" }
|
th { "{t(&l, \"field.description\")}" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "frost_tolerance") {
|
if is_col_visible(&vis, "frost_tolerance") {
|
||||||
th { "Frost Tol." }
|
th { "{t(&l, \"field.frost_tol\")}" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "growing_time") {
|
if is_col_visible(&vis, "growing_time") {
|
||||||
th { "Growing Time" }
|
th { "{t(&l, \"field.growing_time\")}" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "days_germ") {
|
if is_col_visible(&vis, "days_germ") {
|
||||||
th { "Days to Germ." }
|
th { "{t(&l, \"field.days_germ\")}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,14 +240,14 @@ pub fn CultivarList() -> Element {
|
|||||||
if is_col_visible(&vis, "organic") {
|
if is_col_visible(&vis, "organic") {
|
||||||
td {
|
td {
|
||||||
if c.is_organic {
|
if c.is_organic {
|
||||||
span { class: "badge organic", "Yes" }
|
span { class: "badge organic", "{t(&l, \"yes\")}" }
|
||||||
} else {
|
} else {
|
||||||
"-"
|
"-"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "perennial") {
|
if is_col_visible(&vis, "perennial") {
|
||||||
td { if c.perennial { "Yes" } else { "Annual" } }
|
td { if c.perennial { "{t(&l, \"yes\")}" } else { "{t(&l, \"annual\")}" } }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "description") {
|
if is_col_visible(&vis, "description") {
|
||||||
td { class: "cell-truncated",
|
td { class: "cell-truncated",
|
||||||
@@ -283,13 +285,13 @@ pub fn CultivarList() -> Element {
|
|||||||
button {
|
button {
|
||||||
disabled: current_page <= 1,
|
disabled: current_page <= 1,
|
||||||
onclick: move |_| page.set(current_page - 1),
|
onclick: move |_| page.set(current_page - 1),
|
||||||
"Previous"
|
"{t(&l, \"btn.previous\")}"
|
||||||
}
|
}
|
||||||
span { "Page {current_page} of {(data.total + data.per_page - 1) / data.per_page}" }
|
span { "{t(&l, \"page\")} {current_page} {t(&l, \"of\")} {(data.total + data.per_page - 1) / data.per_page}" }
|
||||||
button {
|
button {
|
||||||
disabled: current_page * data.per_page >= data.total,
|
disabled: current_page * data.per_page >= data.total,
|
||||||
onclick: move |_| page.set(current_page + 1),
|
onclick: move |_| page.set(current_page + 1),
|
||||||
"Next"
|
"{t(&l, \"btn.next\")}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -359,10 +361,11 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
rsx! {
|
rsx! {
|
||||||
div { class: "page cultivar-detail",
|
div { class: "page cultivar-detail",
|
||||||
match &*cultivar.read() {
|
match &*cultivar.read() {
|
||||||
None => rsx! { p { "Loading..." } },
|
None => rsx! { p { "{t(&lang.read(), \"loading\")}" } },
|
||||||
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
|
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
|
||||||
Some(Ok(c)) => {
|
Some(Ok(c)) => {
|
||||||
let current_lang = lang.read().clone();
|
let current_lang = lang.read().clone();
|
||||||
|
let l = &*current_lang;
|
||||||
|
|
||||||
// Pre-compute display strings outside of rsx
|
// Pre-compute display strings outside of rsx
|
||||||
let common_name = pick_name(¤t_lang, &c.name_de, &c.name_en, &c.name);
|
let common_name = pick_name(¤t_lang, &c.name_de, &c.name_en, &c.name);
|
||||||
@@ -370,8 +373,8 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
let name_de = opt_str(&c.name_de);
|
let name_de = opt_str(&c.name_de);
|
||||||
let name_sci = opt_str(&c.name_scientific);
|
let name_sci = opt_str(&c.name_scientific);
|
||||||
let desc = pick_desc(¤t_lang, &c.description_de, &c.description_en, &c.description);
|
let desc = pick_desc(¤t_lang, &c.description_de, &c.description_en, &c.description);
|
||||||
let organic = bool_display(c.is_organic);
|
let organic = bool_display_t(c.is_organic, l);
|
||||||
let perennial = bool_display(c.perennial);
|
let perennial = bool_display_t(c.perennial, l);
|
||||||
|
|
||||||
// Planting schedule
|
// Planting schedule
|
||||||
let indoor = months_display(&c.indoor_sowing_months);
|
let indoor = months_display(&c.indoor_sowing_months);
|
||||||
@@ -393,9 +396,9 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
let days_germ = opt_i32_suffix(c.days_to_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 germ_temp = opt_f64_suffix(c.germination_temp_c, " \u{00b0}C");
|
||||||
let light_req = opt_str(&c.light_requirement);
|
let light_req = opt_str(&c.light_requirement);
|
||||||
let strat_req = opt_bool(c.stratification_required);
|
let strat_req = opt_bool_t(c.stratification_required, l);
|
||||||
let strat_days = opt_i32_suffix(c.stratification_days, "");
|
let strat_days = opt_i32_suffix(c.stratification_days, "");
|
||||||
let scar_req = opt_bool(c.scarification_required);
|
let scar_req = opt_bool_t(c.scarification_required, l);
|
||||||
|
|
||||||
// Climate
|
// Climate
|
||||||
let min_temp = opt_f64_suffix(c.min_temp, " \u{00b0}C");
|
let min_temp = opt_f64_suffix(c.min_temp, " \u{00b0}C");
|
||||||
@@ -407,8 +410,6 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
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 ---
|
// --- 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_frost_fb, sp_light_fb, sp_drought_fb, sp_usda_fb) = {
|
||||||
let sp_read = species_data.read();
|
let sp_read = species_data.read();
|
||||||
match &*sp_read {
|
match &*sp_read {
|
||||||
@@ -433,7 +434,7 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
|| sp_drought_fb.is_some()
|
|| sp_drought_fb.is_some()
|
||||||
|| sp_usda_fb.is_some();
|
|| sp_usda_fb.is_some();
|
||||||
|
|
||||||
// Species image: prefer primary from images API, else primary_image_key
|
// Species image
|
||||||
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();
|
||||||
match &*imgs {
|
match &*imgs {
|
||||||
@@ -460,7 +461,7 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
|
|
||||||
// Week-based planting calendar (full width)
|
// Week-based planting calendar (full width)
|
||||||
div { class: "detail-card", style: "margin-top: 1.25rem;",
|
div { class: "detail-card", style: "margin-top: 1.25rem;",
|
||||||
div { class: "detail-card-header", "Planting Calendar" }
|
div { class: "detail-card-header", "{t(l, \"card.planting_calendar\")}" }
|
||||||
div { style: "padding: 0.75rem;",
|
div { style: "padding: 0.75rem;",
|
||||||
PlantingCalendar {
|
PlantingCalendar {
|
||||||
indoor_sowing_months: c.indoor_sowing_months.clone(),
|
indoor_sowing_months: c.indoor_sowing_months.clone(),
|
||||||
@@ -483,27 +484,27 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
|
|
||||||
// Card 1: Cultivar Details
|
// Card 1: Cultivar Details
|
||||||
div { class: "detail-card",
|
div { class: "detail-card",
|
||||||
div { class: "detail-card-header", "Cultivar Details" }
|
div { class: "detail-card-header", "{t(l, \"card.cultivar_details\")}" }
|
||||||
table { class: "attr-table",
|
table { class: "attr-table",
|
||||||
tbody {
|
tbody {
|
||||||
tr {
|
tr {
|
||||||
th { "Name" }
|
th { "{t(l, \"field.name\")}" }
|
||||||
td { "{c.name}" }
|
td { "{c.name}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Common Name" }
|
th { "{t(l, \"field.common_name\")}" }
|
||||||
td { "{common_name}" }
|
td { "{common_name}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Name EN" }
|
th { "{t(l, \"field.name_en\")}" }
|
||||||
td { class: if name_en == "\u{2014}" { "placeholder" } else { "" }, "{name_en}" }
|
td { class: if name_en == "\u{2014}" { "placeholder" } else { "" }, "{name_en}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Name DE" }
|
th { "{t(l, \"field.name_de\")}" }
|
||||||
td { class: if name_de == "\u{2014}" { "placeholder" } else { "" }, "{name_de}" }
|
td { class: if name_de == "\u{2014}" { "placeholder" } else { "" }, "{name_de}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Scientific Name" }
|
th { "{t(l, \"field.scientific_name\")}" }
|
||||||
td { class: if name_sci == "\u{2014}" { "placeholder" } else { "" },
|
td { class: if name_sci == "\u{2014}" { "placeholder" } else { "" },
|
||||||
if name_sci != "\u{2014}" {
|
if name_sci != "\u{2014}" {
|
||||||
em { "{name_sci}" }
|
em { "{name_sci}" }
|
||||||
@@ -513,7 +514,7 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Species" }
|
th { "{t(l, \"field.species\")}" }
|
||||||
td {
|
td {
|
||||||
if let Some(Some(ref sp)) = *species_data.read() {
|
if let Some(Some(ref sp)) = *species_data.read() {
|
||||||
Link { to: Route::SpeciesDetail { slug: sp.slug.clone() },
|
Link { to: Route::SpeciesDetail { slug: sp.slug.clone() },
|
||||||
@@ -530,15 +531,15 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Description" }
|
th { "{t(l, \"field.description\")}" }
|
||||||
td { class: if desc == "\u{2014}" { "placeholder" } else { "" }, "{desc}" }
|
td { class: if desc == "\u{2014}" { "placeholder" } else { "" }, "{desc}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Organic" }
|
th { "{t(l, \"field.organic\")}" }
|
||||||
td { "{organic}" }
|
td { "{organic}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Perennial" }
|
th { "{t(l, \"field.perennial\")}" }
|
||||||
td { "{perennial}" }
|
td { "{perennial}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -547,35 +548,35 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
|
|
||||||
// Card 2: Planting Schedule
|
// Card 2: Planting Schedule
|
||||||
div { class: "detail-card",
|
div { class: "detail-card",
|
||||||
div { class: "detail-card-header", "Planting Schedule" }
|
div { class: "detail-card-header", "{t(l, \"card.planting_schedule\")}" }
|
||||||
table { class: "attr-table",
|
table { class: "attr-table",
|
||||||
tbody {
|
tbody {
|
||||||
tr {
|
tr {
|
||||||
th { "Indoor Sowing" }
|
th { "{t(l, \"cal.indoor_sowing\")}" }
|
||||||
td { class: if indoor == "\u{2014}" { "placeholder" } else { "" }, "{indoor}" }
|
td { class: if indoor == "\u{2014}" { "placeholder" } else { "" }, "{indoor}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Direct Sowing" }
|
th { "{t(l, \"cal.direct_sowing\")}" }
|
||||||
td { class: if direct == "\u{2014}" { "placeholder" } else { "" }, "{direct}" }
|
td { class: if direct == "\u{2014}" { "placeholder" } else { "" }, "{direct}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Transplanting" }
|
th { "{t(l, \"cal.transplanting\")}" }
|
||||||
td { class: if transplant == "\u{2014}" { "placeholder" } else { "" }, "{transplant}" }
|
td { class: if transplant == "\u{2014}" { "placeholder" } else { "" }, "{transplant}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Glasshouse" }
|
th { "{t(l, \"cal.glasshouse\")}" }
|
||||||
td { class: if glasshouse == "\u{2014}" { "placeholder" } else { "" }, "{glasshouse}" }
|
td { class: if glasshouse == "\u{2014}" { "placeholder" } else { "" }, "{glasshouse}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Harvesting" }
|
th { "{t(l, \"cal.harvesting\")}" }
|
||||||
td { class: if harvest == "\u{2014}" { "placeholder" } else { "" }, "{harvest}" }
|
td { class: if harvest == "\u{2014}" { "placeholder" } else { "" }, "{harvest}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Succession Planting" }
|
th { "{t(l, \"field.succession_planting\")}" }
|
||||||
td { class: if succession == "\u{2014}" { "placeholder" } else { "" }, "{succession}" }
|
td { class: if succession == "\u{2014}" { "placeholder" } else { "" }, "{succession}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Planting Notes" }
|
th { "{t(l, \"field.planting_notes\")}" }
|
||||||
td { class: if planting_notes == "\u{2014}" { "placeholder" } else { "" }, "{planting_notes}" }
|
td { class: if planting_notes == "\u{2014}" { "placeholder" } else { "" }, "{planting_notes}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -584,7 +585,7 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
|
|
||||||
// Card 3: Where to Buy
|
// Card 3: Where to Buy
|
||||||
div { class: "detail-card",
|
div { class: "detail-card",
|
||||||
div { class: "detail-card-header", "Where to Buy" }
|
div { class: "detail-card-header", "{t(l, \"card.where_to_buy\")}" }
|
||||||
{
|
{
|
||||||
let sup_links = suppliers_data.read();
|
let sup_links = suppliers_data.read();
|
||||||
let sups = all_suppliers.read();
|
let sups = all_suppliers.read();
|
||||||
@@ -598,10 +599,10 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
table { class: "attr-table",
|
table { class: "attr-table",
|
||||||
thead {
|
thead {
|
||||||
tr {
|
tr {
|
||||||
th { "Supplier" }
|
th { "{t(l, \"field.supplier\")}" }
|
||||||
th { "SKU" }
|
th { "{t(l, \"field.sku\")}" }
|
||||||
th { "Price" }
|
th { "{t(l, \"field.price\")}" }
|
||||||
th { "Link" }
|
th { "{t(l, \"field.link\")}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tbody {
|
tbody {
|
||||||
@@ -625,7 +626,7 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
td {
|
td {
|
||||||
if let Some(ref u) = url {
|
if let Some(ref u) = url {
|
||||||
if !u.is_empty() {
|
if !u.is_empty() {
|
||||||
a { href: "{u}", target: "_blank", class: "external-link", "View" }
|
a { href: "{u}", target: "_blank", class: "external-link", "{t(l, \"btn.view\")}" }
|
||||||
} else {
|
} else {
|
||||||
span { class: "placeholder", "\u{2014}" }
|
span { class: "placeholder", "\u{2014}" }
|
||||||
}
|
}
|
||||||
@@ -642,7 +643,7 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
_ => rsx! {
|
_ => rsx! {
|
||||||
p { class: "placeholder detail-card-empty", "\u{2014} No suppliers linked" }
|
p { class: "placeholder detail-card-empty", "\u{2014} {t(l, \"no_suppliers_linked\")}" }
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -655,7 +656,7 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
// Image card
|
// Image card
|
||||||
if let Some(ref key) = sp_img_key {
|
if let Some(ref key) = sp_img_key {
|
||||||
div { class: "detail-card",
|
div { class: "detail-card",
|
||||||
div { class: "detail-card-header", "Image" }
|
div { class: "detail-card-header", "{t(l, \"card.image\")}" }
|
||||||
div { class: "image-card-body",
|
div { class: "image-card-body",
|
||||||
img { class: "species-image", src: "/img/{key}", alt: "{c.name}" }
|
img { class: "species-image", src: "/img/{key}", alt: "{c.name}" }
|
||||||
if let Some(ref img) = sp_primary_img {
|
if let Some(ref img) = sp_primary_img {
|
||||||
@@ -673,7 +674,7 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
if img.caption.is_some() || img.license.is_some() {
|
if img.caption.is_some() || img.license.is_some() {
|
||||||
" | "
|
" | "
|
||||||
}
|
}
|
||||||
a { href: "{url}", target: "_blank", class: "attribution-link", "Source" }
|
a { href: "{url}", target: "_blank", class: "attribution-link", "{t(l, \"field.source\")}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -683,27 +684,27 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
|
|
||||||
// Card 4: Growing Information
|
// Card 4: Growing Information
|
||||||
div { class: "detail-card",
|
div { class: "detail-card",
|
||||||
div { class: "detail-card-header", "Growing Information" }
|
div { class: "detail-card-header", "{t(l, \"card.growing_info\")}" }
|
||||||
table { class: "attr-table",
|
table { class: "attr-table",
|
||||||
tbody {
|
tbody {
|
||||||
tr {
|
tr {
|
||||||
th { "Growing Time" }
|
th { "{t(l, \"field.growing_time\")}" }
|
||||||
td { class: if growing_time == "\u{2014}" { "placeholder" } else { "" }, "{growing_time}" }
|
td { class: if growing_time == "\u{2014}" { "placeholder" } else { "" }, "{growing_time}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Planting Depth" }
|
th { "{t(l, \"field.planting_depth\")}" }
|
||||||
td { class: if planting_depth == "\u{2014}" { "placeholder" } else { "" }, "{planting_depth}" }
|
td { class: if planting_depth == "\u{2014}" { "placeholder" } else { "" }, "{planting_depth}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Row Spacing" }
|
th { "{t(l, \"field.row_spacing\")}" }
|
||||||
td { class: if row_spacing == "\u{2014}" { "placeholder" } else { "" }, "{row_spacing}" }
|
td { class: if row_spacing == "\u{2014}" { "placeholder" } else { "" }, "{row_spacing}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Plant Spacing" }
|
th { "{t(l, \"field.plant_spacing\")}" }
|
||||||
td { class: if plant_spacing == "\u{2014}" { "placeholder" } else { "" }, "{plant_spacing}" }
|
td { class: if plant_spacing == "\u{2014}" { "placeholder" } else { "" }, "{plant_spacing}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Propagation Methods" }
|
th { "{t(l, \"field.propagation\")}" }
|
||||||
td { class: if propagation == "\u{2014}" { "placeholder" } else { "" }, "{propagation}" }
|
td { class: if propagation == "\u{2014}" { "placeholder" } else { "" }, "{propagation}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -712,19 +713,19 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
|
|
||||||
// Card 5: Germination
|
// Card 5: Germination
|
||||||
div { class: "detail-card",
|
div { class: "detail-card",
|
||||||
div { class: "detail-card-header", "Germination" }
|
div { class: "detail-card-header", "{t(l, \"card.germination\")}" }
|
||||||
table { class: "attr-table",
|
table { class: "attr-table",
|
||||||
tbody {
|
tbody {
|
||||||
tr {
|
tr {
|
||||||
th { "Days to Germination" }
|
th { "{t(l, \"field.days_to_germ\")}" }
|
||||||
td { class: if days_germ == "\u{2014}" { "placeholder" } else { "" }, "{days_germ}" }
|
td { class: if days_germ == "\u{2014}" { "placeholder" } else { "" }, "{days_germ}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Germination Temp" }
|
th { "{t(l, \"field.germ_temp\")}" }
|
||||||
td { class: if germ_temp == "\u{2014}" { "placeholder" } else { "" }, "{germ_temp}" }
|
td { class: if germ_temp == "\u{2014}" { "placeholder" } else { "" }, "{germ_temp}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Light Requirement" }
|
th { "{t(l, \"field.light_req\")}" }
|
||||||
if light_req != "\u{2014}" {
|
if light_req != "\u{2014}" {
|
||||||
td { "{light_req}" }
|
td { "{light_req}" }
|
||||||
} else if let Some(ref fb) = sp_light_fb {
|
} else if let Some(ref fb) = sp_light_fb {
|
||||||
@@ -737,15 +738,15 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Stratification Required" }
|
th { "{t(l, \"field.stratification\")}" }
|
||||||
td { class: if strat_req == "\u{2014}" { "placeholder" } else { "" }, "{strat_req}" }
|
td { class: if strat_req == "\u{2014}" { "placeholder" } else { "" }, "{strat_req}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Stratification Days" }
|
th { "{t(l, \"field.stratification_days\")}" }
|
||||||
td { class: if strat_days == "\u{2014}" { "placeholder" } else { "" }, "{strat_days}" }
|
td { class: if strat_days == "\u{2014}" { "placeholder" } else { "" }, "{strat_days}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Scarification Required" }
|
th { "{t(l, \"field.scarification\")}" }
|
||||||
td { class: if scar_req == "\u{2014}" { "placeholder" } else { "" }, "{scar_req}" }
|
td { class: if scar_req == "\u{2014}" { "placeholder" } else { "" }, "{scar_req}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -754,23 +755,23 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
|
|
||||||
// Card 6: Climate & Environment
|
// Card 6: Climate & Environment
|
||||||
div { class: "detail-card",
|
div { class: "detail-card",
|
||||||
div { class: "detail-card-header", "Climate & Environment" }
|
div { class: "detail-card-header", "{t(l, \"card.climate\")}" }
|
||||||
table { class: "attr-table",
|
table { class: "attr-table",
|
||||||
tbody {
|
tbody {
|
||||||
tr {
|
tr {
|
||||||
th { "Min Temp" }
|
th { "{t(l, \"field.min_temp\")}" }
|
||||||
td { class: if min_temp == "\u{2014}" { "placeholder" } else { "" }, "{min_temp}" }
|
td { class: if min_temp == "\u{2014}" { "placeholder" } else { "" }, "{min_temp}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Max Temp" }
|
th { "{t(l, \"field.max_temp\")}" }
|
||||||
td { class: if max_temp == "\u{2014}" { "placeholder" } else { "" }, "{max_temp}" }
|
td { class: if max_temp == "\u{2014}" { "placeholder" } else { "" }, "{max_temp}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Humidity" }
|
th { "{t(l, \"field.humidity\")}" }
|
||||||
td { class: if humidity == "\u{2014}" { "placeholder" } else { "" }, "{humidity}" }
|
td { class: if humidity == "\u{2014}" { "placeholder" } else { "" }, "{humidity}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Light" }
|
th { "{t(l, \"field.light\")}" }
|
||||||
if light != "\u{2014}" {
|
if light != "\u{2014}" {
|
||||||
td { "{light}" }
|
td { "{light}" }
|
||||||
} else if let Some(ref fb) = sp_light_fb {
|
} else if let Some(ref fb) = sp_light_fb {
|
||||||
@@ -783,7 +784,7 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Frost Tolerance" }
|
th { "{t(l, \"field.frost_tolerance\")}" }
|
||||||
if frost_tol != "\u{2014}" {
|
if frost_tol != "\u{2014}" {
|
||||||
td { "{frost_tol}" }
|
td { "{frost_tol}" }
|
||||||
} else if let Some(ref fb) = sp_frost_fb {
|
} else if let Some(ref fb) = sp_frost_fb {
|
||||||
@@ -796,16 +797,16 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Min Light Hours/Day" }
|
th { "{t(l, \"field.min_light_hours\")}" }
|
||||||
td { class: if min_light_h == "\u{2014}" { "placeholder" } else { "" }, "{min_light_h}" }
|
td { class: if min_light_h == "\u{2014}" { "placeholder" } else { "" }, "{min_light_h}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Optimal Light Hours/Day" }
|
th { "{t(l, \"field.opt_light_hours\")}" }
|
||||||
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 {
|
if let Some(ref fb) = sp_drought_fb {
|
||||||
tr {
|
tr {
|
||||||
th { "Drought Tolerance" }
|
th { "{t(l, \"field.drought_tolerance\")}" }
|
||||||
td {
|
td {
|
||||||
span { class: "estimated-value", "{fb}" }
|
span { class: "estimated-value", "{fb}" }
|
||||||
span { class: "estimated-tag", "~ species" }
|
span { class: "estimated-tag", "~ species" }
|
||||||
@@ -814,7 +815,7 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
}
|
}
|
||||||
if let Some(ref fb) = sp_usda_fb {
|
if let Some(ref fb) = sp_usda_fb {
|
||||||
tr {
|
tr {
|
||||||
th { "USDA Hardiness Zone" }
|
th { "{t(l, \"field.usda_hardiness_zone\")}" }
|
||||||
td {
|
td {
|
||||||
span { class: "estimated-value", "{fb}" }
|
span { class: "estimated-value", "{fb}" }
|
||||||
span { class: "estimated-tag", "~ species" }
|
span { class: "estimated-tag", "~ species" }
|
||||||
@@ -827,7 +828,7 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
|
|
||||||
// Card 7: Species Information
|
// Card 7: Species Information
|
||||||
div { class: "detail-card",
|
div { class: "detail-card",
|
||||||
div { class: "detail-card-header", "Species Information" }
|
div { class: "detail-card-header", "{t(l, \"card.species_info\")}" }
|
||||||
{
|
{
|
||||||
let sp_read = species_data.read();
|
let sp_read = species_data.read();
|
||||||
match &*sp_read {
|
match &*sp_read {
|
||||||
@@ -841,8 +842,8 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
let sp_layer = opt_str(&sp.plant_layer);
|
let sp_layer = opt_str(&sp.plant_layer);
|
||||||
let sp_drought = opt_str(&sp.drought_tolerance);
|
let sp_drought = opt_str(&sp.drought_tolerance);
|
||||||
let sp_usda = opt_str(&sp.hardiness_zone_usda);
|
let sp_usda = opt_str(&sp.hardiness_zone_usda);
|
||||||
let sp_nfix = opt_bool(sp.nitrogen_fixer);
|
let sp_nfix = opt_bool_t(sp.nitrogen_fixer, l);
|
||||||
let sp_dynacc = opt_bool(sp.dynamic_accumulator);
|
let sp_dynacc = opt_bool_t(sp.dynamic_accumulator, l);
|
||||||
let sp_food = pick_desc(¤t_lang, &sp.food_uses_de, &sp.food_uses_en, &sp.food_uses);
|
let sp_food = pick_desc(¤t_lang, &sp.food_uses_de, &sp.food_uses_en, &sp.food_uses);
|
||||||
let sp_med = pick_desc(¤t_lang, &sp.medicinal_uses_de, &sp.medicinal_uses_en, &sp.medicinal_uses);
|
let sp_med = pick_desc(¤t_lang, &sp.medicinal_uses_de, &sp.medicinal_uses_en, &sp.medicinal_uses);
|
||||||
let sp_other = pick_desc(¤t_lang, &sp.other_uses_de, &sp.other_uses_en, &sp.other_uses);
|
let sp_other = pick_desc(¤t_lang, &sp.other_uses_de, &sp.other_uses_en, &sp.other_uses);
|
||||||
@@ -852,47 +853,47 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
table { class: "attr-table",
|
table { class: "attr-table",
|
||||||
tbody {
|
tbody {
|
||||||
tr {
|
tr {
|
||||||
th { "Plant Layer" }
|
th { "{t(l, \"field.plant_layer\")}" }
|
||||||
td { class: if sp_layer == "\u{2014}" { "placeholder" } else { "" }, "{sp_layer}" }
|
td { class: if sp_layer == "\u{2014}" { "placeholder" } else { "" }, "{sp_layer}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Drought Tolerance" }
|
th { "{t(l, \"field.drought_tolerance\")}" }
|
||||||
td { class: if sp_drought == "\u{2014}" { "placeholder" } else { "" }, "{sp_drought}" }
|
td { class: if sp_drought == "\u{2014}" { "placeholder" } else { "" }, "{sp_drought}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "USDA Zone" }
|
th { "{t(l, \"field.usda_zone\")}" }
|
||||||
td { class: if sp_usda == "\u{2014}" { "placeholder" } else { "" }, "{sp_usda}" }
|
td { class: if sp_usda == "\u{2014}" { "placeholder" } else { "" }, "{sp_usda}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "pH Range" }
|
th { "{t(l, \"field.ph_range\")}" }
|
||||||
td { class: if ph_range == "\u{2014}" { "placeholder" } else { "" }, "{ph_range}" }
|
td { class: if ph_range == "\u{2014}" { "placeholder" } else { "" }, "{ph_range}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Nitrogen Fixer" }
|
th { "{t(l, \"field.nitrogen_fixer\")}" }
|
||||||
td { class: if sp_nfix == "\u{2014}" { "placeholder" } else { "" }, "{sp_nfix}" }
|
td { class: if sp_nfix == "\u{2014}" { "placeholder" } else { "" }, "{sp_nfix}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Dynamic Accumulator" }
|
th { "{t(l, \"field.dynamic_acc\")}" }
|
||||||
td { class: if sp_dynacc == "\u{2014}" { "placeholder" } else { "" }, "{sp_dynacc}" }
|
td { class: if sp_dynacc == "\u{2014}" { "placeholder" } else { "" }, "{sp_dynacc}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Food Uses" }
|
th { "{t(l, \"field.food_uses\")}" }
|
||||||
td { class: if sp_food == "\u{2014}" { "placeholder" } else { "" }, "{sp_food}" }
|
td { class: if sp_food == "\u{2014}" { "placeholder" } else { "" }, "{sp_food}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Medicinal Uses" }
|
th { "{t(l, \"field.medicinal_uses\")}" }
|
||||||
td { class: if sp_med == "\u{2014}" { "placeholder" } else { "" }, "{sp_med}" }
|
td { class: if sp_med == "\u{2014}" { "placeholder" } else { "" }, "{sp_med}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Other Uses" }
|
th { "{t(l, \"field.other_uses\")}" }
|
||||||
td { class: if sp_other == "\u{2014}" { "placeholder" } else { "" }, "{sp_other}" }
|
td { class: if sp_other == "\u{2014}" { "placeholder" } else { "" }, "{sp_other}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Wildlife Value" }
|
th { "{t(l, \"field.wildlife_value\")}" }
|
||||||
td { class: if sp_wildlife == "\u{2014}" { "placeholder" } else { "" }, "{sp_wildlife}" }
|
td { class: if sp_wildlife == "\u{2014}" { "placeholder" } else { "" }, "{sp_wildlife}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Native Range" }
|
th { "{t(l, \"field.native_range\")}" }
|
||||||
td { class: if sp_native == "\u{2014}" { "placeholder" } else { "" }, "{sp_native}" }
|
td { class: if sp_native == "\u{2014}" { "placeholder" } else { "" }, "{sp_native}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -900,7 +901,7 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
_ => rsx! {
|
_ => rsx! {
|
||||||
p { class: "placeholder detail-card-empty", "Loading species data\u{2026}" }
|
p { class: "placeholder detail-card-empty", "{t(l, \"loading_species_data\")}" }
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -910,9 +911,7 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
|
|
||||||
if has_any_fallback {
|
if has_any_fallback {
|
||||||
p { class: "species-fallback-note",
|
p { class: "species-fallback-note",
|
||||||
"Values marked "
|
"{t(l, \"estimated_note\")}"
|
||||||
span { class: "estimated-tag", "~ species" }
|
|
||||||
" are estimated from species-level data."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use dioxus::prelude::*;
|
|||||||
use crate::api;
|
use crate::api;
|
||||||
use crate::app::{Lang, Route};
|
use crate::app::{Lang, Route};
|
||||||
use crate::components::table_controls::*;
|
use crate::components::table_controls::*;
|
||||||
use crate::i18n::pick_name;
|
use crate::i18n::{pick_name, t};
|
||||||
|
|
||||||
const STORAGE_KEY_COLS: &str = "herbapi_families_cols";
|
const STORAGE_KEY_COLS: &str = "herbapi_families_cols";
|
||||||
const STORAGE_KEY_PP: &str = "herbapi_families_pp";
|
const STORAGE_KEY_PP: &str = "herbapi_families_pp";
|
||||||
@@ -38,16 +38,17 @@ pub fn FamilyList() -> Element {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let current_page = *page.read();
|
let current_page = *page.read();
|
||||||
|
let l = lang.read().clone();
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "page",
|
div { class: "page",
|
||||||
h1 { "Plant Families" }
|
h1 { "{t(&l, \"page.families\")}" }
|
||||||
|
|
||||||
div { class: "table-toolbar",
|
div { class: "table-toolbar",
|
||||||
div { class: "search-bar",
|
div { class: "search-bar",
|
||||||
input {
|
input {
|
||||||
r#type: "text",
|
r#type: "text",
|
||||||
placeholder: "Search families...",
|
placeholder: "{t(&l, \"search.placeholder_families\")}",
|
||||||
value: "{search}",
|
value: "{search}",
|
||||||
oninput: move |e| {
|
oninput: move |e| {
|
||||||
search.set(e.value());
|
search.set(e.value());
|
||||||
@@ -69,30 +70,30 @@ pub fn FamilyList() -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match &*families.read() {
|
match &*families.read() {
|
||||||
None => rsx! { p { "Loading..." } },
|
None => rsx! { p { "{t(&l, \"loading\")}" } },
|
||||||
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
|
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
|
||||||
Some(Ok(data)) => {
|
Some(Ok(data)) => {
|
||||||
let vis = visible_cols.read();
|
let vis = visible_cols.read();
|
||||||
rsx! {
|
rsx! {
|
||||||
p { class: "result-count", "{data.total} families" }
|
p { class: "result-count", "{data.total} {t(&l, \"nav.families\").to_lowercase()}" }
|
||||||
div { class: "table-wrap",
|
div { class: "table-wrap",
|
||||||
table {
|
table {
|
||||||
thead {
|
thead {
|
||||||
tr {
|
tr {
|
||||||
if is_col_visible(&vis, "name_scientific") {
|
if is_col_visible(&vis, "name_scientific") {
|
||||||
th { "Scientific Name" }
|
th { "{t(&l, \"field.scientific_name\")}" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "common_name") {
|
if is_col_visible(&vis, "common_name") {
|
||||||
th { "Common Name" }
|
th { "{t(&l, \"field.common_name\")}" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "name_en") {
|
if is_col_visible(&vis, "name_en") {
|
||||||
th { "English" }
|
th { "{t(&l, \"field.name_en\")}" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "name_de") {
|
if is_col_visible(&vis, "name_de") {
|
||||||
th { "German" }
|
th { "{t(&l, \"field.name_de\")}" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "description") {
|
if is_col_visible(&vis, "description") {
|
||||||
th { "Description" }
|
th { "{t(&l, \"field.description\")}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,13 +136,13 @@ pub fn FamilyList() -> Element {
|
|||||||
button {
|
button {
|
||||||
disabled: current_page <= 1,
|
disabled: current_page <= 1,
|
||||||
onclick: move |_| page.set(current_page - 1),
|
onclick: move |_| page.set(current_page - 1),
|
||||||
"Previous"
|
"{t(&l, \"btn.previous\")}"
|
||||||
}
|
}
|
||||||
span { "Page {current_page} of {(data.total + data.per_page - 1) / data.per_page}" }
|
span { "{t(&l, \"page\")} {current_page} {t(&l, \"of\")} {(data.total + data.per_page - 1) / data.per_page}" }
|
||||||
button {
|
button {
|
||||||
disabled: current_page * data.per_page >= data.total,
|
disabled: current_page * data.per_page >= data.total,
|
||||||
onclick: move |_| page.set(current_page + 1),
|
onclick: move |_| page.set(current_page + 1),
|
||||||
"Next"
|
"{t(&l, \"btn.next\")}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,10 +168,12 @@ pub fn FamilyDetail(slug: String) -> Element {
|
|||||||
async move { api::list_species(1, 100, Some(&s), None).await }
|
async move { api::list_species(1, 100, Some(&s), None).await }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let l = lang.read().clone();
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "page",
|
div { class: "page",
|
||||||
match &*family.read() {
|
match &*family.read() {
|
||||||
None => rsx! { p { "Loading..." } },
|
None => rsx! { p { "{t(&l, \"loading\")}" } },
|
||||||
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
|
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
|
||||||
Some(Ok(f)) => {
|
Some(Ok(f)) => {
|
||||||
let common = pick_name(&lang.read(), &f.name_de, &f.name_en, &f.name_scientific);
|
let common = pick_name(&lang.read(), &f.name_de, &f.name_en, &f.name_scientific);
|
||||||
@@ -186,9 +189,9 @@ pub fn FamilyDetail(slug: String) -> Element {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 { "Species in this Family" }
|
h2 { "{t(&l, \"page.species_in_family\")}" }
|
||||||
match &*species.read() {
|
match &*species.read() {
|
||||||
None => rsx! { p { "Loading..." } },
|
None => rsx! { p { "{t(&l, \"loading\")}" } },
|
||||||
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
|
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
|
||||||
Some(Ok(data)) => rsx! {
|
Some(Ok(data)) => rsx! {
|
||||||
div { class: "card-grid",
|
div { class: "card-grid",
|
||||||
|
|||||||
@@ -3,23 +3,24 @@ use dioxus::prelude::*;
|
|||||||
use crate::api;
|
use crate::api;
|
||||||
use crate::app::{Lang, Route};
|
use crate::app::{Lang, Route};
|
||||||
use crate::components::plant_card::PlantCard;
|
use crate::components::plant_card::PlantCard;
|
||||||
use crate::i18n::pick_name;
|
use crate::i18n::{pick_name, t};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Home() -> Element {
|
pub fn Home() -> Element {
|
||||||
let lang = use_context::<Lang>().0;
|
let lang = use_context::<Lang>().0;
|
||||||
let mut search_query = use_signal(|| String::new());
|
let mut search_query = use_signal(|| String::new());
|
||||||
let species = use_resource(|| async { api::list_species(1, 12, None, None).await });
|
let species = use_resource(|| async { api::list_species(1, 12, None, None).await });
|
||||||
|
let l = lang.read().clone();
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "page home-page",
|
div { class: "page home-page",
|
||||||
h1 { "HerbAPI" }
|
h1 { "HerbAPI" }
|
||||||
p { class: "subtitle", "Trilingual plant reference database" }
|
p { class: "subtitle", "{t(&l, \"subtitle\")}" }
|
||||||
|
|
||||||
div { class: "search-bar",
|
div { class: "search-bar",
|
||||||
input {
|
input {
|
||||||
r#type: "text",
|
r#type: "text",
|
||||||
placeholder: "Search plants...",
|
placeholder: "{t(&l, \"search.placeholder\")}",
|
||||||
value: "{search_query}",
|
value: "{search_query}",
|
||||||
oninput: move |e| search_query.set(e.value()),
|
oninput: move |e| search_query.set(e.value()),
|
||||||
onkeydown: move |e| {
|
onkeydown: move |e| {
|
||||||
@@ -31,9 +32,9 @@ pub fn Home() -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 { "Recent Species" }
|
h2 { "{t(&l, \"page.recent_species\")}" }
|
||||||
match &*species.read() {
|
match &*species.read() {
|
||||||
None => rsx! { p { "Loading..." } },
|
None => rsx! { p { "{t(&l, \"loading\")}" } },
|
||||||
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
|
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
|
||||||
Some(Ok(data)) => rsx! {
|
Some(Ok(data)) => rsx! {
|
||||||
div { class: "card-grid",
|
div { class: "card-grid",
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
use crate::api;
|
use crate::api;
|
||||||
use crate::app::Route;
|
use crate::app::{Lang, Route};
|
||||||
|
use crate::i18n::t;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn SearchPage() -> Element {
|
pub fn SearchPage() -> Element {
|
||||||
|
let lang = use_context::<Lang>().0;
|
||||||
let mut query = use_signal(|| String::new());
|
let mut query = use_signal(|| String::new());
|
||||||
let mut results = use_signal(|| None::<Result<Vec<crate::types::SearchResult>, String>>);
|
let mut results = use_signal(|| None::<Result<Vec<crate::types::SearchResult>, String>>);
|
||||||
|
|
||||||
@@ -18,14 +20,16 @@ pub fn SearchPage() -> Element {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let l = lang.read().clone();
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "page search-page",
|
div { class: "page search-page",
|
||||||
h1 { "Search" }
|
h1 { "{t(&l, \"page.search\")}" }
|
||||||
|
|
||||||
div { class: "search-bar",
|
div { class: "search-bar",
|
||||||
input {
|
input {
|
||||||
r#type: "text",
|
r#type: "text",
|
||||||
placeholder: "Search plants, families, cultivars...",
|
placeholder: "{t(&l, \"search.placeholder_full\")}",
|
||||||
value: "{query}",
|
value: "{query}",
|
||||||
oninput: move |e| query.set(e.value()),
|
oninput: move |e| query.set(e.value()),
|
||||||
onkeydown: move |e| {
|
onkeydown: move |e| {
|
||||||
@@ -34,14 +38,14 @@ pub fn SearchPage() -> Element {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
button { onclick: move |_| trigger_search(), "Search" }
|
button { onclick: move |_| trigger_search(), "{t(&l, \"btn.search\")}" }
|
||||||
}
|
}
|
||||||
|
|
||||||
match &*results.read() {
|
match &*results.read() {
|
||||||
None => rsx! {},
|
None => rsx! {},
|
||||||
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
|
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
|
||||||
Some(Ok(data)) => rsx! {
|
Some(Ok(data)) => rsx! {
|
||||||
p { class: "result-count", "{data.len()} results" }
|
p { class: "result-count", "{data.len()} {t(&l, \"results\")}" }
|
||||||
div { class: "search-results",
|
div { class: "search-results",
|
||||||
for r in data.iter() {
|
for r in data.iter() {
|
||||||
div { class: "search-result",
|
div { class: "search-result",
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
use crate::app::Lang;
|
||||||
|
use crate::i18n::t;
|
||||||
|
|
||||||
struct DataSource {
|
struct DataSource {
|
||||||
name: &'static str,
|
name: &'static str,
|
||||||
url: &'static str,
|
url: &'static str,
|
||||||
@@ -83,11 +86,14 @@ const SOURCES: &[DataSource] = &[
|
|||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Sources() -> Element {
|
pub fn Sources() -> Element {
|
||||||
|
let lang = use_context::<Lang>().0;
|
||||||
|
let l = lang.read().clone();
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "page sources-page",
|
div { class: "page sources-page",
|
||||||
h1 { "Data Sources" }
|
h1 { "{t(&l, \"page.sources\")}" }
|
||||||
p { class: "sources-intro",
|
p { class: "sources-intro",
|
||||||
"HerbAPI aggregates plant data from multiple open sources. We are grateful to these projects for making botanical knowledge freely available."
|
"{t(&l, \"sources.intro\")}"
|
||||||
}
|
}
|
||||||
div { class: "sources-grid",
|
div { class: "sources-grid",
|
||||||
for source in SOURCES.iter() {
|
for source in SOURCES.iter() {
|
||||||
@@ -105,11 +111,11 @@ pub fn Sources() -> Element {
|
|||||||
p { class: "source-description", "{source.description}" }
|
p { class: "source-description", "{source.description}" }
|
||||||
div { class: "source-details",
|
div { class: "source-details",
|
||||||
div { class: "source-detail",
|
div { class: "source-detail",
|
||||||
span { class: "source-detail-label", "Data used" }
|
span { class: "source-detail-label", "{t(&l, \"field.data_used\")}" }
|
||||||
span { class: "source-detail-value", "{source.data_used}" }
|
span { class: "source-detail-value", "{source.data_used}" }
|
||||||
}
|
}
|
||||||
div { class: "source-detail",
|
div { class: "source-detail",
|
||||||
span { class: "source-detail-label", "License" }
|
span { class: "source-detail-label", "{t(&l, \"field.license\")}" }
|
||||||
span { class: "source-detail-value source-license", "{source.license}" }
|
span { class: "source-detail-value source-license", "{source.license}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+105
-101
@@ -7,7 +7,7 @@ use crate::api;
|
|||||||
use crate::app::{Lang, Route};
|
use crate::app::{Lang, Route};
|
||||||
use crate::components::plant_card::PlantCard;
|
use crate::components::plant_card::PlantCard;
|
||||||
use crate::components::table_controls::*;
|
use crate::components::table_controls::*;
|
||||||
use crate::i18n::{pick_desc, pick_name};
|
use crate::i18n::{pick_desc, pick_name, t};
|
||||||
|
|
||||||
const STORAGE_KEY_COLS: &str = "herbapi_species_cols";
|
const STORAGE_KEY_COLS: &str = "herbapi_species_cols";
|
||||||
const STORAGE_KEY_PP: &str = "herbapi_species_pp";
|
const STORAGE_KEY_PP: &str = "herbapi_species_pp";
|
||||||
@@ -93,16 +93,17 @@ pub fn SpeciesList() -> Element {
|
|||||||
|
|
||||||
let current_page = *page.read();
|
let current_page = *page.read();
|
||||||
let is_table = *table_view.read();
|
let is_table = *table_view.read();
|
||||||
|
let l = lang.read().clone();
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "page",
|
div { class: "page",
|
||||||
h1 { "Species" }
|
h1 { "{t(&l, \"page.species\")}" }
|
||||||
|
|
||||||
div { class: "table-toolbar",
|
div { class: "table-toolbar",
|
||||||
div { class: "search-bar",
|
div { class: "search-bar",
|
||||||
input {
|
input {
|
||||||
r#type: "text",
|
r#type: "text",
|
||||||
placeholder: "Search species...",
|
placeholder: "{t(&l, \"search.placeholder_species\")}",
|
||||||
value: "{search}",
|
value: "{search}",
|
||||||
oninput: move |e| {
|
oninput: move |e| {
|
||||||
search.set(e.value());
|
search.set(e.value());
|
||||||
@@ -118,7 +119,7 @@ pub fn SpeciesList() -> Element {
|
|||||||
table_view.set(new_val);
|
table_view.set(new_val);
|
||||||
let _ = LocalStorage::set(STORAGE_KEY_VIEW, new_val);
|
let _ = LocalStorage::set(STORAGE_KEY_VIEW, new_val);
|
||||||
},
|
},
|
||||||
if is_table { "Card View" } else { "Table View" }
|
if is_table { "{t(&l, \"btn.card_view\")}" } else { "{t(&l, \"btn.table_view\")}" }
|
||||||
}
|
}
|
||||||
PerPageSelector {
|
PerPageSelector {
|
||||||
per_page: per_page,
|
per_page: per_page,
|
||||||
@@ -131,14 +132,14 @@ pub fn SpeciesList() -> Element {
|
|||||||
// Filter bar
|
// Filter bar
|
||||||
div { class: "filter-bar",
|
div { class: "filter-bar",
|
||||||
div { class: "filter-group",
|
div { class: "filter-group",
|
||||||
label { "Layer" }
|
label { "{t(&l, \"filter.layer\")}" }
|
||||||
select {
|
select {
|
||||||
value: "{filter_layer}",
|
value: "{filter_layer}",
|
||||||
onchange: move |e| {
|
onchange: move |e| {
|
||||||
filter_layer.set(e.value());
|
filter_layer.set(e.value());
|
||||||
page.set(1);
|
page.set(1);
|
||||||
},
|
},
|
||||||
option { value: "", "All" }
|
option { value: "", "{t(&l, \"filter.all\")}" }
|
||||||
option { value: "canopy", "Canopy" }
|
option { value: "canopy", "Canopy" }
|
||||||
option { value: "understory", "Understory" }
|
option { value: "understory", "Understory" }
|
||||||
option { value: "shrub", "Shrub" }
|
option { value: "shrub", "Shrub" }
|
||||||
@@ -149,14 +150,14 @@ pub fn SpeciesList() -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
div { class: "filter-group",
|
div { class: "filter-group",
|
||||||
label { "Drought Tol." }
|
label { "{t(&l, \"filter.drought_tol\")}" }
|
||||||
select {
|
select {
|
||||||
value: "{filter_drought}",
|
value: "{filter_drought}",
|
||||||
onchange: move |e| {
|
onchange: move |e| {
|
||||||
filter_drought.set(e.value());
|
filter_drought.set(e.value());
|
||||||
page.set(1);
|
page.set(1);
|
||||||
},
|
},
|
||||||
option { value: "", "All" }
|
option { value: "", "{t(&l, \"filter.all\")}" }
|
||||||
option { value: "none", "None" }
|
option { value: "none", "None" }
|
||||||
option { value: "low", "Low" }
|
option { value: "low", "Low" }
|
||||||
option { value: "moderate", "Moderate" }
|
option { value: "moderate", "Moderate" }
|
||||||
@@ -174,7 +175,7 @@ pub fn SpeciesList() -> Element {
|
|||||||
page.set(1);
|
page.set(1);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
"N-Fixer"
|
"{t(&l, \"field.n_fixer\")}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div { class: "filter-group filter-checkbox",
|
div { class: "filter-group filter-checkbox",
|
||||||
@@ -187,7 +188,7 @@ pub fn SpeciesList() -> Element {
|
|||||||
page.set(1);
|
page.set(1);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
"Dyn. Accumulator"
|
"{t(&l, \"field.dyn_accum\")}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,7 +202,7 @@ pub fn SpeciesList() -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match &*species.read() {
|
match &*species.read() {
|
||||||
None => rsx! { p { "Loading..." } },
|
None => rsx! { p { "{t(&l, \"loading\")}" } },
|
||||||
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
|
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
|
||||||
Some(Ok(data)) => {
|
Some(Ok(data)) => {
|
||||||
let fmap_read = family_map.read();
|
let fmap_read = family_map.read();
|
||||||
@@ -212,7 +213,7 @@ pub fn SpeciesList() -> Element {
|
|||||||
};
|
};
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
p { class: "result-count", "{data.total} species" }
|
p { class: "result-count", "{data.total} {t(&l, \"nav.species\").to_lowercase()}" }
|
||||||
|
|
||||||
if is_table {
|
if is_table {
|
||||||
{
|
{
|
||||||
@@ -223,49 +224,49 @@ pub fn SpeciesList() -> Element {
|
|||||||
thead {
|
thead {
|
||||||
tr {
|
tr {
|
||||||
if is_col_visible(&vis, "name_scientific") {
|
if is_col_visible(&vis, "name_scientific") {
|
||||||
th { "Scientific Name" }
|
th { "{t(&l, \"field.scientific_name\")}" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "common_name") {
|
if is_col_visible(&vis, "common_name") {
|
||||||
th { "Common Name" }
|
th { "{t(&l, \"field.common_name\")}" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "name_de") {
|
if is_col_visible(&vis, "name_de") {
|
||||||
th { "German" }
|
th { "{t(&l, \"field.name_de\")}" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "name_en") {
|
if is_col_visible(&vis, "name_en") {
|
||||||
th { "English" }
|
th { "{t(&l, \"field.name_en\")}" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "family") {
|
if is_col_visible(&vis, "family") {
|
||||||
th { "Family" }
|
th { "{t(&l, \"field.family\")}" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "plant_layer") {
|
if is_col_visible(&vis, "plant_layer") {
|
||||||
th { "Layer" }
|
th { "{t(&l, \"field.layer\")}" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "nitrogen_fixer") {
|
if is_col_visible(&vis, "nitrogen_fixer") {
|
||||||
th { "N-Fixer" }
|
th { "{t(&l, \"field.n_fixer\")}" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "dynamic_accumulator") {
|
if is_col_visible(&vis, "dynamic_accumulator") {
|
||||||
th { "Dyn. Accum." }
|
th { "{t(&l, \"field.dyn_accum\")}" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "food_uses") {
|
if is_col_visible(&vis, "food_uses") {
|
||||||
th { "Food Uses" }
|
th { "{t(&l, \"field.food_uses\")}" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "edibility_rating") {
|
if is_col_visible(&vis, "edibility_rating") {
|
||||||
th { "Edibility" }
|
th { "{t(&l, \"field.edibility\")}" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "drought_tolerance") {
|
if is_col_visible(&vis, "drought_tolerance") {
|
||||||
th { "Drought Tol." }
|
th { "{t(&l, \"field.drought_tol\")}" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "hardiness_zone_usda") {
|
if is_col_visible(&vis, "hardiness_zone_usda") {
|
||||||
th { "USDA Zone" }
|
th { "{t(&l, \"field.usda_zone\")}" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "nectar_value") {
|
if is_col_visible(&vis, "nectar_value") {
|
||||||
th { "Nectar" }
|
th { "{t(&l, \"field.nectar\")}" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "wild_bee_count") {
|
if is_col_visible(&vis, "wild_bee_count") {
|
||||||
th { "Wild Bees" }
|
th { "{t(&l, \"field.wild_bees\")}" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "native_status") {
|
if is_col_visible(&vis, "native_status") {
|
||||||
th { "Native Status" }
|
th { "{t(&l, \"field.native_status\")}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -301,7 +302,7 @@ pub fn SpeciesList() -> Element {
|
|||||||
if is_col_visible(&vis, "nitrogen_fixer") {
|
if is_col_visible(&vis, "nitrogen_fixer") {
|
||||||
td {
|
td {
|
||||||
match s.nitrogen_fixer {
|
match s.nitrogen_fixer {
|
||||||
Some(true) => rsx! { span { class: "badge organic", "Yes" } },
|
Some(true) => rsx! { span { class: "badge organic", "{t(&l, \"yes\")}" } },
|
||||||
Some(false) => rsx! { "-" },
|
Some(false) => rsx! { "-" },
|
||||||
None => rsx! { "-" },
|
None => rsx! { "-" },
|
||||||
}
|
}
|
||||||
@@ -310,7 +311,7 @@ pub fn SpeciesList() -> Element {
|
|||||||
if is_col_visible(&vis, "dynamic_accumulator") {
|
if is_col_visible(&vis, "dynamic_accumulator") {
|
||||||
td {
|
td {
|
||||||
match s.dynamic_accumulator {
|
match s.dynamic_accumulator {
|
||||||
Some(true) => rsx! { span { class: "badge organic", "Yes" } },
|
Some(true) => rsx! { span { class: "badge organic", "{t(&l, \"yes\")}" } },
|
||||||
Some(false) => rsx! { "-" },
|
Some(false) => rsx! { "-" },
|
||||||
None => rsx! { "-" },
|
None => rsx! { "-" },
|
||||||
}
|
}
|
||||||
@@ -401,13 +402,13 @@ pub fn SpeciesList() -> Element {
|
|||||||
button {
|
button {
|
||||||
disabled: current_page <= 1,
|
disabled: current_page <= 1,
|
||||||
onclick: move |_| page.set(current_page - 1),
|
onclick: move |_| page.set(current_page - 1),
|
||||||
"Previous"
|
"{t(&l, \"btn.previous\")}"
|
||||||
}
|
}
|
||||||
span { "Page {current_page} of {(data.total + data.per_page - 1) / data.per_page}" }
|
span { "{t(&l, \"page\")} {current_page} {t(&l, \"of\")} {(data.total + data.per_page - 1) / data.per_page}" }
|
||||||
button {
|
button {
|
||||||
disabled: current_page * data.per_page >= data.total,
|
disabled: current_page * data.per_page >= data.total,
|
||||||
onclick: move |_| page.set(current_page + 1),
|
onclick: move |_| page.set(current_page + 1),
|
||||||
"Next"
|
"{t(&l, \"btn.next\")}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -461,11 +462,12 @@ pub fn SpeciesDetail(slug: String) -> Element {
|
|||||||
rsx! {
|
rsx! {
|
||||||
div { class: "page species-detail",
|
div { class: "page species-detail",
|
||||||
match &*species.read() {
|
match &*species.read() {
|
||||||
None => rsx! { p { "Loading..." } },
|
None => rsx! { p { "{t(&lang.read(), \"loading\")}" } },
|
||||||
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
|
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
|
||||||
Some(Ok(s)) => {
|
Some(Ok(s)) => {
|
||||||
let species_slug = s.slug.clone();
|
let species_slug = s.slug.clone();
|
||||||
let current_lang = lang.read().clone();
|
let current_lang = lang.read().clone();
|
||||||
|
let l = &*current_lang;
|
||||||
|
|
||||||
// Helper closures to format fields
|
// Helper closures to format fields
|
||||||
let os = |v: &Option<String>| -> String {
|
let os = |v: &Option<String>| -> String {
|
||||||
@@ -476,8 +478,8 @@ pub fn SpeciesDetail(slug: String) -> Element {
|
|||||||
};
|
};
|
||||||
let ob = |v: Option<bool>| -> String {
|
let ob = |v: Option<bool>| -> String {
|
||||||
match v {
|
match v {
|
||||||
Some(true) => "Yes".to_string(),
|
Some(true) => t(l, "yes").to_string(),
|
||||||
Some(false) => "No".to_string(),
|
Some(false) => t(l, "no").to_string(),
|
||||||
None => "\u{2014}".to_string(),
|
None => "\u{2014}".to_string(),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -565,27 +567,27 @@ pub fn SpeciesDetail(slug: String) -> Element {
|
|||||||
|
|
||||||
// Card 1: Species Details
|
// Card 1: Species Details
|
||||||
div { class: "detail-card",
|
div { class: "detail-card",
|
||||||
div { class: "detail-card-header", "Species Details" }
|
div { class: "detail-card-header", "{t(l, \"card.species_details\")}" }
|
||||||
table { class: "attr-table",
|
table { class: "attr-table",
|
||||||
tbody {
|
tbody {
|
||||||
tr {
|
tr {
|
||||||
th { "Scientific Name" }
|
th { "{t(l, \"field.scientific_name\")}" }
|
||||||
td { em { "{s.name_scientific}" } }
|
td { em { "{s.name_scientific}" } }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Common Name" }
|
th { "{t(l, \"field.common_name\")}" }
|
||||||
td { "{common_name}" }
|
td { "{common_name}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Name EN" }
|
th { "{t(l, \"field.name_en\")}" }
|
||||||
td { class: if name_en == em { "placeholder" } else { "" }, "{name_en}" }
|
td { class: if name_en == em { "placeholder" } else { "" }, "{name_en}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Name DE" }
|
th { "{t(l, \"field.name_de\")}" }
|
||||||
td { class: if name_de == em { "placeholder" } else { "" }, "{name_de}" }
|
td { class: if name_de == em { "placeholder" } else { "" }, "{name_de}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Family" }
|
th { "{t(l, \"field.family\")}" }
|
||||||
td {
|
td {
|
||||||
if let Some(Some(ref fam)) = *family_data.read() {
|
if let Some(Some(ref fam)) = *family_data.read() {
|
||||||
Link { to: Route::FamilyDetail { slug: fam.slug.clone() },
|
Link { to: Route::FamilyDetail { slug: fam.slug.clone() },
|
||||||
@@ -597,7 +599,7 @@ pub fn SpeciesDetail(slug: String) -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Description" }
|
th { "{t(l, \"field.description\")}" }
|
||||||
td { class: if desc == em { "placeholder" } else { "" }, "{desc}" }
|
td { class: if desc == em { "placeholder" } else { "" }, "{desc}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -606,23 +608,23 @@ pub fn SpeciesDetail(slug: String) -> Element {
|
|||||||
|
|
||||||
// Card 2: Uses
|
// Card 2: Uses
|
||||||
div { class: "detail-card",
|
div { class: "detail-card",
|
||||||
div { class: "detail-card-header", "Uses" }
|
div { class: "detail-card-header", "{t(l, \"card.uses\")}" }
|
||||||
table { class: "attr-table",
|
table { class: "attr-table",
|
||||||
tbody {
|
tbody {
|
||||||
tr {
|
tr {
|
||||||
th { "Food Uses" }
|
th { "{t(l, \"field.food_uses\")}" }
|
||||||
td { class: if food == em { "placeholder" } else { "" }, "{food}" }
|
td { class: if food == em { "placeholder" } else { "" }, "{food}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Medicinal Uses" }
|
th { "{t(l, \"field.medicinal_uses\")}" }
|
||||||
td { class: if med == em { "placeholder" } else { "" }, "{med}" }
|
td { class: if med == em { "placeholder" } else { "" }, "{med}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Other Uses" }
|
th { "{t(l, \"field.other_uses\")}" }
|
||||||
td { class: if other == em { "placeholder" } else { "" }, "{other}" }
|
td { class: if other == em { "placeholder" } else { "" }, "{other}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Edibility Rating" }
|
th { "{t(l, \"field.edibility\")}" }
|
||||||
td { class: if edibility == em { "placeholder" } else { "" }, "{edibility}" }
|
td { class: if edibility == em { "placeholder" } else { "" }, "{edibility}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -631,27 +633,27 @@ pub fn SpeciesDetail(slug: String) -> Element {
|
|||||||
|
|
||||||
// Card 3: Ecology
|
// Card 3: Ecology
|
||||||
div { class: "detail-card",
|
div { class: "detail-card",
|
||||||
div { class: "detail-card-header", "Ecology" }
|
div { class: "detail-card-header", "{t(l, \"card.ecology\")}" }
|
||||||
table { class: "attr-table",
|
table { class: "attr-table",
|
||||||
tbody {
|
tbody {
|
||||||
tr {
|
tr {
|
||||||
th { "Plant Layer" }
|
th { "{t(l, \"field.plant_layer\")}" }
|
||||||
td { class: if layer == em { "placeholder" } else { "" }, "{layer}" }
|
td { class: if layer == em { "placeholder" } else { "" }, "{layer}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Succession Stage" }
|
th { "{t(l, \"field.succession\")}" }
|
||||||
td { class: if succession == em { "placeholder" } else { "" }, "{succession}" }
|
td { class: if succession == em { "placeholder" } else { "" }, "{succession}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Wildlife Value" }
|
th { "{t(l, \"field.wildlife_value\")}" }
|
||||||
td { class: if wildlife == em { "placeholder" } else { "" }, "{wildlife}" }
|
td { class: if wildlife == em { "placeholder" } else { "" }, "{wildlife}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Native Range" }
|
th { "{t(l, \"field.native_range\")}" }
|
||||||
td { class: if native == em { "placeholder" } else { "" }, "{native}" }
|
td { class: if native == em { "placeholder" } else { "" }, "{native}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Pollination Type" }
|
th { "{t(l, \"field.pollination\")}" }
|
||||||
td { class: if pollination == em { "placeholder" } else { "" }, "{pollination}" }
|
td { class: if pollination == em { "placeholder" } else { "" }, "{pollination}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -665,7 +667,7 @@ pub fn SpeciesDetail(slug: String) -> Element {
|
|||||||
// Image card
|
// Image card
|
||||||
if let Some(ref key) = img_key {
|
if let Some(ref key) = img_key {
|
||||||
div { class: "detail-card",
|
div { class: "detail-card",
|
||||||
div { class: "detail-card-header", "Image" }
|
div { class: "detail-card-header", "{t(l, \"card.image\")}" }
|
||||||
div { class: "image-card-body",
|
div { class: "image-card-body",
|
||||||
img { class: "species-image", src: "/img/{key}", alt: "{s.name_scientific}" }
|
img { class: "species-image", src: "/img/{key}", alt: "{s.name_scientific}" }
|
||||||
if let Some(ref img) = primary_img {
|
if let Some(ref img) = primary_img {
|
||||||
@@ -683,7 +685,7 @@ pub fn SpeciesDetail(slug: String) -> Element {
|
|||||||
if img.caption.is_some() || img.license.is_some() {
|
if img.caption.is_some() || img.license.is_some() {
|
||||||
" | "
|
" | "
|
||||||
}
|
}
|
||||||
a { href: "{url}", target: "_blank", class: "attribution-link", "Source" }
|
a { href: "{url}", target: "_blank", class: "attribution-link", "{t(l, \"field.source\")}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -693,31 +695,31 @@ pub fn SpeciesDetail(slug: String) -> Element {
|
|||||||
|
|
||||||
// Card 4: Growing Requirements
|
// Card 4: Growing Requirements
|
||||||
div { class: "detail-card",
|
div { class: "detail-card",
|
||||||
div { class: "detail-card-header", "Growing Requirements" }
|
div { class: "detail-card-header", "{t(l, \"card.growing_requirements\")}" }
|
||||||
table { class: "attr-table",
|
table { class: "attr-table",
|
||||||
tbody {
|
tbody {
|
||||||
tr {
|
tr {
|
||||||
th { "Soil Moisture" }
|
th { "{t(l, \"field.soil_moisture\")}" }
|
||||||
td { class: if soil_moist == em { "placeholder" } else { "" }, "{soil_moist}" }
|
td { class: if soil_moist == em { "placeholder" } else { "" }, "{soil_moist}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "pH Range" }
|
th { "{t(l, \"field.ph_range\")}" }
|
||||||
td { class: if ph_range == em { "placeholder" } else { "" }, "{ph_range}" }
|
td { class: if ph_range == em { "placeholder" } else { "" }, "{ph_range}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Drought Tolerance" }
|
th { "{t(l, \"field.drought_tolerance\")}" }
|
||||||
td { class: if drought == em { "placeholder" } else { "" }, "{drought}" }
|
td { class: if drought == em { "placeholder" } else { "" }, "{drought}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Salt Tolerance" }
|
th { "{t(l, \"field.salt_tolerance\")}" }
|
||||||
td { class: if salt == em { "placeholder" } else { "" }, "{salt}" }
|
td { class: if salt == em { "placeholder" } else { "" }, "{salt}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "USDA Zone" }
|
th { "{t(l, \"field.usda_zone\")}" }
|
||||||
td { class: if usda == em { "placeholder" } else { "" }, "{usda}" }
|
td { class: if usda == em { "placeholder" } else { "" }, "{usda}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "AT Zone" }
|
th { "{t(l, \"field.at_zone\")}" }
|
||||||
td { class: if at_zone == em { "placeholder" } else { "" }, "{at_zone}" }
|
td { class: if at_zone == em { "placeholder" } else { "" }, "{at_zone}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -726,39 +728,39 @@ pub fn SpeciesDetail(slug: String) -> Element {
|
|||||||
|
|
||||||
// Card 5: Permaculture
|
// Card 5: Permaculture
|
||||||
div { class: "detail-card",
|
div { class: "detail-card",
|
||||||
div { class: "detail-card-header", "Permaculture" }
|
div { class: "detail-card-header", "{t(l, \"card.permaculture\")}" }
|
||||||
table { class: "attr-table",
|
table { class: "attr-table",
|
||||||
tbody {
|
tbody {
|
||||||
tr {
|
tr {
|
||||||
th { "Nitrogen Fixer" }
|
th { "{t(l, \"field.nitrogen_fixer\")}" }
|
||||||
td { class: if n_fixer == em { "placeholder" } else { "" }, "{n_fixer}" }
|
td { class: if n_fixer == em { "placeholder" } else { "" }, "{n_fixer}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Dynamic Accumulator" }
|
th { "{t(l, \"field.dynamic_acc\")}" }
|
||||||
td { class: if dyn_acc == em { "placeholder" } else { "" }, "{dyn_acc}" }
|
td { class: if dyn_acc == em { "placeholder" } else { "" }, "{dyn_acc}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Attracts Pollinators" }
|
th { "{t(l, \"field.attracts_pollinators\")}" }
|
||||||
td { class: if pollinators == em { "placeholder" } else { "" }, "{pollinators}" }
|
td { class: if pollinators == em { "placeholder" } else { "" }, "{pollinators}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Attracts Beneficial Insects" }
|
th { "{t(l, \"field.attracts_beneficial\")}" }
|
||||||
td { class: if beneficial == em { "placeholder" } else { "" }, "{beneficial}" }
|
td { class: if beneficial == em { "placeholder" } else { "" }, "{beneficial}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Mulch Plant" }
|
th { "{t(l, \"field.mulch_plant\")}" }
|
||||||
td { class: if mulch == em { "placeholder" } else { "" }, "{mulch}" }
|
td { class: if mulch == em { "placeholder" } else { "" }, "{mulch}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Ground Cover Quality" }
|
th { "{t(l, \"field.ground_cover_quality\")}" }
|
||||||
td { class: if gc_quality == em { "placeholder" } else { "" }, "{gc_quality}" }
|
td { class: if gc_quality == em { "placeholder" } else { "" }, "{gc_quality}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Allelopathic" }
|
th { "{t(l, \"field.allelopathic\")}" }
|
||||||
td { class: if allelo == em { "placeholder" } else { "" }, "{allelo}" }
|
td { class: if allelo == em { "placeholder" } else { "" }, "{allelo}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Guild Role" }
|
th { "{t(l, \"field.guild_role\")}" }
|
||||||
td { class: if guild == em { "placeholder" } else { "" }, "{guild}" }
|
td { class: if guild == em { "placeholder" } else { "" }, "{guild}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -767,11 +769,11 @@ pub fn SpeciesDetail(slug: String) -> Element {
|
|||||||
|
|
||||||
// Card 6: Wildlife & Ecology
|
// Card 6: Wildlife & Ecology
|
||||||
div { class: "detail-card",
|
div { class: "detail-card",
|
||||||
div { class: "detail-card-header", "Wildlife & Ecology" }
|
div { class: "detail-card-header", "{t(l, \"card.wildlife\")}" }
|
||||||
table { class: "attr-table",
|
table { class: "attr-table",
|
||||||
tbody {
|
tbody {
|
||||||
tr {
|
tr {
|
||||||
th { "Nectar Value" }
|
th { "{t(l, \"field.nectar\")}" }
|
||||||
td {
|
td {
|
||||||
match s.nectar_value {
|
match s.nectar_value {
|
||||||
Some(v) => rsx! {
|
Some(v) => rsx! {
|
||||||
@@ -788,7 +790,7 @@ pub fn SpeciesDetail(slug: String) -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Pollen Value" }
|
th { "{t(l, \"field.pollen\")}" }
|
||||||
td {
|
td {
|
||||||
match s.pollen_value {
|
match s.pollen_value {
|
||||||
Some(v) => rsx! {
|
Some(v) => rsx! {
|
||||||
@@ -805,69 +807,69 @@ pub fn SpeciesDetail(slug: String) -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Wild Bees" }
|
th { "{t(l, \"field.wild_bees\")}" }
|
||||||
td {
|
td {
|
||||||
match (s.wild_bee_count, s.wild_bee_specialist_count) {
|
match (s.wild_bee_count, s.wild_bee_specialist_count) {
|
||||||
(Some(total), Some(spec)) => rsx! { "{total} species ({spec} specialists)" },
|
(Some(total), Some(spec)) => rsx! { "{total} {t(l, \"species_species\")} ({spec} specialists)" },
|
||||||
(Some(total), None) => rsx! { "{total} species" },
|
(Some(total), None) => rsx! { "{total} {t(l, \"species_species\")}" },
|
||||||
_ => rsx! { span { class: "placeholder", "\u{2014}" } },
|
_ => rsx! { span { class: "placeholder", "\u{2014}" } },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Butterflies & Moths" }
|
th { "{t(l, \"field.butterflies\")}" }
|
||||||
td {
|
td {
|
||||||
match (s.butterfly_moth_count, s.caterpillar_host_count) {
|
match (s.butterfly_moth_count, s.caterpillar_host_count) {
|
||||||
(Some(bm), Some(ch)) => rsx! { "{bm} species ({ch} caterpillar hosts)" },
|
(Some(bm), Some(ch)) => rsx! { "{bm} {t(l, \"species_species\")} ({ch} caterpillar hosts)" },
|
||||||
(Some(bm), None) => rsx! { "{bm} species" },
|
(Some(bm), None) => rsx! { "{bm} {t(l, \"species_species\")}" },
|
||||||
_ => rsx! { span { class: "placeholder", "\u{2014}" } },
|
_ => rsx! { span { class: "placeholder", "\u{2014}" } },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if s.caterpillar_specialist_count.is_some() {
|
if s.caterpillar_specialist_count.is_some() {
|
||||||
tr {
|
tr {
|
||||||
th { "Caterpillar Specialists" }
|
th { "{t(l, \"field.caterpillar_specialists\")}" }
|
||||||
td { "{s.caterpillar_specialist_count.unwrap()}" }
|
td { "{s.caterpillar_specialist_count.unwrap()}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Hoverflies" }
|
th { "{t(l, \"field.hoverflies\")}" }
|
||||||
td {
|
td {
|
||||||
match s.hoverfly_count {
|
match s.hoverfly_count {
|
||||||
Some(v) => rsx! { "{v} species" },
|
Some(v) => rsx! { "{v} {t(l, \"species_species\")}" },
|
||||||
None => rsx! { span { class: "placeholder", "\u{2014}" } },
|
None => rsx! { span { class: "placeholder", "\u{2014}" } },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Beetles" }
|
th { "{t(l, \"field.beetles\")}" }
|
||||||
td {
|
td {
|
||||||
match s.beetle_count {
|
match s.beetle_count {
|
||||||
Some(v) => rsx! { "{v} species" },
|
Some(v) => rsx! { "{v} {t(l, \"species_species\")}" },
|
||||||
None => rsx! { span { class: "placeholder", "\u{2014}" } },
|
None => rsx! { span { class: "placeholder", "\u{2014}" } },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Birds" }
|
th { "{t(l, \"field.birds\")}" }
|
||||||
td {
|
td {
|
||||||
match s.bird_count {
|
match s.bird_count {
|
||||||
Some(v) => rsx! { "{v} species" },
|
Some(v) => rsx! { "{v} {t(l, \"species_species\")}" },
|
||||||
None => rsx! { span { class: "placeholder", "\u{2014}" } },
|
None => rsx! { span { class: "placeholder", "\u{2014}" } },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Mammals" }
|
th { "{t(l, \"field.mammals\")}" }
|
||||||
td {
|
td {
|
||||||
match s.mammal_count {
|
match s.mammal_count {
|
||||||
Some(v) => rsx! { "{v} species" },
|
Some(v) => rsx! { "{v} {t(l, \"species_species\")}" },
|
||||||
None => rsx! { span { class: "placeholder", "\u{2014}" } },
|
None => rsx! { span { class: "placeholder", "\u{2014}" } },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Native Status" }
|
th { "{t(l, \"field.native_status\")}" }
|
||||||
td {
|
td {
|
||||||
match &s.native_status {
|
match &s.native_status {
|
||||||
Some(ns) if !ns.is_empty() => {
|
Some(ns) if !ns.is_empty() => {
|
||||||
@@ -884,7 +886,7 @@ pub fn SpeciesDetail(slug: String) -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "NaturaDB Tags" }
|
th { "{t(l, \"field.naturadb_tags\")}" }
|
||||||
td {
|
td {
|
||||||
match &s.naturadb_tags {
|
match &s.naturadb_tags {
|
||||||
Some(tags) if !tags.is_empty() => {
|
Some(tags) if !tags.is_empty() => {
|
||||||
@@ -907,11 +909,11 @@ pub fn SpeciesDetail(slug: String) -> Element {
|
|||||||
|
|
||||||
// Card 7: External Links
|
// Card 7: External Links
|
||||||
div { class: "detail-card",
|
div { class: "detail-card",
|
||||||
div { class: "detail-card-header", "External Links" }
|
div { class: "detail-card-header", "{t(l, \"card.external_links\")}" }
|
||||||
table { class: "attr-table",
|
table { class: "attr-table",
|
||||||
tbody {
|
tbody {
|
||||||
tr {
|
tr {
|
||||||
th { "Wikidata QID" }
|
th { "{t(l, \"field.wikidata_qid\")}" }
|
||||||
td {
|
td {
|
||||||
if let Some(ref q) = qid {
|
if let Some(ref q) = qid {
|
||||||
if !q.is_empty() {
|
if !q.is_empty() {
|
||||||
@@ -925,7 +927,7 @@ pub fn SpeciesDetail(slug: String) -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "GBIF ID" }
|
th { "{t(l, \"field.gbif_id\")}" }
|
||||||
td {
|
td {
|
||||||
if let Some(ref g) = gbif {
|
if let Some(ref g) = gbif {
|
||||||
if !g.is_empty() {
|
if !g.is_empty() {
|
||||||
@@ -939,15 +941,15 @@ pub fn SpeciesDetail(slug: String) -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "EPPO Code" }
|
th { "{t(l, \"field.eppo_code\")}" }
|
||||||
td { class: if eppo == em { "placeholder" } else { "" }, "{eppo}" }
|
td { class: if eppo == em { "placeholder" } else { "" }, "{eppo}" }
|
||||||
}
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "PFAF URL" }
|
th { "{t(l, \"field.pfaf_url\")}" }
|
||||||
td {
|
td {
|
||||||
if let Some(ref u) = pfaf {
|
if let Some(ref u) = pfaf {
|
||||||
if !u.is_empty() {
|
if !u.is_empty() {
|
||||||
a { href: "{u}", target: "_blank", class: "external-link", "View on PFAF" }
|
a { href: "{u}", target: "_blank", class: "external-link", "{t(l, \"field.view_on_pfaf\")}" }
|
||||||
} else {
|
} else {
|
||||||
span { class: "placeholder", "\u{2014}" }
|
span { class: "placeholder", "\u{2014}" }
|
||||||
}
|
}
|
||||||
@@ -963,7 +965,7 @@ pub fn SpeciesDetail(slug: String) -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cultivars for this species (below the two-column layout)
|
// Cultivars for this species (below the two-column layout)
|
||||||
h2 { "Cultivars" }
|
h2 { "{t(l, \"card.cultivars\")}" }
|
||||||
CultivarListForSpecies { species_slug: species_slug }
|
CultivarListForSpecies { species_slug: species_slug }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -981,13 +983,15 @@ fn CultivarListForSpecies(species_slug: String) -> Element {
|
|||||||
async move { api::list_cultivars(1, 100, Some(&s), None).await }
|
async move { api::list_cultivars(1, 100, Some(&s), None).await }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let l = lang.read().clone();
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
match &*cultivars.read() {
|
match &*cultivars.read() {
|
||||||
None => rsx! { p { "Loading..." } },
|
None => rsx! { p { "{t(&l, \"loading\")}" } },
|
||||||
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
|
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
|
||||||
Some(Ok(data)) => {
|
Some(Ok(data)) => {
|
||||||
if data.data.is_empty() {
|
if data.data.is_empty() {
|
||||||
rsx! { p { class: "empty", "No cultivars yet." } }
|
rsx! { p { class: "empty", "{t(&l, \"no_cultivars\")}" } }
|
||||||
} else {
|
} else {
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "card-grid",
|
div { class: "card-grid",
|
||||||
@@ -1004,7 +1008,7 @@ fn CultivarListForSpecies(species_slug: String) -> Element {
|
|||||||
p { class: "card-common", "{common}" }
|
p { class: "card-common", "{common}" }
|
||||||
}
|
}
|
||||||
if c.is_organic {
|
if c.is_organic {
|
||||||
span { class: "badge organic", "Organic" }
|
span { class: "badge organic", "{t(&l, \"field.organic\")}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
use crate::api;
|
use crate::api;
|
||||||
use crate::app::Route;
|
use crate::app::{Lang, Route};
|
||||||
use crate::components::table_controls::*;
|
use crate::components::table_controls::*;
|
||||||
|
use crate::i18n::t;
|
||||||
|
|
||||||
const STORAGE_KEY_COLS: &str = "herbapi_suppliers_cols";
|
const STORAGE_KEY_COLS: &str = "herbapi_suppliers_cols";
|
||||||
|
|
||||||
@@ -19,14 +20,17 @@ fn supplier_columns() -> Vec<ColumnDef> {
|
|||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn SupplierList() -> Element {
|
pub fn SupplierList() -> Element {
|
||||||
|
let lang = use_context::<Lang>().0;
|
||||||
let columns = supplier_columns();
|
let columns = supplier_columns();
|
||||||
let visible_cols = use_signal(|| load_visible_columns(STORAGE_KEY_COLS, &supplier_columns()));
|
let visible_cols = use_signal(|| load_visible_columns(STORAGE_KEY_COLS, &supplier_columns()));
|
||||||
|
|
||||||
let suppliers = use_resource(|| async { api::list_suppliers().await });
|
let suppliers = use_resource(|| async { api::list_suppliers().await });
|
||||||
|
|
||||||
|
let l = lang.read().clone();
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "page",
|
div { class: "page",
|
||||||
h1 { "Suppliers" }
|
h1 { "{t(&l, \"page.suppliers\")}" }
|
||||||
|
|
||||||
ColumnToggle {
|
ColumnToggle {
|
||||||
columns: columns.clone(),
|
columns: columns.clone(),
|
||||||
@@ -35,33 +39,33 @@ pub fn SupplierList() -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match &*suppliers.read() {
|
match &*suppliers.read() {
|
||||||
None => rsx! { p { "Loading..." } },
|
None => rsx! { p { "{t(&l, \"loading\")}" } },
|
||||||
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
|
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
|
||||||
Some(Ok(data)) => {
|
Some(Ok(data)) => {
|
||||||
let vis = visible_cols.read();
|
let vis = visible_cols.read();
|
||||||
rsx! {
|
rsx! {
|
||||||
p { class: "result-count", "{data.len()} suppliers" }
|
p { class: "result-count", "{data.len()} {t(&l, \"nav.suppliers\").to_lowercase()}" }
|
||||||
div { class: "table-wrap",
|
div { class: "table-wrap",
|
||||||
table {
|
table {
|
||||||
thead {
|
thead {
|
||||||
tr {
|
tr {
|
||||||
if is_col_visible(&vis, "name") {
|
if is_col_visible(&vis, "name") {
|
||||||
th { "Name" }
|
th { "{t(&l, \"field.name\")}" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "country") {
|
if is_col_visible(&vis, "country") {
|
||||||
th { "Country" }
|
th { "{t(&l, \"field.country\")}" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "organic") {
|
if is_col_visible(&vis, "organic") {
|
||||||
th { "Organic" }
|
th { "{t(&l, \"field.organic\")}" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "demeter") {
|
if is_col_visible(&vis, "demeter") {
|
||||||
th { "Demeter" }
|
th { "Demeter" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "website") {
|
if is_col_visible(&vis, "website") {
|
||||||
th { "Website" }
|
th { "{t(&l, \"field.website\")}" }
|
||||||
}
|
}
|
||||||
if is_col_visible(&vis, "notes") {
|
if is_col_visible(&vis, "notes") {
|
||||||
th { "Notes" }
|
th { "{t(&l, \"field.notes\")}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,7 +85,7 @@ pub fn SupplierList() -> Element {
|
|||||||
if is_col_visible(&vis, "organic") {
|
if is_col_visible(&vis, "organic") {
|
||||||
td {
|
td {
|
||||||
if s.is_organic {
|
if s.is_organic {
|
||||||
span { class: "badge organic", "Organic" }
|
span { class: "badge organic", "{t(&l, \"field.organic\")}" }
|
||||||
} else {
|
} else {
|
||||||
"-"
|
"-"
|
||||||
}
|
}
|
||||||
@@ -127,16 +131,19 @@ pub fn SupplierList() -> Element {
|
|||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn SupplierDetail(slug: String) -> Element {
|
pub fn SupplierDetail(slug: String) -> Element {
|
||||||
|
let lang = use_context::<Lang>().0;
|
||||||
let slug_clone = slug.clone();
|
let slug_clone = slug.clone();
|
||||||
let supplier = use_resource(move || {
|
let supplier = use_resource(move || {
|
||||||
let s = slug_clone.clone();
|
let s = slug_clone.clone();
|
||||||
async move { api::get_supplier(&s).await }
|
async move { api::get_supplier(&s).await }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let l = lang.read().clone();
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "page",
|
div { class: "page",
|
||||||
match &*supplier.read() {
|
match &*supplier.read() {
|
||||||
None => rsx! { p { "Loading..." } },
|
None => rsx! { p { "{t(&l, \"loading\")}" } },
|
||||||
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
|
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
|
||||||
Some(Ok(s)) => rsx! {
|
Some(Ok(s)) => rsx! {
|
||||||
h1 { "{s.name}" }
|
h1 { "{s.name}" }
|
||||||
@@ -145,7 +152,7 @@ pub fn SupplierDetail(slug: String) -> Element {
|
|||||||
}
|
}
|
||||||
div { class: "badges",
|
div { class: "badges",
|
||||||
if s.is_organic {
|
if s.is_organic {
|
||||||
span { class: "badge organic", "Organic" }
|
span { class: "badge organic", "{t(&l, \"field.organic\")}" }
|
||||||
}
|
}
|
||||||
if s.is_demeter {
|
if s.is_demeter {
|
||||||
span { class: "badge demeter", "Demeter" }
|
span { class: "badge demeter", "Demeter" }
|
||||||
|
|||||||
Reference in New Issue
Block a user