use super::layout::into_node;
use super::render_child;
use crate::i18n::Text;
use crate::render::{Render, escape};
use crate::screen::{Component, Node};
use serde_json::{Value as Json, json};
into_node!(Hero);
pub const MAX_HERO_SLIDES: usize = 10;
pub const HERO_SECONDS: (u8, u8) = (4, 30);
struct Slide {
title: Text,
text: Option<Text>,
image: Option<String>,
actions: usize,
}
pub struct Hero {
slides: Vec<Slide>,
actions: Vec<Node>,
rotate: Option<u8>,
}
impl Hero {
pub fn new(title: impl Into<Text>) -> Self {
Hero {
slides: vec![Slide {
title: title.into(),
text: None,
image: None,
actions: 0,
}],
actions: Vec::new(),
rotate: None,
}
}
pub fn slide(mut self, title: impl Into<Text>) -> Self {
if self.slides.len() < MAX_HERO_SLIDES {
self.slides.push(Slide {
title: title.into(),
text: None,
image: None,
actions: 0,
});
}
self
}
fn last(&mut self) -> &mut Slide {
let at = self.slides.len() - 1;
&mut self.slides[at]
}
pub fn text(mut self, text: impl Into<Text>) -> Self {
self.last().text = Some(text.into());
self
}
pub fn image(mut self, src: impl Into<String>) -> Self {
self.last().image = Some(src.into());
self
}
pub fn action(mut self, node: impl Into<Node>) -> Self {
self.last().actions += 1;
self.actions.push(node.into());
self
}
pub fn rotate(mut self, seconds: u8) -> Self {
self.rotate = Some(seconds.clamp(HERO_SECONDS.0, HERO_SECONDS.1));
self
}
}
impl Component for Hero {
fn render(&self, out: &mut String, r: &Render) {
let t = |key: &str| r.text(&Text::key(format!("zeyn.ui.hero.{key}")));
let id = super::dom_id(r, "hero");
let count = self.slides.len();
let many = count > 1;
let policy = super::embed::policy(r);
let mut slides = String::new();
let mut index = 0;
for (i, slide) in self.slides.iter().enumerate() {
let image = slide
.image
.as_deref()
.filter(|src| policy.allows_image(src))
.map(|src| {
let load = if i == 0 {
r#" fetchpriority="high""#
} else {
r#" loading="lazy""#
};
format!(
r#"<img class="z-hero-img" src="{}" alt=""{load} decoding="async">"#,
escape(src)
)
})
.unwrap_or_default();
let group = if many {
let name = r.text(
&Text::key("zeyn.ui.hero.slide_of")
.arg("n", (i + 1).to_string())
.arg("count", count.to_string()),
);
format!(
r#" role="group" aria-roledescription="{}" aria-label="{name}""#,
t("slide")
)
} else {
String::new()
};
let hidden = if i > 0 { " hidden" } else { "" };
slides.push_str(&format!(
r#"<div class="z-hero-slide" id="{id}-{i}"{group}{hidden}>{image}<div class="z-hero-body"><h2 class="z-hero-title">{}</h2>"#,
r.text(&slide.title)
));
if let Some(text) = &slide.text {
slides.push_str(&format!(r#"<p class="z-hero-text">{}</p>"#, r.text(text)));
}
if slide.actions > 0 {
slides.push_str(r#"<div class="z-hero-actions">"#);
for node in &self.actions[index..index + slide.actions] {
render_child(&mut slides, r, index, node);
index += 1;
}
slides.push_str("</div>");
}
slides.push_str("</div></div>");
}
if !many {
out.push_str(&format!(r#"<section class="z-hero">{slides}</section>"#));
return;
}
let rotate = self
.rotate
.map(|s| format!(r#" data-rotate="{s}""#))
.unwrap_or_default();
out.push_str(&format!(
r#"<section class="z-hero z-hero-slides" aria-roledescription="{}" aria-label="{}" data-zeyn-hero{rotate}><div class="z-hero-controls" hidden>"#,
t("carousel"),
r.text(&self.slides[0].title)
));
if self.rotate.is_some() {
out.push_str(&format!(
r#"<button type="button" class="z-hero-pause" data-zeyn-hero-pause data-text-pause="{0}" data-text-play="{1}" aria-label="{1}"><span class="z-hero-pause-icon" aria-hidden="true"></span></button>"#,
t("pause"),
t("play")
));
}
out.push_str(r#"<div class="z-hero-pick">"#);
for i in 0..count {
let name = r.text(
&Text::key("zeyn.ui.hero.show")
.arg("n", (i + 1).to_string())
.arg("count", count.to_string()),
);
let current = if i == 0 {
r#" aria-current="true""#
} else {
""
};
out.push_str(&format!(
r#"<button type="button" class="z-hero-dot" aria-controls="{id}-{i}" aria-label="{name}" data-zeyn-hero-show="{i}"{current}></button>"#
));
}
out.push_str(&format!(
r#"</div></div><div class="z-hero-track" aria-live="{}">{slides}</div></section>"#,
if self.rotate.is_some() {
"off"
} else {
"polite"
}
));
}
fn schema(&self) -> Json {
json!({
"type": "hero",
"rotate": self.rotate,
"slides": self
.slides
.iter()
.map(|s| json!({ "title": s.title, "text": s.text, "image": s.image, "actions": s.actions }))
.collect::<Vec<_>>(),
"children": self
.actions
.iter()
.map(Node::schema)
.collect::<Vec<_>>(),
})
}
fn children(&self) -> &[Node] {
&self.actions
}
}