Add DE/EN language toggle with bilingual descriptions
- Language switcher in sidebar (DE/EN buttons, persists to localStorage) - i18n module with pick_desc/pick_name helpers for language-aware fallback - All detail/list pages use language context for names and descriptions - Species/Cultivar types updated with description_de/description_en - Common Name column added to species/families lists
This commit is contained in:
@@ -120,8 +120,51 @@ em {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-user {
|
/* Language toggle */
|
||||||
|
|
||||||
|
.sidebar-lang {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
|
border-top: 1px solid rgba(255,255,255,0.1);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-toggle {
|
||||||
|
display: flex;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-btn {
|
||||||
|
padding: 0.3rem 0.75rem;
|
||||||
|
background: transparent;
|
||||||
|
color: rgba(255,255,255,0.5);
|
||||||
|
border: none;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-btn:hover {
|
||||||
|
color: rgba(255,255,255,0.8);
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-btn-active {
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-btn-active:hover {
|
||||||
|
background: var(--accent-hover);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-user {
|
||||||
padding: 1rem 1.25rem;
|
padding: 1rem 1.25rem;
|
||||||
border-top: 1px solid rgba(255,255,255,0.1);
|
border-top: 1px solid rgba(255,255,255,0.1);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
use gloo_storage::{LocalStorage, Storage};
|
||||||
|
|
||||||
use crate::api;
|
use crate::api;
|
||||||
use crate::types::MeResponse;
|
use crate::types::MeResponse;
|
||||||
|
|
||||||
|
/// Global language signal shared via context. Values: "de" or "en".
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct Lang(pub Signal<String>);
|
||||||
|
|
||||||
#[derive(Routable, Clone, Debug, PartialEq)]
|
#[derive(Routable, Clone, Debug, PartialEq)]
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
pub enum Route {
|
pub enum Route {
|
||||||
@@ -43,6 +48,15 @@ pub fn App() -> Element {
|
|||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn Layout() -> Element {
|
fn Layout() -> Element {
|
||||||
|
// Language state: load from localStorage, default "de"
|
||||||
|
let lang_signal = use_signal(|| {
|
||||||
|
LocalStorage::get::<String>("herbapi_lang").unwrap_or_else(|_| "de".to_string())
|
||||||
|
});
|
||||||
|
use_context_provider(|| Lang(lang_signal));
|
||||||
|
|
||||||
|
let mut lang = use_context::<Lang>().0;
|
||||||
|
let current_lang = lang.read().clone();
|
||||||
|
|
||||||
// Try to get current user (may be None for public access)
|
// Try to get current user (may be None for public access)
|
||||||
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());
|
||||||
@@ -66,6 +80,26 @@ fn Layout() -> Element {
|
|||||||
NavLink { to: Route::SearchPage {}, label: "Search" }
|
NavLink { to: Route::SearchPage {}, label: "Search" }
|
||||||
NavLink { to: Route::Sources {}, label: "Sources" }
|
NavLink { to: Route::Sources {}, label: "Sources" }
|
||||||
}
|
}
|
||||||
|
div { class: "sidebar-lang",
|
||||||
|
div { class: "lang-toggle",
|
||||||
|
button {
|
||||||
|
class: if current_lang == "de" { "lang-btn lang-btn-active" } else { "lang-btn" },
|
||||||
|
onclick: move |_| {
|
||||||
|
lang.set("de".to_string());
|
||||||
|
let _ = LocalStorage::set("herbapi_lang", "de".to_string());
|
||||||
|
},
|
||||||
|
"DE"
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
class: if current_lang == "en" { "lang-btn lang-btn-active" } else { "lang-btn" },
|
||||||
|
onclick: move |_| {
|
||||||
|
lang.set("en".to_string());
|
||||||
|
let _ = LocalStorage::set("herbapi_lang", "en".to_string());
|
||||||
|
},
|
||||||
|
"EN"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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)}" }
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/// Pick the right description based on language, falling back to the other.
|
||||||
|
pub fn pick_desc(lang: &str, de: &Option<String>, en: &Option<String>, fallback: &Option<String>) -> String {
|
||||||
|
match lang {
|
||||||
|
"de" => de.as_deref()
|
||||||
|
.or(fallback.as_deref())
|
||||||
|
.or(en.as_deref())
|
||||||
|
.unwrap_or("\u{2014}").to_string(),
|
||||||
|
_ => en.as_deref()
|
||||||
|
.or(fallback.as_deref())
|
||||||
|
.or(de.as_deref())
|
||||||
|
.unwrap_or("\u{2014}").to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pick the right name based on language
|
||||||
|
pub fn pick_name(lang: &str, de: &Option<String>, en: &Option<String>, scientific: &str) -> String {
|
||||||
|
match lang {
|
||||||
|
"de" => de.as_deref().filter(|s| !s.is_empty())
|
||||||
|
.or(en.as_deref().filter(|s| !s.is_empty()))
|
||||||
|
.unwrap_or(scientific).to_string(),
|
||||||
|
_ => en.as_deref().filter(|s| !s.is_empty())
|
||||||
|
.or(de.as_deref().filter(|s| !s.is_empty()))
|
||||||
|
.unwrap_or(scientific).to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
mod api;
|
mod api;
|
||||||
mod app;
|
mod app;
|
||||||
mod components;
|
mod components;
|
||||||
|
mod i18n;
|
||||||
mod pages;
|
mod pages;
|
||||||
mod types;
|
mod types;
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ use std::collections::HashMap;
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
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::{pick_desc, pick_name};
|
||||||
|
|
||||||
/// 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 {
|
||||||
@@ -279,6 +280,7 @@ pub fn CultivarList() -> Element {
|
|||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn CultivarDetail(slug: String) -> Element {
|
pub fn CultivarDetail(slug: String) -> Element {
|
||||||
|
let lang = use_context::<Lang>().0;
|
||||||
let slug_clone = slug.clone();
|
let slug_clone = slug.clone();
|
||||||
let cultivar = use_resource(move || {
|
let cultivar = use_resource(move || {
|
||||||
let s = slug_clone.clone();
|
let s = slug_clone.clone();
|
||||||
@@ -326,11 +328,14 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
None => rsx! { p { "Loading..." } },
|
None => rsx! { p { "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();
|
||||||
|
|
||||||
// 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 name_en = opt_str(&c.name_en);
|
let name_en = opt_str(&c.name_en);
|
||||||
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 = opt_str(&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(c.is_organic);
|
||||||
let perennial = bool_display(c.perennial);
|
let perennial = bool_display(c.perennial);
|
||||||
|
|
||||||
@@ -369,6 +374,9 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
h1 { "{c.name}" }
|
h1 { "{c.name}" }
|
||||||
|
if common_name != c.name {
|
||||||
|
p { class: "name-common", "{common_name}" }
|
||||||
|
}
|
||||||
|
|
||||||
div { class: "detail-row",
|
div { class: "detail-row",
|
||||||
// === LEFT COLUMN ===
|
// === LEFT COLUMN ===
|
||||||
@@ -383,6 +391,10 @@ pub fn CultivarDetail(slug: String) -> Element {
|
|||||||
th { "Name" }
|
th { "Name" }
|
||||||
td { "{c.name}" }
|
td { "{c.name}" }
|
||||||
}
|
}
|
||||||
|
tr {
|
||||||
|
th { "Common Name" }
|
||||||
|
td { "{common_name}" }
|
||||||
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Name EN" }
|
th { "Name EN" }
|
||||||
td { class: if name_en == "\u{2014}" { "placeholder" } else { "" }, "{name_en}" }
|
td { class: if name_en == "\u{2014}" { "placeholder" } else { "" }, "{name_en}" }
|
||||||
|
|||||||
@@ -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::pick_name;
|
||||||
|
|
||||||
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";
|
||||||
@@ -10,14 +11,16 @@ const STORAGE_KEY_PP: &str = "herbapi_families_pp";
|
|||||||
fn family_columns() -> Vec<ColumnDef> {
|
fn family_columns() -> Vec<ColumnDef> {
|
||||||
vec![
|
vec![
|
||||||
ColumnDef { key: "name_scientific", label: "Scientific Name", default_visible: true },
|
ColumnDef { key: "name_scientific", label: "Scientific Name", default_visible: true },
|
||||||
ColumnDef { key: "name_en", label: "English", default_visible: true },
|
ColumnDef { key: "common_name", label: "Common Name", default_visible: true },
|
||||||
ColumnDef { key: "name_de", label: "German", default_visible: true },
|
ColumnDef { key: "name_en", label: "English", default_visible: false },
|
||||||
|
ColumnDef { key: "name_de", label: "German", default_visible: false },
|
||||||
ColumnDef { key: "description", label: "Description", default_visible: false },
|
ColumnDef { key: "description", label: "Description", default_visible: false },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn FamilyList() -> Element {
|
pub fn FamilyList() -> Element {
|
||||||
|
let lang = use_context::<Lang>().0;
|
||||||
let columns = family_columns();
|
let columns = family_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));
|
||||||
@@ -79,6 +82,9 @@ pub fn FamilyList() -> Element {
|
|||||||
if is_col_visible(&vis, "name_scientific") {
|
if is_col_visible(&vis, "name_scientific") {
|
||||||
th { "Scientific Name" }
|
th { "Scientific Name" }
|
||||||
}
|
}
|
||||||
|
if is_col_visible(&vis, "common_name") {
|
||||||
|
th { "Common Name" }
|
||||||
|
}
|
||||||
if is_col_visible(&vis, "name_en") {
|
if is_col_visible(&vis, "name_en") {
|
||||||
th { "English" }
|
th { "English" }
|
||||||
}
|
}
|
||||||
@@ -92,23 +98,31 @@ pub fn FamilyList() -> Element {
|
|||||||
}
|
}
|
||||||
tbody {
|
tbody {
|
||||||
for f in data.data.iter() {
|
for f in data.data.iter() {
|
||||||
tr {
|
{
|
||||||
if is_col_visible(&vis, "name_scientific") {
|
let common = pick_name(&lang.read(), &f.name_de, &f.name_en, &f.name_scientific);
|
||||||
td {
|
rsx! {
|
||||||
Link { to: Route::FamilyDetail { slug: f.slug.clone() },
|
tr {
|
||||||
em { "{f.name_scientific}" }
|
if is_col_visible(&vis, "name_scientific") {
|
||||||
|
td {
|
||||||
|
Link { to: Route::FamilyDetail { slug: f.slug.clone() },
|
||||||
|
em { "{f.name_scientific}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if is_col_visible(&vis, "common_name") {
|
||||||
|
td { "{common}" }
|
||||||
|
}
|
||||||
|
if is_col_visible(&vis, "name_en") {
|
||||||
|
td { "{f.name_en.as_deref().unwrap_or(\"-\")}" }
|
||||||
|
}
|
||||||
|
if is_col_visible(&vis, "name_de") {
|
||||||
|
td { "{f.name_de.as_deref().unwrap_or(\"-\")}" }
|
||||||
|
}
|
||||||
|
if is_col_visible(&vis, "description") {
|
||||||
|
td { class: "cell-truncated",
|
||||||
|
"{f.description.as_deref().map(|d| truncate(d, 80)).unwrap_or_else(|| \"-\".to_string())}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
if is_col_visible(&vis, "name_en") {
|
|
||||||
td { "{f.name_en.as_deref().unwrap_or(\"-\")}" }
|
|
||||||
}
|
|
||||||
if is_col_visible(&vis, "name_de") {
|
|
||||||
td { "{f.name_de.as_deref().unwrap_or(\"-\")}" }
|
|
||||||
}
|
|
||||||
if is_col_visible(&vis, "description") {
|
|
||||||
td { class: "cell-truncated",
|
|
||||||
"{f.description.as_deref().map(|d| truncate(d, 80)).unwrap_or_else(|| \"-\".to_string())}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,6 +154,7 @@ pub fn FamilyList() -> Element {
|
|||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn FamilyDetail(slug: String) -> Element {
|
pub fn FamilyDetail(slug: String) -> Element {
|
||||||
|
let lang = use_context::<Lang>().0;
|
||||||
let slug_clone = slug.clone();
|
let slug_clone = slug.clone();
|
||||||
let family = use_resource(move || {
|
let family = use_resource(move || {
|
||||||
let s = slug_clone.clone();
|
let s = slug_clone.clone();
|
||||||
@@ -157,16 +172,16 @@ pub fn FamilyDetail(slug: String) -> Element {
|
|||||||
match &*family.read() {
|
match &*family.read() {
|
||||||
None => rsx! { p { "Loading..." } },
|
None => rsx! { p { "Loading..." } },
|
||||||
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
|
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
|
||||||
Some(Ok(f)) => rsx! {
|
Some(Ok(f)) => {
|
||||||
h1 { em { "{f.name_scientific}" } }
|
let common = pick_name(&lang.read(), &f.name_de, &f.name_en, &f.name_scientific);
|
||||||
if let Some(ref en) = f.name_en {
|
rsx! {
|
||||||
p { class: "name-common", "{en}" }
|
h1 { em { "{f.name_scientific}" } }
|
||||||
}
|
if common != f.name_scientific {
|
||||||
if let Some(ref de) = f.name_de {
|
p { class: "name-common", "{common}" }
|
||||||
p { class: "name-common", "{de}" }
|
}
|
||||||
}
|
if let Some(ref desc) = f.description {
|
||||||
if let Some(ref desc) = f.description {
|
p { "{desc}" }
|
||||||
p { "{desc}" }
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -178,12 +193,18 @@ pub fn FamilyDetail(slug: String) -> Element {
|
|||||||
Some(Ok(data)) => rsx! {
|
Some(Ok(data)) => rsx! {
|
||||||
div { class: "card-grid",
|
div { class: "card-grid",
|
||||||
for s in data.data.iter() {
|
for s in data.data.iter() {
|
||||||
div { class: "plant-card",
|
{
|
||||||
Link { to: Route::SpeciesDetail { slug: s.slug.clone() },
|
let sp_common = pick_name(&lang.read(), &s.name_de, &s.name_en, &s.name_scientific);
|
||||||
em { "{s.name_scientific}" }
|
let show_common = if sp_common == s.name_scientific { None } else { Some(sp_common) };
|
||||||
}
|
rsx! {
|
||||||
if let Some(ref en) = s.name_en {
|
div { class: "plant-card",
|
||||||
p { class: "card-common", "{en}" }
|
Link { to: Route::SpeciesDetail { slug: s.slug.clone() },
|
||||||
|
em { "{s.name_scientific}" }
|
||||||
|
}
|
||||||
|
if let Some(ref common) = show_common {
|
||||||
|
p { class: "card-common", "{common}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
use crate::api;
|
use crate::api;
|
||||||
use crate::app::Route;
|
use crate::app::{Lang, Route};
|
||||||
use crate::components::plant_card::PlantCard;
|
use crate::components::plant_card::PlantCard;
|
||||||
|
use crate::i18n::pick_name;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Home() -> Element {
|
pub fn Home() -> Element {
|
||||||
|
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 });
|
||||||
|
|
||||||
@@ -36,12 +38,18 @@ pub fn Home() -> Element {
|
|||||||
Some(Ok(data)) => rsx! {
|
Some(Ok(data)) => rsx! {
|
||||||
div { class: "card-grid",
|
div { class: "card-grid",
|
||||||
for s in data.data.iter().take(12) {
|
for s in data.data.iter().take(12) {
|
||||||
PlantCard {
|
{
|
||||||
key: "{s.id}",
|
let card_common = pick_name(&lang.read(), &s.name_de, &s.name_en, &s.name_scientific);
|
||||||
slug: s.slug.clone(),
|
let show_common = if card_common == s.name_scientific { None } else { Some(card_common) };
|
||||||
name: s.name_scientific.clone(),
|
rsx! {
|
||||||
name_common: s.name_en.clone(),
|
PlantCard {
|
||||||
entity_type: "species".to_string(),
|
key: "{s.id}",
|
||||||
|
slug: s.slug.clone(),
|
||||||
|
name: s.name_scientific.clone(),
|
||||||
|
name_common: show_common,
|
||||||
|
entity_type: "species".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ use std::collections::HashMap;
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::api;
|
use crate::api;
|
||||||
use crate::app::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};
|
||||||
|
|
||||||
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";
|
||||||
@@ -15,7 +16,8 @@ const STORAGE_KEY_VIEW: &str = "herbapi_species_view";
|
|||||||
fn species_columns() -> Vec<ColumnDef> {
|
fn species_columns() -> Vec<ColumnDef> {
|
||||||
vec![
|
vec![
|
||||||
ColumnDef { key: "name_scientific", label: "Scientific Name", default_visible: true },
|
ColumnDef { key: "name_scientific", label: "Scientific Name", default_visible: true },
|
||||||
ColumnDef { key: "name_de", label: "German", default_visible: true },
|
ColumnDef { key: "common_name", label: "Common Name", default_visible: true },
|
||||||
|
ColumnDef { key: "name_de", label: "German", default_visible: false },
|
||||||
ColumnDef { key: "name_en", label: "English", default_visible: false },
|
ColumnDef { key: "name_en", label: "English", default_visible: false },
|
||||||
ColumnDef { key: "family", label: "Family", default_visible: true },
|
ColumnDef { key: "family", label: "Family", default_visible: true },
|
||||||
ColumnDef { key: "plant_layer", label: "Layer", default_visible: true },
|
ColumnDef { key: "plant_layer", label: "Layer", default_visible: true },
|
||||||
@@ -33,6 +35,7 @@ fn species_columns() -> Vec<ColumnDef> {
|
|||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn SpeciesList() -> Element {
|
pub fn SpeciesList() -> Element {
|
||||||
|
let lang = use_context::<Lang>().0;
|
||||||
let columns = species_columns();
|
let columns = species_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));
|
||||||
@@ -222,6 +225,9 @@ pub fn SpeciesList() -> Element {
|
|||||||
if is_col_visible(&vis, "name_scientific") {
|
if is_col_visible(&vis, "name_scientific") {
|
||||||
th { "Scientific Name" }
|
th { "Scientific Name" }
|
||||||
}
|
}
|
||||||
|
if is_col_visible(&vis, "common_name") {
|
||||||
|
th { "Common Name" }
|
||||||
|
}
|
||||||
if is_col_visible(&vis, "name_de") {
|
if is_col_visible(&vis, "name_de") {
|
||||||
th { "German" }
|
th { "German" }
|
||||||
}
|
}
|
||||||
@@ -267,6 +273,7 @@ pub fn SpeciesList() -> Element {
|
|||||||
for s in data.data.iter() {
|
for s in data.data.iter() {
|
||||||
{
|
{
|
||||||
let family_name: &str = fm.get(&s.family_id).map(String::as_str).unwrap_or("-");
|
let family_name: &str = fm.get(&s.family_id).map(String::as_str).unwrap_or("-");
|
||||||
|
let common = pick_name(&lang.read(), &s.name_de, &s.name_en, &s.name_scientific);
|
||||||
rsx! {
|
rsx! {
|
||||||
tr {
|
tr {
|
||||||
if is_col_visible(&vis, "name_scientific") {
|
if is_col_visible(&vis, "name_scientific") {
|
||||||
@@ -276,6 +283,9 @@ pub fn SpeciesList() -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if is_col_visible(&vis, "common_name") {
|
||||||
|
td { "{common}" }
|
||||||
|
}
|
||||||
if is_col_visible(&vis, "name_de") {
|
if is_col_visible(&vis, "name_de") {
|
||||||
td { "{s.name_de.as_deref().unwrap_or(\"-\")}" }
|
td { "{s.name_de.as_deref().unwrap_or(\"-\")}" }
|
||||||
}
|
}
|
||||||
@@ -369,12 +379,18 @@ pub fn SpeciesList() -> Element {
|
|||||||
} else {
|
} else {
|
||||||
div { class: "card-grid",
|
div { class: "card-grid",
|
||||||
for s in data.data.iter() {
|
for s in data.data.iter() {
|
||||||
PlantCard {
|
{
|
||||||
key: "{s.id}",
|
let card_common = pick_name(&lang.read(), &s.name_de, &s.name_en, &s.name_scientific);
|
||||||
slug: s.slug.clone(),
|
let show_common = if card_common == s.name_scientific { None } else { Some(card_common) };
|
||||||
name: s.name_scientific.clone(),
|
rsx! {
|
||||||
name_common: s.name_en.clone(),
|
PlantCard {
|
||||||
entity_type: "species".to_string(),
|
key: "{s.id}",
|
||||||
|
slug: s.slug.clone(),
|
||||||
|
name: s.name_scientific.clone(),
|
||||||
|
name_common: show_common,
|
||||||
|
entity_type: "species".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -404,6 +420,7 @@ pub fn SpeciesList() -> Element {
|
|||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn SpeciesDetail(slug: String) -> Element {
|
pub fn SpeciesDetail(slug: String) -> Element {
|
||||||
|
let lang = use_context::<Lang>().0;
|
||||||
let slug_clone = slug.clone();
|
let slug_clone = slug.clone();
|
||||||
let species = use_resource(move || {
|
let species = use_resource(move || {
|
||||||
let s = slug_clone.clone();
|
let s = slug_clone.clone();
|
||||||
@@ -436,6 +453,7 @@ pub fn SpeciesDetail(slug: String) -> Element {
|
|||||||
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();
|
||||||
|
|
||||||
// Helper closures to format fields
|
// Helper closures to format fields
|
||||||
let os = |v: &Option<String>| -> String {
|
let os = |v: &Option<String>| -> String {
|
||||||
@@ -460,9 +478,10 @@ pub fn SpeciesDetail(slug: String) -> Element {
|
|||||||
|
|
||||||
let em = "\u{2014}";
|
let em = "\u{2014}";
|
||||||
|
|
||||||
|
let common_name = pick_name(¤t_lang, &s.name_de, &s.name_en, &s.name_scientific);
|
||||||
let name_en = os(&s.name_en);
|
let name_en = os(&s.name_en);
|
||||||
let name_de = os(&s.name_de);
|
let name_de = os(&s.name_de);
|
||||||
let desc = os(&s.description);
|
let desc = pick_desc(¤t_lang, &s.description_de, &s.description_en, &s.description);
|
||||||
|
|
||||||
// Uses
|
// Uses
|
||||||
let food = os(&s.food_uses);
|
let food = os(&s.food_uses);
|
||||||
@@ -511,6 +530,9 @@ pub fn SpeciesDetail(slug: String) -> Element {
|
|||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
h1 { em { "{s.name_scientific}" } }
|
h1 { em { "{s.name_scientific}" } }
|
||||||
|
if common_name != s.name_scientific {
|
||||||
|
p { class: "name-common", "{common_name}" }
|
||||||
|
}
|
||||||
|
|
||||||
div { class: "detail-row",
|
div { class: "detail-row",
|
||||||
// === LEFT COLUMN ===
|
// === LEFT COLUMN ===
|
||||||
@@ -525,6 +547,10 @@ pub fn SpeciesDetail(slug: String) -> Element {
|
|||||||
th { "Scientific Name" }
|
th { "Scientific Name" }
|
||||||
td { em { "{s.name_scientific}" } }
|
td { em { "{s.name_scientific}" } }
|
||||||
}
|
}
|
||||||
|
tr {
|
||||||
|
th { "Common Name" }
|
||||||
|
td { "{common_name}" }
|
||||||
|
}
|
||||||
tr {
|
tr {
|
||||||
th { "Name EN" }
|
th { "Name EN" }
|
||||||
td { class: if name_en == em { "placeholder" } else { "" }, "{name_en}" }
|
td { class: if name_en == em { "placeholder" } else { "" }, "{name_en}" }
|
||||||
@@ -894,6 +920,7 @@ pub fn SpeciesDetail(slug: String) -> Element {
|
|||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn CultivarListForSpecies(species_slug: String) -> Element {
|
fn CultivarListForSpecies(species_slug: String) -> Element {
|
||||||
|
let lang = use_context::<Lang>().0;
|
||||||
let slug = species_slug.clone();
|
let slug = species_slug.clone();
|
||||||
let cultivars = use_resource(move || {
|
let cultivars = use_resource(move || {
|
||||||
let s = slug.clone();
|
let s = slug.clone();
|
||||||
@@ -911,15 +938,21 @@ fn CultivarListForSpecies(species_slug: String) -> Element {
|
|||||||
rsx! {
|
rsx! {
|
||||||
div { class: "card-grid",
|
div { class: "card-grid",
|
||||||
for c in data.data.iter() {
|
for c in data.data.iter() {
|
||||||
div { class: "plant-card",
|
{
|
||||||
Link { to: Route::CultivarDetail { slug: c.slug.clone() },
|
let cv_common = pick_name(&lang.read(), &c.name_de, &c.name_en, &c.name);
|
||||||
strong { "{c.name}" }
|
let show_common = if cv_common == c.name { None } else { Some(cv_common) };
|
||||||
}
|
rsx! {
|
||||||
if let Some(ref en) = c.name_en {
|
div { class: "plant-card",
|
||||||
p { class: "card-common", "{en}" }
|
Link { to: Route::CultivarDetail { slug: c.slug.clone() },
|
||||||
}
|
strong { "{c.name}" }
|
||||||
if c.is_organic {
|
}
|
||||||
span { class: "badge organic", "Organic" }
|
if let Some(ref common) = show_common {
|
||||||
|
p { class: "card-common", "{common}" }
|
||||||
|
}
|
||||||
|
if c.is_organic {
|
||||||
|
span { class: "badge organic", "Organic" }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ pub struct Species {
|
|||||||
pub name_en: Option<String>,
|
pub name_en: Option<String>,
|
||||||
pub name_de: Option<String>,
|
pub name_de: Option<String>,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
pub description_en: Option<String>,
|
||||||
|
pub description_de: Option<String>,
|
||||||
pub soil_moisture: Option<String>,
|
pub soil_moisture: Option<String>,
|
||||||
pub ph_min: Option<f64>,
|
pub ph_min: Option<f64>,
|
||||||
pub ph_max: Option<f64>,
|
pub ph_max: Option<f64>,
|
||||||
@@ -89,6 +91,8 @@ pub struct Cultivar {
|
|||||||
pub name_de: Option<String>,
|
pub name_de: Option<String>,
|
||||||
pub name_scientific: Option<String>,
|
pub name_scientific: Option<String>,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
pub description_en: Option<String>,
|
||||||
|
pub description_de: Option<String>,
|
||||||
pub is_organic: bool,
|
pub is_organic: bool,
|
||||||
pub perennial: bool,
|
pub perennial: bool,
|
||||||
pub growing_time_days: Option<i32>,
|
pub growing_time_days: Option<i32>,
|
||||||
|
|||||||
Reference in New Issue
Block a user