Files
Wabenhain/src/components/hero.tsx
T
Nick SzaunigandClaude Opus 5.5 3de17ca070 feat(ui): Startseite mutiger gestaltet
- Hero: deutlich groessere Headline, langsam atmendes Bild, Wabenraster,
  Honigtropfen als Scroll-Hinweis, Hauptaktion in Honig
- "Sechs Schritte" als Weg: eine Honiglinie fuellt sich beim Scrollen und
  laesst die Waben der Schritte nacheinander aufleuchten (waagerecht ab lg,
  senkrecht auf dem Handy; reduzierte Bewegung: sofort gefuellt)
- Philosophie mit Waben statt Nummern, Verantwortung als geteiltes Layout
  mit mitlaufender Ueberschrift
- Bilder mit versetztem Honigrahmen, Wabenraster auf dunklen Flaechen,
  grosse Anfuehrungszeichen bei den Kundenstimmen
- Knopf-Varianten "inverted"/"accent" fuer dunkle Flaechen ersetzen
  !-Overrides, die den Fuell-Effekt ausgehebelt haetten
- Fix: Ueberschrift der Kundenstimmen war Waldgruen auf Waldgruen

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-25 07:48:08 +02:00

186 lines
8.3 KiB
TypeScript

"use client";
import { motion, useReducedMotion, type Variants } from "motion/react";
import { Container, LinkButton } from "@/components/ui";
import { ContentImage } from "@/components/content-image";
import { HoneycombPattern } from "@/components/honeycomb";
import { useI18n } from "@/i18n/i18n-provider";
const itemContainerVariants: Variants = {
hidden: {},
visible: { transition: { delayChildren: 0.3, staggerChildren: 0.12 } },
};
const itemVariants: Variants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.7, ease: "easeOut" } },
};
/**
* Startseiten-Hero mit grossflaechigem Bild und gestaffelter
* Einblend-Animation (Eyebrow, Headline, Text, Buttons) beim Laden.
*/
export function Hero({
intro,
footnote,
location,
image,
video,
}: {
intro: string;
footnote: string;
location: string;
image: string | null;
video: string | null;
}) {
const { t } = useI18n();
const shouldReduceMotion = useReducedMotion();
return (
// Bewusst KEINE Einblend-Animation auf der aeussersten Ebene: eine
// Section, die bei opacity 0 startet, wird erst sichtbar, wenn das
// JS-Bundle geladen und die Animation gelaufen ist. Das Hero-Bild ist
// aber das groesste Element im ersten Viewport (LCP) — es so lange
// zurueckzuhalten verschiebt den gemessenen Ladezeitpunkt der Seite um
// die gesamte Hydrationsdauer. Der gestaffelte Aufbau der Textspalte
// weiter unten bleibt unveraendert, der laeuft unabhaengig davon.
<section className="relative">
{video ? (
<div className="relative h-[82vh] min-h-[560px] w-full overflow-hidden">
{/* Standbild als eigene Ebene UNTER dem Video statt im
poster-Attribut. poster nimmt nur eine fertige URL und laedt die
hochgeladene Datei damit in voller gespeicherter Groesse (bis
MAX_IMAGE_DIMENSION, siehe lib/uploads.ts) — am Bildoptimierer
vorbei und ohne Ruecksicht auf das Geraet. Als ContentImage
laeuft dasselbe Bild durch next/image: passende Breite je
Bildschirm, WebP statt JPEG, und mit "priority" als erstes
angefordert. Das Video liegt darueber und ist transparent, bis es
eigene Bilddaten hat — der Uebergang ist deshalb nicht sichtbar. */}
<div className="motion-drift absolute inset-0 animate-[wh-drift_26s_ease-in-out_infinite_alternate]">
<ContentImage
src={image}
alt=""
tone="forest"
className="h-full w-full"
rounded={false}
sizes="100vw"
priority
/>
</div>
<video
src={video}
className="absolute inset-0 h-full w-full object-cover"
// Nutzer mit "Bewegung reduzieren" bekommen nur das Standbild
// (poster) statt der automatisch laufenden Videoschleife (WCAG
// 2.2.2 "Pause, Stop, Hide" fuer automatisch startende Bewegung).
autoPlay={!shouldReduceMotion}
muted
loop
playsInline
// Ohne Autoplay (Einstellung "Bewegung reduzieren") gibt es
// nichts abzuspielen — dann nur die Kopfdaten holen statt die
// ganze Datei. Mit Autoplay ignorieren Browser den Hinweis
// ohnehin und laden so viel, wie zum Starten noetig ist.
preload={shouldReduceMotion ? "metadata" : "auto"}
// JS-Fallback zusaetzlich zum loop-Attribut: manche Browser
// (u.a. aeltere iOS-Safari-Versionen) starten ein Video nach
// "ended" nicht zuverlaessig neu, wenn autoplay/muted/playsinline
// kombiniert sind. Erzwingt den Neustart explizit, damit die
// Dauerschleife garantiert nie sichtbar stehen bleibt.
onEnded={(event) => {
const el = event.currentTarget;
el.currentTime = 0;
void el.play().catch(() => {});
}}
/>
</div>
) : (
<div className="relative h-[82vh] min-h-[560px] w-full overflow-hidden">
<div className="motion-drift absolute inset-0 animate-[wh-drift_26s_ease-in-out_infinite_alternate]">
<ContentImage
src={image}
alt=""
tone="forest"
className="h-full w-full"
label="Bienenstand im Morgenlicht · Altenmünster"
rounded={false}
priority
/>
</div>
</div>
)}
{/* Gleichmaessige Abdunklung ueber die gesamte Flaeche — noetig, weil
redaktionell gepflegtes Bild-/Videomaterial (z.B. helle Etiketten,
Himmel) sonst je nach Motiv beliebig hell ausfallen und den Text
unlesbar machen kann. Der gerichtete Verlauf darunter verstaerkt das
gezielt dort, wo die Textspalte tatsaechlich sitzt (unten links). */}
<div className="absolute inset-0 bg-forest/25" />
{/* Wabenraster ueber der rechten Bildhaelfte — die Marke im ersten Blick. */}
<HoneycombPattern className="text-honey-pale/[0.12] hidden md:block" size={34} fade="right" />
{/* Senkrecht auf schmalen Schirmen, waagerecht ab md.
Der frueher hier stehende to-tr-Verlauf liess die Textspalte auf
rund 44 % Abdunklung liegen. Ueber hellem Motiv (Holz, Himmel)
ergab das 1,34:1 fuer die Ortszeile und 2,73:1 fuer die
Ueberschrift — beides weit unter WCAG AA. Lighthouse meldet das
nicht, weil es Text ueber Bildern nicht bewerten kann.
Waagerecht funktioniert nur ab md: dort endet die Textspalte bei
etwa 45 % Breite und liegt damit ganz im dunklen Teil, waehrend
die rechte Bildhaelfte unangetastet bleibt. Auf dem Handy laeuft
der Text ueber die volle Breite und geriete rechts in den
durchsichtigen Teil — deshalb dort senkrecht, was horizontal
gleichmaessig abdunkelt und den oberen Bildrand frei laesst. */}
<div className="absolute inset-0 flex items-center bg-gradient-to-t from-forest/85 via-forest/70 to-forest/10 md:bg-gradient-to-r md:from-forest/85 md:via-forest/70 md:to-transparent">
<Container>
<motion.div
className="max-w-2xl text-cream"
initial={shouldReduceMotion ? false : "hidden"}
animate="visible"
variants={itemContainerVariants}
>
<motion.p
data-reveal="" variants={itemVariants}
className="tracking-wide-caps text-xs text-honey-pale mb-4"
>
{location}
</motion.p>
<motion.h1
data-reveal=""
variants={itemVariants}
className="text-5xl sm:text-6xl md:text-7xl lg:text-8xl leading-[0.95] tracking-[-0.02em]"
>
{t.footer.tagline}
</motion.h1>
<motion.p data-reveal="" variants={itemVariants} className="mt-8 text-lg text-cream/85 max-w-md leading-relaxed">
{intro}
</motion.p>
<motion.div data-reveal="" variants={itemVariants} className="mt-10 flex flex-wrap gap-4">
<LinkButton href="/shop" variant="accent">
{t.home.hero.ctaPrimary}
</LinkButton>
<LinkButton
href="/imkerei"
variant="inverted"
>
{t.home.hero.ctaSecondary}
</LinkButton>
</motion.div>
<motion.p data-reveal="" variants={itemVariants} className="mt-8 text-xs text-cream/80 tracking-wide">
{footnote}
</motion.p>
</motion.div>
</Container>
</div>
{/* Scroll-Hinweis: ein Tropfen loest sich aus einer Wabe. Rein
dekorativ; bei reduzierter Bewegung steht er still. */}
<div aria-hidden="true" className="pointer-events-none absolute bottom-8 left-1/2 hidden -translate-x-1/2 flex-col items-center md:flex">
<svg viewBox="0 0 24 26" className="h-6 w-6 text-honey-pale/80" fill="none">
<polygon points="12,1 22.4,7 22.4,19 12,25 1.6,19 1.6,7" stroke="currentColor" strokeWidth="1.2" />
</svg>
<span className="motion-drip -mt-1 block h-2.5 w-2 rounded-b-full rounded-t-[40%] bg-honey animate-[wh-drip_2.6s_var(--ease-arrive)_infinite]" />
</div>
</section>
);
}