226 lines
8.5 KiB
Rust
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>
|
|
}
|
|
}
|