Files
herbapi/herbapi-ui/src/pages/home.rs
T
florian.berthold 00e26b3a84 Add companion planting section to species detail page and redesign home page
Species detail now shows a "Companion Plants" card (left column, after
Ecology) with beneficial/antagonistic sub-lists. Each entry links to
the companion species and shows the mechanism.

Home page gains stats cards, quick-filter buttons, and a hero section.
Species list supports URL query params for quick-filter links.
New /api/v1/stats endpoint provides database counts.

i18n keys added for DE/EN: card.companion_plants, companions.beneficial_for,
companions.antagonistic_for, stat.*, filter.*, home.*.
2026-03-16 11:18:10 +01:00

123 lines
5.2 KiB
Rust

use dioxus::prelude::*;
use crate::api;
use crate::app::{Lang, Route};
use crate::components::plant_card::PlantCard;
use crate::i18n::{pick_name, t};
#[component]
pub fn Home() -> Element {
let lang = use_context::<Lang>().0;
let mut search_query = use_signal(|| String::new());
let species = use_resource(|| async { api::list_species(1, 8, None, None).await });
let stats = use_resource(|| async { api::get_stats().await });
let l = lang.read().clone();
rsx! {
div { class: "page home-page",
// Hero section
div { class: "hero",
h1 { class: "hero-title", "HerbAPI" }
p { class: "hero-subtitle", "{t(&l, \"subtitle\")}" }
}
// Stats cards
div { class: "stats-row",
match &*stats.read() {
None => rsx! {
for _ in 0..6 {
div { class: "stat-card stat-card-loading",
span { class: "stat-value", "\u{2014}" }
span { class: "stat-label", "\u{2026}" }
}
}
},
Some(Err(_)) => rsx! {},
Some(Ok(s)) => rsx! {
StatCard { value: s.families, label: t(&l, "stat.families"), icon: "\u{1fab4}" }
StatCard { value: s.species, label: t(&l, "stat.species"), icon: "\u{1f33f}" }
StatCard { value: s.cultivars, label: t(&l, "stat.cultivars"), icon: "\u{1f331}" }
StatCard { value: s.suppliers, label: t(&l, "stat.suppliers"), icon: "\u{1f6d2}" }
StatCard { value: s.companions, label: t(&l, "stat.companions"), icon: "\u{1f91d}" }
StatCard { value: s.images, label: t(&l, "stat.images"), icon: "\u{1f5bc}\u{fe0f}" }
},
}
}
// Search bar
div { class: "search-bar",
input {
r#type: "text",
placeholder: "{t(&l, \"search.placeholder\")}",
value: "{search_query}",
oninput: move |e| search_query.set(e.value()),
onkeydown: move |e| {
if e.key() == Key::Enter {
let nav = navigator();
nav.push(Route::SearchPage {});
}
},
}
}
// Quick filters
h2 { "{t(&l, \"home.quick_filters\")}" }
div { class: "quick-filters",
a { href: "/species?nitrogen_fixer=true", class: "quick-filter-btn qf-nitrogen",
span { class: "qf-icon", "\u{2699}\u{fe0f}" }
span { "{t(&l, \"filter.nitrogen_fixers\")}" }
}
a { href: "/species?min_nectar=3", class: "quick-filter-btn qf-bee",
span { class: "qf-icon", "\u{1f41d}" }
span { "{t(&l, \"filter.bee_plants\")}" }
}
a { href: "/species?plant_layer=ground_cover", class: "quick-filter-btn qf-ground",
span { class: "qf-icon", "\u{1f343}" }
span { "{t(&l, \"filter.ground_cover\")}" }
}
a { href: "/species?plant_layer=canopy", class: "quick-filter-btn qf-tree",
span { class: "qf-icon", "\u{1f333}" }
span { "{t(&l, \"filter.fruit_trees\")}" }
}
}
// Featured species
h2 { "{t(&l, \"home.featured_species\")}" }
match &*species.read() {
None => rsx! { p { "{t(&l, \"loading\")}" } },
Some(Err(e)) => rsx! { p { class: "error", "Error: {e}" } },
Some(Ok(data)) => rsx! {
div { class: "card-grid",
for s in data.data.iter().take(8) {
{
let card_common = pick_name(&lang.read(), &s.name_de, &s.name_en, &s.name_scientific);
let show_common = if card_common == s.name_scientific { None } else { Some(card_common) };
rsx! {
PlantCard {
key: "{s.id}",
slug: s.slug.clone(),
name: s.name_scientific.clone(),
name_common: show_common,
entity_type: "species".to_string(),
}
}
}
}
}
},
}
}
}
}
#[component]
fn StatCard(value: i64, label: &'static str, icon: &'static str) -> Element {
rsx! {
div { class: "stat-card",
span { class: "stat-icon", "{icon}" }
span { class: "stat-value", "{value}" }
span { class: "stat-label", "{label}" }
}
}
}