From b77516b9f5f136c33b35e94ff9f00d0d2c41b29e Mon Sep 17 00:00:00 2001 From: Nick Szaunig Date: Sat, 26 Sep 2026 05:57:45 +0200 Subject: [PATCH] feat(ui): Startseite lebt - fliegende Biene, Imker-Portraet, bewegte Fotos - Eine gezeichnete Biene fliegt beim Scrollen eine geschwungene Bahn ueber "Aktuelle Ernte" und zieht eine gepunktete Honigspur hinter sich her; Fluegelschlag und leichtes Schweben per CSS (components/bee.tsx). - Neuer Abschnitt nach der Philosophie: Portraet des Imkers in Wabenform, die Punkte des Wabenrings wandern an der Kante entlang, eine Biene umkreist das Bild. Daneben der eigene Satz von der Imkerei-Seite als Zitat - kein neuer Text. - Herkunft-, Region- und Firmenkunden-Foto bewegen sich beim Scrollen leicht mit (Zoom und Parallax, nur transform; components/parallax-media). Alles rein dekorativ (aria-hidden) und mit Pfad fuer reduzierte Bewegung: die Biene sitzt dann still, Ring und Fotos stehen. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/app/globals.css | 43 ++++++++++ src/app/page.tsx | 2 + src/components/bee.tsx | 129 +++++++++++++++++++++++++++++ src/components/home-sections-a.tsx | 104 ++++++++++++++++++++--- src/components/home-sections-b.tsx | 39 +++++---- src/components/parallax-media.tsx | 27 ++++++ 6 files changed, 317 insertions(+), 27 deletions(-) create mode 100644 src/components/bee.tsx create mode 100644 src/components/parallax-media.tsx diff --git a/src/app/globals.css b/src/app/globals.css index 54b5eb8..ef4cc0f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -305,6 +305,49 @@ textarea::placeholder { } } +/* Biene (components/bee.tsx): Fluegelschlag, leichtes Schweben im Flug und + die Runde um das Imker-Portraet auf der Startseite. */ +@keyframes wh-flutter { + from { + transform: scaleY(1); + } + to { + transform: scaleY(0.35); + } +} + +@keyframes wh-hover { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-5px); + } +} + +@keyframes wh-orbit { + to { + transform: rotate(360deg); + } +} + +/* Punkte laufen entlang einer gestrichelten Linie (Wabenring um das Portraet). */ +@keyframes wh-march { + to { + stroke-dashoffset: -35; + } +} + +@media (prefers-reduced-motion: reduce) { + .motion-flutter, + .motion-hover, + .motion-orbit, + .motion-march { + animation: none !important; + } +} + /* --------------------------------------------------------------------------- Admin-Arbeitsbereich (components/admin-shell.tsx): einheitliche Details fuer alle Admin-Seiten zentral statt in jeder der rund 20 Seiten einzeln. diff --git a/src/app/page.tsx b/src/app/page.tsx index c2368cf..0b9e40d 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Hero } from "@/components/hero"; import { + BeekeeperNote, CurrentHarvest, OriginStory, Philosophy, @@ -57,6 +58,7 @@ export default async function HomePage() { + {spotlightProduct ? : null} diff --git a/src/components/bee.tsx b/src/components/bee.tsx new file mode 100644 index 0000000..4a54d0b --- /dev/null +++ b/src/components/bee.tsx @@ -0,0 +1,129 @@ +"use client"; + +import { useCallback, useEffect, useId, useRef } from "react"; +import { motion, useMotionValueEvent, useReducedMotion, useScroll, useSpring } from "motion/react"; + +/** + * Gezeichnete Biene (Blick nach rechts) im Strich der uebrigen Marken-Grafik. + * Die Fluegel schlagen per CSS (wh-flutter); bei reduzierter Bewegung stehen + * sie still (.motion-flutter, globals.css). + */ +export function BeeGlyph({ className = "h-8 w-10" }: { className?: string }) { + const clipId = `bee-body-${useId().replace(/:/g, "")}`; + return ( + + ); +} + +// Flugbahn im Koordinatenraum 1000 x 200: kommt links herein, schlaegt zwei +// Boegen und verlaesst den Abschnitt rechts oben. +const FLIGHT_PATH = "M -40 150 C 120 40, 260 190, 430 100 S 700 10, 820 120 S 980 90, 1060 30"; +const VIEW_W = 1000; +const VIEW_H = 200; +// Wo die Biene bei reduzierter Bewegung ruhig sitzt (Anteil der Bahn). +const RESTING_POINT = 0.62; + +/** + * Eine Biene fliegt beim Scrollen eine geschwungene Bahn ueber den Abschnitt + * und zieht eine gepunktete Honigspur hinter sich her. Rein dekorativ + * (aria-hidden, pointer-events-none). Bei reduzierter Bewegung sitzt sie + * still an einer Stelle, die Spur ist bis dorthin sichtbar. + */ +export function BeeFlight({ className = "" }: { className?: string }) { + const boxRef = useRef(null); + const pathRef = useRef(null); + const beeRef = useRef(null); + const maskId = `bee-trail-${useId().replace(/:/g, "")}`; + const reduceMotion = useReducedMotion(); + + const { scrollYProgress } = useScroll({ target: boxRef, offset: ["start end", "end start"] }); + // Gefedert: die Biene folgt dem Scrollen mit etwas Traegheit, statt ruckartig zu springen. + const progress = useSpring(scrollYProgress, { stiffness: 55, damping: 18, mass: 0.7 }); + + const placeBee = useCallback((p: number) => { + const path = pathRef.current; + const bee = beeRef.current; + const box = boxRef.current; + if (!path || !bee || !box) return; + const length = path.getTotalLength(); + const at = Math.min(1, Math.max(0, p)) * length; + const point = path.getPointAtLength(at); + const ahead = path.getPointAtLength(Math.min(length, at + 2)); + const sx = box.clientWidth / VIEW_W; + const sy = box.clientHeight / VIEW_H; + const angle = (Math.atan2((ahead.y - point.y) * sy, (ahead.x - point.x) * sx) * 180) / Math.PI; + bee.style.transform = `translate(${point.x * sx}px, ${point.y * sy}px) translate(-50%, -50%) rotate(${angle}deg)`; + }, []); + + useMotionValueEvent(progress, "change", (p) => { + if (!reduceMotion) placeBee(p); + }); + + useEffect(() => { + const place = () => placeBee(reduceMotion ? RESTING_POINT : progress.get()); + place(); + window.addEventListener("resize", place); + return () => window.removeEventListener("resize", place); + }, [placeBee, progress, reduceMotion]); + + return ( + + ); +} diff --git a/src/components/home-sections-a.tsx b/src/components/home-sections-a.tsx index 043b4bd..845a95e 100644 --- a/src/components/home-sections-a.tsx +++ b/src/components/home-sections-a.tsx @@ -2,6 +2,9 @@ import { Container, SectionHeading, LinkButton } from "@/components/ui"; import { HoneycombPattern, HexMark } from "@/components/honeycomb"; import { ScrollReveal } from "@/components/scroll-reveal"; import { ContentImage } from "@/components/content-image"; +import { BeeFlight, BeeGlyph } from "@/components/bee"; +import { ParallaxMedia } from "@/components/parallax-media"; +import { getCompanyInfo } from "@/lib/company-info"; import { ProductCard, TasteScale, AddToCartButton } from "@/components/product-card"; import { formatPrice, formatDate } from "@/lib/format"; import { getLocale } from "@/i18n/locale"; @@ -22,7 +25,11 @@ export async function CurrentHarvest({ // Dreierraster. const isFew = products.length <= 2; return ( -
+ // overflow-x-clip statt overflow-hidden: die Biene verlaesst den Abschnitt + // rechts, ohne eine Scrollleiste zu erzeugen — und die klebende + // Ueberschrift (sticky) funktioniert weiter. +
+
@@ -68,15 +75,17 @@ export async function OriginStory() { // Fensterrand, der Text steht daneben — kein Rahmen, keine Karte.
- + + +
@@ -130,6 +139,81 @@ export async function Philosophy() { ); } +/** + * Das Gesicht hinter dem Honig: Portraet des Imkers in Wabenform, ein + * langsam drehender Wabenring und eine Biene, die das Bild umkreist. + * Daneben sein eigener Satz von der Imkerei-Seite — kein neuer Text, nur + * hierher geholt, damit die Leidenschaft schon auf der Startseite spricht. + */ +export async function BeekeeperNote() { + const locale = await getLocale(); + const t = getDictionary(locale); + const [content, company] = await Promise.all([getContentValues(["imkerei.image"], locale), getCompanyInfo()]); + const quote = t.imkereiPage.paragraphs[0]; + return ( +
+ + + +
+ {/* Wabenring deckungsgleich zum Portraet; die Punkte wandern + langsam an der Kante entlang, wie Bienen auf ihrem Weg. */} + +
+ +
+ {/* Die Biene umkreist das Portraet */} + +
+
+ +
+

+ + {quote} + +

+
+ + {company.name} · {company.location} +
+
+ + {t.home.regionalSection.ctaAbout} + +
+
+
+ ); +} + type HoneySpotlightProduct = ProductWithRelations & { harvest: NonNullable; batch: NonNullable; diff --git a/src/components/home-sections-b.tsx b/src/components/home-sections-b.tsx index 95351a6..a959f75 100644 --- a/src/components/home-sections-b.tsx +++ b/src/components/home-sections-b.tsx @@ -3,6 +3,7 @@ import { ProcessPath } from "@/components/process-path"; import { HoneycombPattern, HexMark } from "@/components/honeycomb"; import { ScrollReveal } from "@/components/scroll-reveal"; import { ContentImage } from "@/components/content-image"; +import { ParallaxMedia } from "@/components/parallax-media"; import { StarRating } from "@/components/star-rating"; import { fetchGoogleReviews } from "@/lib/google-reviews"; import { getLocale } from "@/i18n/locale"; @@ -47,14 +48,16 @@ export async function RegionalSection() { {/* Breites Bild, der Text legt sich als Tafel darueber — Tiefe statt eines weiteren Bild-neben-Text-Wechsels. */} - + + +
- + + +
diff --git a/src/components/parallax-media.tsx b/src/components/parallax-media.tsx new file mode 100644 index 0000000..4c0d37b --- /dev/null +++ b/src/components/parallax-media.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { useRef, type ReactNode } from "react"; +import { motion, useReducedMotion, useScroll, useTransform } from "motion/react"; + +/** + * Foto, das sich beim Scrollen leicht mitbewegt: es kommt etwas vergroessert + * herein, setzt sich beim Durchscrollen und wandert dabei ein wenig gegen die + * Scrollrichtung — die Aufnahme wirkt wie ein Blick durch ein Fenster statt + * wie ein aufgeklebtes Bild. Nur transform (scale/translate), kein Layout. + * Bei reduzierter Bewegung steht das Bild still. + */ +export function ParallaxMedia({ children, className = "" }: { children: ReactNode; className?: string }) { + const ref = useRef(null); + const reduceMotion = useReducedMotion(); + const { scrollYProgress } = useScroll({ target: ref, offset: ["start end", "end start"] }); + const scale = useTransform(scrollYProgress, [0, 0.5, 1], [1.14, 1.05, 1]); + const y = useTransform(scrollYProgress, [0, 1], ["-4%", "4%"]); + + return ( +
+ + {children} + +
+ ); +}