- 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]>
186 lines
8.3 KiB
TypeScript
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>
|
|
);
|
|
}
|