00e26b3a84
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.*.
123 lines
5.2 KiB
Rust
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}" }
|
|
}
|
|
}
|
|
}
|