mantra/crates/mantra-ui/src/pages/landing.rs

226 lines
8.5 KiB
Rust

//! `/` — two-door entry into the design-dna library.
//!
//! Shows all cycles (each with its works + themes columns) plus the
//! artifacts strip. v0.2: cycle 1 (philosophy) + cycle 2 (design
//! theory) + manifest + applied-principles.
use leptos::prelude::*;
use leptos_router::hooks::use_query_map;
use crate::api::{fetch_landing, ArtifactRef, CycleSnapshot, LandingData};
use crate::corpus::{Lang, Source, Theme};
use crate::pages::shared::LangToggle;
#[component]
pub fn Landing() -> impl IntoView {
let query = use_query_map();
let lang = Memo::new(move |_| {
Lang::from_query(query.read().get("lang").as_deref())
});
let data = Resource::new(
move || lang.get().as_str().to_string(),
|l| fetch_landing(l),
);
let hero_title = move || match lang.get() {
Lang::Ru => "дизайн днк",
Lang::En => "design dna",
};
let hero_subtitle = move || match lang.get() {
Lang::Ru => "философия · теория дизайна · дистилляция",
Lang::En => "philosophy · design theory · distillation",
};
let hero_question = move || match lang.get() {
Lang::Ru => "почему форма трогает человеческое сердце?",
Lang::En => "why does form move the human heart?",
};
let artifacts_label = move || match lang.get() {
Lang::Ru => "артефакты",
Lang::En => "artifacts",
};
let works_label = move || match lang.get() {
Lang::Ru => "работы",
Lang::En => "works",
};
let themes_label = move || match lang.get() {
Lang::Ru => "темы",
Lang::En => "themes",
};
view! {
<main class="landing">
<LangToggle current=lang/>
<header class="landing-head">
<h1 class="landing-title">{hero_title}</h1>
<p class="landing-subtitle">{hero_subtitle}</p>
<p class="landing-question">{hero_question}</p>
</header>
<Suspense fallback=move || view! { <p class="landing-loading">""</p> }>
{move || {
data.get().map(|res| {
let lang_val = lang.get();
match res {
Ok(LandingData { cycles, artifacts }) => {
view! {
{if !artifacts.is_empty() {
let art_label = artifacts_label();
view! {
<section class="landing-artifacts">
<h2 class="landing-section-label">{art_label}</h2>
<ul class="artifacts-list">
<For
each=move || artifacts.clone()
key=|a| a.slug.clone()
let:artifact
>
<ArtifactLine
artifact=artifact
lang=lang_val
/>
</For>
</ul>
</section>
}.into_any()
} else { ().into_any() }}
<For
each=move || cycles.clone()
key=|c| c.slug.clone()
let:cycle
>
<CycleSection
cycle=cycle
lang=lang_val
works_label=works_label()
themes_label=themes_label()
/>
</For>
}.into_any()
}
Err(e) => view! { <p class="err">{format!("{e}")}</p> }.into_any(),
}
})
}}
</Suspense>
</main>
}
}
#[component]
fn CycleSection(
cycle: CycleSnapshot,
lang: Lang,
works_label: &'static str,
themes_label: &'static str,
) -> impl IntoView {
let cycle_title = cycle.title.clone();
let cycle_subtitle = cycle.subtitle.clone();
let sources = cycle.sources.clone();
let themes = cycle.themes.clone();
let order = cycle.order.clone();
let mut by_slug: std::collections::HashMap<String, Source> =
std::collections::HashMap::with_capacity(sources.len());
for s in sources {
by_slug.insert(s.slug.clone(), s);
}
let sources_suffix = move |n: usize| match lang {
Lang::Ru => format!(" · {n} источников"),
Lang::En => format!(" · {n} sources"),
};
view! {
<section class="landing-cycle">
<header class="landing-cycle-head">
<h2 class="landing-cycle-title">{cycle_title}</h2>
<p class="landing-cycle-subtitle">{cycle_subtitle}</p>
</header>
<div class="landing-grid">
<div class="landing-col">
<h3 class="landing-col-label">{works_label}</h3>
<ul class="works-list">
<For
each=move || order.clone()
key=|slug| slug.clone()
let:slug
>
<WorkLine
source=by_slug.get(&slug).cloned()
lang=lang
slug=slug
/>
</For>
</ul>
</div>
<div class="landing-col">
<h3 class="landing-col-label">{themes_label}</h3>
<ul class="themes-list">
<For
each=move || themes.clone()
key=|t| t.slug.clone()
let:theme
>
<ThemeLine
theme=theme
lang=lang
suffix=sources_suffix.clone()
/>
</For>
</ul>
</div>
</div>
</section>
}
}
#[component]
fn WorkLine(source: Option<Source>, slug: String, lang: Lang) -> impl IntoView {
let href = format!("/source/{slug}?lang={}", lang.as_str());
match source {
None => view! { <li></li> }.into_any(),
Some(s) => view! {
<li class="work-line">
<a href=href>
<span class="work-author">{s.author}</span>
<span class="work-sep">" · "</span>
<span class="work-title">{s.title}</span>
<div class="work-claim">{s.core_claim}</div>
</a>
</li>
}.into_any(),
}
}
#[component]
fn ThemeLine<F>(theme: Theme, lang: Lang, suffix: F) -> impl IntoView
where
F: Fn(usize) -> String + 'static + Send + Sync + Clone,
{
let href = format!("/theme/{}?lang={}", theme.slug, lang.as_str());
let count = theme.contributing.len();
view! {
<li class="theme-line">
<a href=href>
<span class="theme-title">{theme.title}</span>
<span class="theme-count">{suffix(count)}</span>
</a>
</li>
}
}
#[component]
fn ArtifactLine(artifact: ArtifactRef, lang: Lang) -> impl IntoView {
let href = format!("/artifact/{}?lang={}", artifact.slug, lang.as_str());
view! {
<li class="artifact-line">
<a href=href>
<span class="artifact-title">{artifact.title}</span>
<span class="artifact-sep">" · "</span>
<span class="artifact-subtitle">{artifact.subtitle}</span>
</a>
</li>
}
}