Alternative.images: array of urls, rendered as a Swiper card deck
Deploy / deploy (push) Successful in 1m0s
Deploy / deploy (push) Successful in 1m0s
image: Option<String> becomes images: Vec<String> (nothing in live content used the old field). One url renders as the plain banner it was; several become a swipeable cards-effect deck via Swiper Element 12.2.0, vendored into public/ (MIT) like prosekit-editor.js/yes.js rather than pulled from a CDN - the bundle only loads on pages where some alternative actually declares more than one image. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
40694a8f3f
commit
3233d9a3db
Vendored
+14
File diff suppressed because one or more lines are too long
+34
-3
@@ -163,7 +163,13 @@ fn QuestionView(
|
|||||||
.into_any();
|
.into_any();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Swiper Element only registers its custom elements when its
|
||||||
|
// (vendored, ~180KB) script runs - loaded once per page, and only
|
||||||
|
// on pages where some alternative actually declares a deck.
|
||||||
|
let needs_swiper = question.alternatives.iter().any(|a| a.images.len() > 1);
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
|
{needs_swiper.then(|| view! { <leptos_meta::Script src="/swiper-element-bundle.min.js"/> })}
|
||||||
<Hero
|
<Hero
|
||||||
title=question.name.clone()
|
title=question.name.clone()
|
||||||
description=question.description.clone()
|
description=question.description.clone()
|
||||||
@@ -370,6 +376,33 @@ fn Hero(title: String, description: String, show_yes: bool) -> impl IntoView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An alternative's banner imagery: nothing, a plain image, or - for
|
||||||
|
/// several urls - a Swiper Element card deck. The `swiper-container`/
|
||||||
|
/// `swiper-slide` custom elements render inert server-side and upgrade
|
||||||
|
/// once the vendored bundle registers them (loaded per-page by
|
||||||
|
/// `QuestionView` only when some alternative actually needs it).
|
||||||
|
#[component]
|
||||||
|
fn AltImages(images: Vec<String>) -> impl IntoView {
|
||||||
|
match images.len() {
|
||||||
|
0 => ().into_any(),
|
||||||
|
1 => view! { <img class="alt-image" src=images[0].clone() alt="" /> }.into_any(),
|
||||||
|
_ => view! {
|
||||||
|
<swiper-container class="alt-image-deck" effect="cards" grab-cursor="true">
|
||||||
|
<For
|
||||||
|
each=move || images.clone()
|
||||||
|
key=|src| src.clone()
|
||||||
|
children=|src: String| view! {
|
||||||
|
<swiper-slide>
|
||||||
|
<img class="alt-image" src=src alt="" />
|
||||||
|
</swiper-slide>
|
||||||
|
}.into_any()
|
||||||
|
/>
|
||||||
|
</swiper-container>
|
||||||
|
}
|
||||||
|
.into_any(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn AlternativeCard(
|
fn AlternativeCard(
|
||||||
question_id: String,
|
question_id: String,
|
||||||
@@ -593,9 +626,7 @@ fn AlternativeCard(
|
|||||||
|
|
||||||
view! {
|
view! {
|
||||||
<section class="alt-card">
|
<section class="alt-card">
|
||||||
{alternative.image.clone().map(|src| view! {
|
<AltImages images=alternative.images.clone() />
|
||||||
<img class="alt-image" src=src alt="" />
|
|
||||||
})}
|
|
||||||
<h2>{alternative.name.clone()}</h2>
|
<h2>{alternative.name.clone()}</h2>
|
||||||
<p class="alt-description">{alternative.description.clone()}</p>
|
<p class="alt-description">{alternative.description.clone()}</p>
|
||||||
<div class="features">
|
<div class="features">
|
||||||
|
|||||||
+8
-6
@@ -60,13 +60,15 @@ pub struct Alternative {
|
|||||||
pub consequence: Vec<String>,
|
pub consequence: Vec<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub encouragements: Vec<String>,
|
pub encouragements: Vec<String>,
|
||||||
/// A banner image url, rendered above the description - purely
|
/// Banner image urls, rendered above the description - one renders
|
||||||
/// decorative, no upload/hosting mechanism of its own, just an
|
/// as a plain image, several as a swipeable card deck (Swiper
|
||||||
/// already-hosted url the browser fetches directly (unlike
|
/// Element, vendored in `public/`). Purely decorative, no
|
||||||
/// `ResourceSource::Url`, this is never fetched server-side, so it
|
/// upload/hosting mechanism of their own, just already-hosted urls
|
||||||
/// carries none of that variant's SSRF concern).
|
/// the browser fetches directly (unlike `ResourceSource::Url`,
|
||||||
|
/// never fetched server-side, so none of that variant's SSRF
|
||||||
|
/// concern).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub image: Option<String>,
|
pub images: Vec<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub features: Vec<Feature>,
|
pub features: Vec<Feature>,
|
||||||
/// Names a NATS KV bucket to also durably store this submission
|
/// Names a NATS KV bucket to also durably store this submission
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
let favicon_light_path = format!("{}/favicon-light.svg", leptos_options.site_root);
|
let favicon_light_path = format!("{}/favicon-light.svg", leptos_options.site_root);
|
||||||
let favicon_dark_path = format!("{}/favicon-dark.svg", leptos_options.site_root);
|
let favicon_dark_path = format!("{}/favicon-dark.svg", leptos_options.site_root);
|
||||||
let wordmark_path = format!("{}/wordmark.svg", leptos_options.site_root);
|
let wordmark_path = format!("{}/wordmark.svg", leptos_options.site_root);
|
||||||
|
let swiper_path = format!("{}/swiper-element-bundle.min.js", leptos_options.site_root);
|
||||||
let fonts_dir = format!("{}/fonts", leptos_options.site_root);
|
let fonts_dir = format!("{}/fonts", leptos_options.site_root);
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
@@ -139,6 +140,10 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
.route_service("/favicon-light.svg", ServeFile::new(favicon_light_path))
|
.route_service("/favicon-light.svg", ServeFile::new(favicon_light_path))
|
||||||
.route_service("/favicon-dark.svg", ServeFile::new(favicon_dark_path))
|
.route_service("/favicon-dark.svg", ServeFile::new(favicon_dark_path))
|
||||||
.route_service("/wordmark.svg", ServeFile::new(wordmark_path))
|
.route_service("/wordmark.svg", ServeFile::new(wordmark_path))
|
||||||
|
.route_service(
|
||||||
|
"/swiper-element-bundle.min.js",
|
||||||
|
ServeFile::new(swiper_path),
|
||||||
|
)
|
||||||
.leptos_routes_with_context(
|
.leptos_routes_with_context(
|
||||||
&state,
|
&state,
|
||||||
routes,
|
routes,
|
||||||
|
|||||||
@@ -270,6 +270,25 @@ main.not-found {
|
|||||||
margin-bottom: 1.1rem;
|
margin-bottom: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Sized down from full card width so the cards effect's fanned-out
|
||||||
|
neighbors stay inside the card - swiper positions slides absolutely,
|
||||||
|
so the container needs its own height. Before the bundle upgrades
|
||||||
|
the element (or without JS) slides stack as plain blocks; capping
|
||||||
|
the container height keeps that fallback from towering. */
|
||||||
|
.alt-image-deck {
|
||||||
|
display: block;
|
||||||
|
width: min(100%, 22rem);
|
||||||
|
height: 14rem;
|
||||||
|
margin: 0 auto 1.1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alt-image-deck .alt-image {
|
||||||
|
height: 100%;
|
||||||
|
max-height: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.alt-card h2 {
|
.alt-card h2 {
|
||||||
font-size: 1.3rem;
|
font-size: 1.3rem;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user