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]>
This commit is contained in:
1 parent
3cb639c154
commit
3de17ca070
9 files changed
+298
-103
No files matched your search
@@ -165,3 +165,41 @@ p {
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hero: das Bild "atmet" kaum merklich (langsamer Zoom hin und zurueck). */
|
||||||
|
@keyframes wh-drift {
|
||||||
|
from {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1.07) translate(-1%, -1%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scroll-Hinweis im Hero: ein Honigtropfen loest sich und faellt. */
|
||||||
|
@keyframes wh-drip {
|
||||||
|
0%,
|
||||||
|
15% {
|
||||||
|
transform: translateY(0) scale(0.6);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: translateY(26px) scale(0.9, 1.1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(34px) scale(0.4);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.motion-drift,
|
||||||
|
.motion-drip {
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,8 +49,8 @@ export function CookieConsentBanner() {
|
|||||||
</p>
|
</p>
|
||||||
<div className="flex flex-shrink-0 flex-wrap gap-3">
|
<div className="flex flex-shrink-0 flex-wrap gap-3">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="inverted"
|
||||||
className="!border-cream !text-cream hover:!bg-cream hover:!text-forest whitespace-nowrap"
|
className="whitespace-nowrap"
|
||||||
onClick={() => choose("essential")}
|
onClick={() => choose("essential")}
|
||||||
>
|
>
|
||||||
{t.cookieConsent.essentialOnly}
|
{t.cookieConsent.essentialOnly}
|
||||||
|
|||||||
+37
-17
@@ -3,6 +3,7 @@
|
|||||||
import { motion, useReducedMotion, type Variants } from "motion/react";
|
import { motion, useReducedMotion, type Variants } from "motion/react";
|
||||||
import { Container, LinkButton } from "@/components/ui";
|
import { Container, LinkButton } from "@/components/ui";
|
||||||
import { ContentImage } from "@/components/content-image";
|
import { ContentImage } from "@/components/content-image";
|
||||||
|
import { HoneycombPattern } from "@/components/honeycomb";
|
||||||
import { useI18n } from "@/i18n/i18n-provider";
|
import { useI18n } from "@/i18n/i18n-provider";
|
||||||
|
|
||||||
const itemContainerVariants: Variants = {
|
const itemContainerVariants: Variants = {
|
||||||
@@ -45,7 +46,7 @@ export function Hero({
|
|||||||
// weiter unten bleibt unveraendert, der laeuft unabhaengig davon.
|
// weiter unten bleibt unveraendert, der laeuft unabhaengig davon.
|
||||||
<section className="relative">
|
<section className="relative">
|
||||||
{video ? (
|
{video ? (
|
||||||
<div className="relative h-[78vh] min-h-[520px] w-full overflow-hidden">
|
<div className="relative h-[82vh] min-h-[560px] w-full overflow-hidden">
|
||||||
{/* Standbild als eigene Ebene UNTER dem Video statt im
|
{/* Standbild als eigene Ebene UNTER dem Video statt im
|
||||||
poster-Attribut. poster nimmt nur eine fertige URL und laedt die
|
poster-Attribut. poster nimmt nur eine fertige URL und laedt die
|
||||||
hochgeladene Datei damit in voller gespeicherter Groesse (bis
|
hochgeladene Datei damit in voller gespeicherter Groesse (bis
|
||||||
@@ -55,7 +56,7 @@ export function Hero({
|
|||||||
Bildschirm, WebP statt JPEG, und mit "priority" als erstes
|
Bildschirm, WebP statt JPEG, und mit "priority" als erstes
|
||||||
angefordert. Das Video liegt darueber und ist transparent, bis es
|
angefordert. Das Video liegt darueber und ist transparent, bis es
|
||||||
eigene Bilddaten hat — der Uebergang ist deshalb nicht sichtbar. */}
|
eigene Bilddaten hat — der Uebergang ist deshalb nicht sichtbar. */}
|
||||||
<div className="absolute inset-0">
|
<div className="motion-drift absolute inset-0 animate-[wh-drift_26s_ease-in-out_infinite_alternate]">
|
||||||
<ContentImage
|
<ContentImage
|
||||||
src={image}
|
src={image}
|
||||||
alt=""
|
alt=""
|
||||||
@@ -94,15 +95,19 @@ export function Hero({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<ContentImage
|
<div className="relative h-[82vh] min-h-[560px] w-full overflow-hidden">
|
||||||
src={image}
|
<div className="motion-drift absolute inset-0 animate-[wh-drift_26s_ease-in-out_infinite_alternate]">
|
||||||
alt=""
|
<ContentImage
|
||||||
tone="forest"
|
src={image}
|
||||||
className="h-[78vh] min-h-[520px] w-full"
|
alt=""
|
||||||
label="Bienenstand im Morgenlicht · Altenmünster"
|
tone="forest"
|
||||||
rounded={false}
|
className="h-full w-full"
|
||||||
priority
|
label="Bienenstand im Morgenlicht · Altenmünster"
|
||||||
/>
|
rounded={false}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Gleichmaessige Abdunklung ueber die gesamte Flaeche — noetig, weil
|
{/* Gleichmaessige Abdunklung ueber die gesamte Flaeche — noetig, weil
|
||||||
redaktionell gepflegtes Bild-/Videomaterial (z.B. helle Etiketten,
|
redaktionell gepflegtes Bild-/Videomaterial (z.B. helle Etiketten,
|
||||||
@@ -110,6 +115,8 @@ export function Hero({
|
|||||||
unlesbar machen kann. Der gerichtete Verlauf darunter verstaerkt das
|
unlesbar machen kann. Der gerichtete Verlauf darunter verstaerkt das
|
||||||
gezielt dort, wo die Textspalte tatsaechlich sitzt (unten links). */}
|
gezielt dort, wo die Textspalte tatsaechlich sitzt (unten links). */}
|
||||||
<div className="absolute inset-0 bg-forest/25" />
|
<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.
|
{/* Senkrecht auf schmalen Schirmen, waagerecht ab md.
|
||||||
|
|
||||||
Der frueher hier stehende to-tr-Verlauf liess die Textspalte auf
|
Der frueher hier stehende to-tr-Verlauf liess die Textspalte auf
|
||||||
@@ -127,7 +134,7 @@ export function Hero({
|
|||||||
<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">
|
<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>
|
<Container>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="max-w-xl text-cream"
|
className="max-w-2xl text-cream"
|
||||||
initial={shouldReduceMotion ? false : "hidden"}
|
initial={shouldReduceMotion ? false : "hidden"}
|
||||||
animate="visible"
|
animate="visible"
|
||||||
variants={itemContainerVariants}
|
variants={itemContainerVariants}
|
||||||
@@ -138,18 +145,23 @@ export function Hero({
|
|||||||
>
|
>
|
||||||
{location}
|
{location}
|
||||||
</motion.p>
|
</motion.p>
|
||||||
<motion.h1 data-reveal="" variants={itemVariants} className="text-4xl md:text-6xl leading-tight">
|
<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}
|
{t.footer.tagline}
|
||||||
</motion.h1>
|
</motion.h1>
|
||||||
<motion.p data-reveal="" variants={itemVariants} className="mt-6 text-cream/85 max-w-md leading-relaxed">
|
<motion.p data-reveal="" variants={itemVariants} className="mt-8 text-lg text-cream/85 max-w-md leading-relaxed">
|
||||||
{intro}
|
{intro}
|
||||||
</motion.p>
|
</motion.p>
|
||||||
<motion.div data-reveal="" variants={itemVariants} className="mt-10 flex flex-wrap gap-4">
|
<motion.div data-reveal="" variants={itemVariants} className="mt-10 flex flex-wrap gap-4">
|
||||||
<LinkButton href="/shop">{t.home.hero.ctaPrimary}</LinkButton>
|
<LinkButton href="/shop" variant="accent">
|
||||||
|
{t.home.hero.ctaPrimary}
|
||||||
|
</LinkButton>
|
||||||
<LinkButton
|
<LinkButton
|
||||||
href="/imkerei"
|
href="/imkerei"
|
||||||
variant="secondary"
|
variant="inverted"
|
||||||
className="!border-cream !text-cream hover:!bg-cream hover:!text-forest"
|
|
||||||
>
|
>
|
||||||
{t.home.hero.ctaSecondary}
|
{t.home.hero.ctaSecondary}
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
@@ -160,6 +172,14 @@ export function Hero({
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
</Container>
|
</Container>
|
||||||
</div>
|
</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>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Container, SectionHeading, LinkButton } from "@/components/ui";
|
import { Container, SectionHeading, LinkButton, FramedMedia } from "@/components/ui";
|
||||||
|
import { HoneycombPattern, HexMark } from "@/components/honeycomb";
|
||||||
import { ScrollReveal } from "@/components/scroll-reveal";
|
import { ScrollReveal } from "@/components/scroll-reveal";
|
||||||
import { ContentImage } from "@/components/content-image";
|
import { ContentImage } from "@/components/content-image";
|
||||||
import { ProductCard, TasteScale, AddToCartButton } from "@/components/product-card";
|
import { ProductCard, TasteScale, AddToCartButton } from "@/components/product-card";
|
||||||
@@ -55,17 +56,19 @@ export async function OriginStory() {
|
|||||||
);
|
);
|
||||||
const facts = parseListSimple(content["home.originStory.facts"]);
|
const facts = parseListSimple(content["home.originStory.facts"]);
|
||||||
return (
|
return (
|
||||||
<section className="py-24 bg-cream/60">
|
<section className="py-28 bg-cream/60">
|
||||||
<Container className="grid md:grid-cols-2 gap-12 items-center">
|
<Container className="grid md:grid-cols-2 gap-16 lg:gap-24 items-center">
|
||||||
<ScrollReveal>
|
<ScrollReveal>
|
||||||
<ContentImage
|
<FramedMedia>
|
||||||
src={content["home.originStory.image"] || null}
|
<ContentImage
|
||||||
alt=""
|
src={content["home.originStory.image"] || null}
|
||||||
tone="forest"
|
alt=""
|
||||||
className="aspect-[4/5] w-full"
|
tone="forest"
|
||||||
label={t.home.originStory.eyebrow}
|
className="aspect-[4/5] w-full"
|
||||||
sizes="(min-width: 768px) 50vw, 100vw"
|
label={t.home.originStory.eyebrow}
|
||||||
/>
|
sizes="(min-width: 768px) 50vw, 100vw"
|
||||||
|
/>
|
||||||
|
</FramedMedia>
|
||||||
</ScrollReveal>
|
</ScrollReveal>
|
||||||
<ScrollReveal delay={0.1}>
|
<ScrollReveal delay={0.1}>
|
||||||
<SectionHeading
|
<SectionHeading
|
||||||
@@ -74,9 +77,12 @@ export async function OriginStory() {
|
|||||||
/>
|
/>
|
||||||
<p className="mt-6 text-ink/75 leading-relaxed">{content["home.originStory.paragraph1"]}</p>
|
<p className="mt-6 text-ink/75 leading-relaxed">{content["home.originStory.paragraph1"]}</p>
|
||||||
<p className="mt-4 text-ink/75 leading-relaxed">{content["home.originStory.paragraph2"]}</p>
|
<p className="mt-4 text-ink/75 leading-relaxed">{content["home.originStory.paragraph2"]}</p>
|
||||||
<ul className="mt-6 space-y-1 text-sm text-ink/70">
|
<ul className="mt-8 grid gap-2.5 text-sm text-ink/75 sm:grid-cols-2">
|
||||||
{facts.map((fact) => (
|
{facts.map((fact) => (
|
||||||
<li key={fact}>{fact}</li>
|
<li key={fact} className="flex items-start gap-2.5">
|
||||||
|
<HexMark className="mt-1.5 h-2 w-2 flex-shrink-0 text-honey" />
|
||||||
|
{fact}
|
||||||
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<LinkButton href="/herkunft" variant="secondary" className="mt-8">
|
<LinkButton href="/herkunft" variant="secondary" className="mt-8">
|
||||||
@@ -108,11 +114,10 @@ export async function Philosophy() {
|
|||||||
{items.map((item, i) => (
|
{items.map((item, i) => (
|
||||||
<ScrollReveal key={item.title} delay={i * 0.08}>
|
<ScrollReveal key={item.title} delay={i * 0.08}>
|
||||||
<div className={`border-t border-ink/15 pt-6 ${i % 2 === 1 ? "lg:mt-10" : ""}`}>
|
<div className={`border-t border-ink/15 pt-6 ${i % 2 === 1 ? "lg:mt-10" : ""}`}>
|
||||||
<span className="font-serif text-3xl text-honey-dark">
|
{/* Wabe statt "01"-Nummer: die vier Punkte sind gleichrangig, keine Reihenfolge. */}
|
||||||
{String(i + 1).padStart(2, "0")}
|
<HexMark filled={i % 2 === 0} className="h-5 w-5 text-honey" />
|
||||||
</span>
|
<h3 className="mt-4 text-2xl text-forest">{item.title}</h3>
|
||||||
<h3 className="mt-3 text-lg text-forest">{item.title}</h3>
|
<p className="mt-3 text-sm text-ink/70 leading-relaxed">
|
||||||
<p className="mt-2 text-sm text-ink/65 leading-relaxed">
|
|
||||||
{item.text}
|
{item.text}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -133,8 +138,9 @@ export async function ProductSpotlight({ product }: { product: HoneySpotlightPro
|
|||||||
const locale = await getLocale();
|
const locale = await getLocale();
|
||||||
const t = getDictionary(locale);
|
const t = getDictionary(locale);
|
||||||
return (
|
return (
|
||||||
<section className="py-24 bg-forest text-cream">
|
<section className="relative overflow-hidden py-28 bg-forest text-cream">
|
||||||
<Container className="grid md:grid-cols-2 gap-14 items-center">
|
<HoneycombPattern className="text-honey-light/[0.08]" size={28} fade="right" />
|
||||||
|
<Container className="relative grid md:grid-cols-2 gap-14 lg:gap-24 items-center">
|
||||||
<ScrollReveal>
|
<ScrollReveal>
|
||||||
<ContentImage
|
<ContentImage
|
||||||
src={product.images[0] ?? null}
|
src={product.images[0] ?? null}
|
||||||
@@ -149,7 +155,7 @@ export async function ProductSpotlight({ product }: { product: HoneySpotlightPro
|
|||||||
<p className="tracking-wide-caps text-xs text-honey-light mb-3">
|
<p className="tracking-wide-caps text-xs text-honey-light mb-3">
|
||||||
{product.harvest.type} {product.harvest.year}
|
{product.harvest.type} {product.harvest.year}
|
||||||
</p>
|
</p>
|
||||||
<h2 className="text-3xl md:text-4xl">{product.name}</h2>
|
<h2 className="text-5xl md:text-6xl leading-[1] tracking-[-0.015em]">{product.name}</h2>
|
||||||
<p className="mt-4 text-cream/75 leading-relaxed">
|
<p className="mt-4 text-cream/75 leading-relaxed">
|
||||||
{t.home.productSpotlight.description}
|
{t.home.productSpotlight.description}
|
||||||
</p>
|
</p>
|
||||||
@@ -178,19 +184,20 @@ export async function ProductSpotlight({ product }: { product: HoneySpotlightPro
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-cream/65">{t.home.productSpotlight.price}</dt>
|
<dt className="text-cream/65">{t.home.productSpotlight.price}</dt>
|
||||||
<dd>{formatPrice(product.priceCents, product.currency, locale)}</dd>
|
<dd className="font-serif text-2xl text-cream tabular-nums">
|
||||||
|
{formatPrice(product.priceCents, product.currency, locale)}
|
||||||
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<div className="mt-10 flex flex-wrap gap-4">
|
<div className="mt-10 flex flex-wrap gap-4">
|
||||||
<LinkButton
|
<LinkButton
|
||||||
href={`/honig/${product.slug}`}
|
href={`/honig/${product.slug}`}
|
||||||
variant="secondary"
|
variant="inverted"
|
||||||
className="!border-cream !text-cream hover:!bg-cream hover:!text-forest"
|
|
||||||
>
|
>
|
||||||
{t.home.productSpotlight.cta}
|
{t.home.productSpotlight.cta}
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
<AddToCartButton product={product} />
|
<AddToCartButton product={product} variant="accent" />
|
||||||
</div>
|
</div>
|
||||||
</ScrollReveal>
|
</ScrollReveal>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { Container, SectionHeading, LinkButton } from "@/components/ui";
|
import { Container, SectionHeading, LinkButton, FramedMedia } from "@/components/ui";
|
||||||
|
import { ProcessPath } from "@/components/process-path";
|
||||||
|
import { HoneycombPattern, HexMark } from "@/components/honeycomb";
|
||||||
import { ScrollReveal } from "@/components/scroll-reveal";
|
import { ScrollReveal } from "@/components/scroll-reveal";
|
||||||
import { ContentImage } from "@/components/content-image";
|
import { ContentImage } from "@/components/content-image";
|
||||||
import { StarRating } from "@/components/star-rating";
|
import { StarRating } from "@/components/star-rating";
|
||||||
@@ -23,21 +25,7 @@ export async function ProcessTimeline() {
|
|||||||
className="mx-auto"
|
className="mx-auto"
|
||||||
/>
|
/>
|
||||||
</ScrollReveal>
|
</ScrollReveal>
|
||||||
<div className="mt-16 grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-8">
|
<ProcessPath steps={steps} />
|
||||||
{steps.map((step, i) => (
|
|
||||||
<ScrollReveal key={step.number} delay={i * 0.06}>
|
|
||||||
<div>
|
|
||||||
<span className="text-honey-dark text-sm tracking-wide-caps">
|
|
||||||
{step.number}
|
|
||||||
</span>
|
|
||||||
<h3 className="mt-2 text-lg text-forest">{step.title}</h3>
|
|
||||||
<p className="mt-2 text-xs text-ink/70 leading-relaxed">
|
|
||||||
{step.text}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</ScrollReveal>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Container>
|
</Container>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@@ -56,7 +44,7 @@ export async function RegionalSection() {
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<section className="py-24">
|
<section className="py-24">
|
||||||
<Container className="grid md:grid-cols-2 gap-12 items-center">
|
<Container className="grid md:grid-cols-2 gap-16 lg:gap-24 items-center">
|
||||||
<ScrollReveal className="order-2 md:order-1">
|
<ScrollReveal className="order-2 md:order-1">
|
||||||
<SectionHeading
|
<SectionHeading
|
||||||
eyebrow={t.home.regionalSection.eyebrow}
|
eyebrow={t.home.regionalSection.eyebrow}
|
||||||
@@ -74,14 +62,16 @@ export async function RegionalSection() {
|
|||||||
</div>
|
</div>
|
||||||
</ScrollReveal>
|
</ScrollReveal>
|
||||||
<ScrollReveal className="order-1 md:order-2" delay={0.1}>
|
<ScrollReveal className="order-1 md:order-2" delay={0.1}>
|
||||||
<ContentImage
|
<FramedMedia side="left">
|
||||||
src={content["home.regionalSection.image"] || null}
|
<ContentImage
|
||||||
alt=""
|
src={content["home.regionalSection.image"] || null}
|
||||||
tone="forest"
|
alt=""
|
||||||
className="aspect-[4/5] w-full"
|
tone="forest"
|
||||||
label={t.home.regionalSection.eyebrow}
|
className="aspect-[4/5] w-full"
|
||||||
sizes="(min-width: 768px) 50vw, 100vw"
|
label={t.home.regionalSection.eyebrow}
|
||||||
/>
|
sizes="(min-width: 768px) 50vw, 100vw"
|
||||||
|
/>
|
||||||
|
</FramedMedia>
|
||||||
</ScrollReveal>
|
</ScrollReveal>
|
||||||
</Container>
|
</Container>
|
||||||
</section>
|
</section>
|
||||||
@@ -94,26 +84,30 @@ export async function Sustainability() {
|
|||||||
const content = await getContentValues(["home.sustainability.items"], locale);
|
const content = await getContentValues(["home.sustainability.items"], locale);
|
||||||
const items = parseListTitled(content["home.sustainability.items"]);
|
const items = parseListTitled(content["home.sustainability.items"]);
|
||||||
return (
|
return (
|
||||||
<section className="py-24 bg-cream/60">
|
<section className="py-28 bg-cream/60">
|
||||||
<Container>
|
{/* Geteilt statt Viererreihe: die Ueberschrift laeuft mit, die Punkte
|
||||||
<ScrollReveal>
|
werden als ruhige Liste gelesen. */}
|
||||||
<SectionHeading
|
<Container className="grid gap-12 lg:grid-cols-12 lg:gap-16">
|
||||||
eyebrow={t.home.sustainability.eyebrow}
|
<ScrollReveal className="lg:col-span-5">
|
||||||
title={t.home.sustainability.title}
|
<div className="lg:sticky lg:top-32">
|
||||||
align="center"
|
<SectionHeading eyebrow={t.home.sustainability.eyebrow} title={t.home.sustainability.title} />
|
||||||
className="mx-auto"
|
</div>
|
||||||
/>
|
|
||||||
</ScrollReveal>
|
</ScrollReveal>
|
||||||
<div className="mt-14 grid sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
<ul className="divide-y divide-ink/10 border-y border-ink/10 lg:col-span-7">
|
||||||
{items.map((item, i) => (
|
{items.map((item, i) => (
|
||||||
<ScrollReveal key={item.title} delay={i * 0.08}>
|
<li key={item.title}>
|
||||||
<h3 className="text-lg text-forest">{item.title}</h3>
|
<ScrollReveal delay={i * 0.08}>
|
||||||
<p className="mt-2 text-sm text-ink/65 leading-relaxed">
|
<div className="group grid grid-cols-[auto_1fr] gap-x-5 py-7">
|
||||||
{item.text}
|
<HexMark className="mt-2 h-3 w-3 text-honey transition-transform duration-500 ease-[var(--ease-arrive)] group-hover:rotate-90" />
|
||||||
</p>
|
<div>
|
||||||
</ScrollReveal>
|
<h3 className="text-2xl text-forest">{item.title}</h3>
|
||||||
|
<p className="mt-2 max-w-[55ch] text-ink/70 leading-relaxed">{item.text}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollReveal>
|
||||||
|
</li>
|
||||||
))}
|
))}
|
||||||
</div>
|
</ul>
|
||||||
</Container>
|
</Container>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
@@ -128,16 +122,18 @@ export async function BusinessTeaser() {
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<section className="py-24">
|
<section className="py-24">
|
||||||
<Container className="grid md:grid-cols-2 gap-12 items-center">
|
<Container className="grid md:grid-cols-2 gap-16 lg:gap-24 items-center">
|
||||||
<ScrollReveal>
|
<ScrollReveal>
|
||||||
<ContentImage
|
<FramedMedia>
|
||||||
src={content["home.businessTeaser.image"] || null}
|
<ContentImage
|
||||||
alt=""
|
src={content["home.businessTeaser.image"] || null}
|
||||||
tone="honey"
|
alt=""
|
||||||
className="aspect-[4/5] w-full"
|
tone="honey"
|
||||||
label={t.home.businessTeaser.eyebrow}
|
className="aspect-[4/5] w-full"
|
||||||
sizes="(min-width: 768px) 50vw, 100vw"
|
label={t.home.businessTeaser.eyebrow}
|
||||||
/>
|
sizes="(min-width: 768px) 50vw, 100vw"
|
||||||
|
/>
|
||||||
|
</FramedMedia>
|
||||||
</ScrollReveal>
|
</ScrollReveal>
|
||||||
<ScrollReveal delay={0.1}>
|
<ScrollReveal delay={0.1}>
|
||||||
<SectionHeading
|
<SectionHeading
|
||||||
@@ -173,8 +169,9 @@ export async function Reviews() {
|
|||||||
: t.home.reviews.testimonials.map((r) => ({ ...r, rating: 5, authorUrl: null }));
|
: t.home.reviews.testimonials.map((r) => ({ ...r, rating: 5, authorUrl: null }));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="py-24 bg-forest text-cream">
|
<section className="relative overflow-hidden py-28 bg-forest text-cream">
|
||||||
<Container>
|
<HoneycombPattern className="text-honey-light/[0.07]" size={30} />
|
||||||
|
<Container className="relative">
|
||||||
<ScrollReveal>
|
<ScrollReveal>
|
||||||
<SectionHeading
|
<SectionHeading
|
||||||
eyebrow={t.home.reviews.eyebrow}
|
eyebrow={t.home.reviews.eyebrow}
|
||||||
@@ -192,10 +189,13 @@ export async function Reviews() {
|
|||||||
<div className="mt-14 grid md:grid-cols-3 gap-10">
|
<div className="mt-14 grid md:grid-cols-3 gap-10">
|
||||||
{items.slice(0, 6).map((review, i) => (
|
{items.slice(0, 6).map((review, i) => (
|
||||||
<ScrollReveal key={`${review.author}-${i}`} delay={i * 0.08}>
|
<ScrollReveal key={`${review.author}-${i}`} delay={i * 0.08}>
|
||||||
|
<span aria-hidden="true" className="block font-serif text-7xl leading-[0.6] text-honey-light/70">
|
||||||
|
„
|
||||||
|
</span>
|
||||||
<StarRating value={review.rating} />
|
<StarRating value={review.rating} />
|
||||||
<span className="sr-only">{t.home.reviews.starsOutOf5(review.rating)}</span>
|
<span className="sr-only">{t.home.reviews.starsOutOf5(review.rating)}</span>
|
||||||
<p className="mt-4 text-cream/85 leading-relaxed italic">
|
<p className="mt-4 font-serif text-xl text-cream/90 leading-relaxed italic">
|
||||||
„{review.quote}“
|
{review.quote}
|
||||||
</p>
|
</p>
|
||||||
{/* Zuordnung zur Quelle: die Nutzungsbedingungen der Places API
|
{/* Zuordnung zur Quelle: die Nutzungsbedingungen der Places API
|
||||||
verlangen, dass eine angezeigte Rezension zu Google
|
verlangen, dass eine angezeigte Rezension zu Google
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRef } from "react";
|
||||||
|
import { motion, useReducedMotion, useScroll, useSpring, useTransform, type MotionValue } from "motion/react";
|
||||||
|
|
||||||
|
interface Step {
|
||||||
|
number: string;
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Vom Bienenstock ins Glas" als Weg, den der Honig nimmt: eine Honiglinie
|
||||||
|
* fuellt sich im Takt des Scrollens und laesst die Waben der einzelnen
|
||||||
|
* Schritte der Reihe nach aufleuchten. Hier traegt die Bewegung Bedeutung —
|
||||||
|
* die Reihenfolge IST der Inhalt.
|
||||||
|
*
|
||||||
|
* Ab lg waagerecht (sechs Spalten), darunter senkrecht mit Linie links.
|
||||||
|
* Bei reduzierter Bewegung ist der Weg von Anfang an gefuellt.
|
||||||
|
*/
|
||||||
|
export function ProcessPath({ steps }: { steps: Step[] }) {
|
||||||
|
const ref = useRef<HTMLOListElement>(null);
|
||||||
|
const reduceMotion = useReducedMotion();
|
||||||
|
const { scrollYProgress } = useScroll({ target: ref, offset: ["start 85%", "end 55%"] });
|
||||||
|
// Gefedert statt direkt am Scrollwert: die Linie "fliesst" nach, wie Honig.
|
||||||
|
const smooth = useSpring(scrollYProgress, { stiffness: 90, damping: 22, mass: 0.6 });
|
||||||
|
const full = useTransform(smooth, () => 1);
|
||||||
|
const progress = reduceMotion ? full : smooth;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ol ref={ref} className="relative mt-16 grid gap-10 pl-10 lg:grid-cols-6 lg:gap-8 lg:pl-0 lg:pt-12">
|
||||||
|
{/* Leitung (Grundlinie) und Honigfuellung — waagerecht ab lg, sonst senkrecht. */}
|
||||||
|
<span aria-hidden="true" className="absolute left-[11px] top-2 bottom-2 w-px bg-ink/15 lg:hidden" />
|
||||||
|
<motion.span
|
||||||
|
aria-hidden="true"
|
||||||
|
style={{ scaleY: progress }}
|
||||||
|
className="absolute left-[10px] top-2 bottom-2 w-[3px] origin-top rounded-full bg-honey lg:hidden"
|
||||||
|
/>
|
||||||
|
<span aria-hidden="true" className="absolute left-3 right-[calc((100%_-_10rem)/6_-_12px)] top-[11px] hidden h-px bg-ink/15 lg:block" />
|
||||||
|
<motion.span
|
||||||
|
aria-hidden="true"
|
||||||
|
style={{ scaleX: progress }}
|
||||||
|
className="absolute left-3 right-[calc((100%_-_10rem)/6_-_12px)] top-[10px] hidden h-[3px] origin-left rounded-full bg-honey lg:block"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{steps.map((step, i) => (
|
||||||
|
<ProcessStep key={step.number} step={step} index={i} total={steps.length} progress={progress} />
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProcessStep({
|
||||||
|
step,
|
||||||
|
index,
|
||||||
|
total,
|
||||||
|
progress,
|
||||||
|
}: {
|
||||||
|
step: Step;
|
||||||
|
index: number;
|
||||||
|
total: number;
|
||||||
|
progress: MotionValue<number>;
|
||||||
|
}) {
|
||||||
|
// Die Wabe "entzuendet" sich, sobald die Linie ihre Position erreicht.
|
||||||
|
const at = total > 1 ? index / (total - 1) : 0;
|
||||||
|
const lit = useTransform(progress, [Math.max(0, at - 0.06), at + 0.02], [0, 1]);
|
||||||
|
const scale = useTransform(lit, [0, 1], [0.85, 1]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className="relative">
|
||||||
|
<motion.span
|
||||||
|
aria-hidden="true"
|
||||||
|
style={{ scale }}
|
||||||
|
className="absolute -left-10 top-0 flex h-6 w-6 items-center justify-center lg:-top-12 lg:left-0"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 26" className="h-6 w-6">
|
||||||
|
<polygon
|
||||||
|
points="12,1 22.4,7 22.4,19 12,25 1.6,19 1.6,7"
|
||||||
|
fill="var(--color-background)"
|
||||||
|
stroke="var(--color-honey-dark)"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
/>
|
||||||
|
<motion.polygon
|
||||||
|
points="12,4.5 19.4,8.75 19.4,17.25 12,21.5 4.6,17.25 4.6,8.75"
|
||||||
|
fill="var(--color-honey)"
|
||||||
|
style={{ opacity: lit }}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</motion.span>
|
||||||
|
<span className="font-serif text-sm text-honey-dark tabular-nums">{step.number}</span>
|
||||||
|
<h3 className="mt-1 text-xl text-forest">{step.title}</h3>
|
||||||
|
<p className="mt-2 text-sm text-ink/70 leading-relaxed">{step.text}</p>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -160,7 +160,7 @@ export function AddToCartButton({
|
|||||||
}: {
|
}: {
|
||||||
product: ProductWithRelations;
|
product: ProductWithRelations;
|
||||||
quantity?: number;
|
quantity?: number;
|
||||||
variant?: "primary" | "secondary" | "ghost";
|
variant?: "primary" | "secondary" | "ghost" | "inverted" | "accent";
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|||||||
@@ -419,8 +419,8 @@ export function Header({ companyName }: { companyName: string }) {
|
|||||||
<Container className="pb-10">
|
<Container className="pb-10">
|
||||||
<LinkButton
|
<LinkButton
|
||||||
href="/shop"
|
href="/shop"
|
||||||
variant="secondary"
|
variant="inverted"
|
||||||
className="!border-cream !text-cream hover:!bg-cream hover:!text-forest w-full"
|
className="w-full"
|
||||||
onClick={() => setMobileOpen(false)}
|
onClick={() => setMobileOpen(false)}
|
||||||
>
|
>
|
||||||
{t.nav.discoverHoney}
|
{t.nav.discoverHoney}
|
||||||
|
|||||||
+36
-1
@@ -18,7 +18,7 @@ export function Container({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type ButtonVariant = "primary" | "secondary" | "ghost";
|
type ButtonVariant = "primary" | "secondary" | "ghost" | "inverted" | "accent";
|
||||||
|
|
||||||
// Signatur-Hover: der Knopf fuellt sich von unten wie ein Glas — beim
|
// Signatur-Hover: der Knopf fuellt sich von unten wie ein Glas — beim
|
||||||
// Primaerknopf mit Honig, beim Sekundaerknopf mit Waldgruen. Umgesetzt als
|
// Primaerknopf mit Honig, beim Sekundaerknopf mit Waldgruen. Umgesetzt als
|
||||||
@@ -31,6 +31,11 @@ const buttonStyles: Record<ButtonVariant, string> = {
|
|||||||
primary: `${fillBase} bg-forest bg-[linear-gradient(var(--color-honey-dark),var(--color-honey-dark))] text-cream border border-forest hover:border-honey-dark`,
|
primary: `${fillBase} bg-forest bg-[linear-gradient(var(--color-honey-dark),var(--color-honey-dark))] text-cream border border-forest hover:border-honey-dark`,
|
||||||
secondary: `${fillBase} bg-transparent bg-[linear-gradient(var(--color-forest),var(--color-forest))] text-forest border border-forest hover:text-cream focus-visible:text-cream`,
|
secondary: `${fillBase} bg-transparent bg-[linear-gradient(var(--color-forest),var(--color-forest))] text-forest border border-forest hover:text-cream focus-visible:text-cream`,
|
||||||
ghost: "bg-transparent text-ink border border-ink/20 hover:border-ink hover:bg-ink/[0.03]",
|
ghost: "bg-transparent text-ink border border-ink/20 hover:border-ink hover:bg-ink/[0.03]",
|
||||||
|
// Fuer dunkle Flaechen (Hero, Waldgruen-Abschnitte, Menue): Creme-Rahmen,
|
||||||
|
// fuellt sich mit Creme, Text wechselt auf Waldgruen.
|
||||||
|
inverted: `${fillBase} bg-transparent bg-[linear-gradient(var(--color-cream),var(--color-cream))] text-cream border border-cream hover:text-forest focus-visible:text-forest`,
|
||||||
|
// Hauptaktion auf dunklen Flaechen, wo Waldgruen im Grund verschwinden wuerde.
|
||||||
|
accent: `${fillBase} bg-honey-dark bg-[linear-gradient(var(--color-cream),var(--color-cream))] text-cream border border-honey-dark hover:text-forest hover:border-cream focus-visible:text-forest`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttonBase =
|
const buttonBase =
|
||||||
@@ -129,3 +134,33 @@ export function SectionHeading({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bild mit versetztem Honigrahmen dahinter — gibt den Bildspalten der
|
||||||
|
* Startseite Tiefe, statt sie flach neben den Text zu legen. Der Rahmen
|
||||||
|
* weicht beim Hover etwas zurueck, das Bild bleibt ruhig.
|
||||||
|
*/
|
||||||
|
export function FramedMedia({
|
||||||
|
children,
|
||||||
|
side = "right",
|
||||||
|
className = "",
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
/** Richtung, in die der Rahmen versetzt ist. */
|
||||||
|
side?: "left" | "right";
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={`group/frame relative isolate ${className}`}>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`absolute inset-0 -z-10 border border-honey/60 transition-transform duration-700 ease-[var(--ease-arrive)] ${
|
||||||
|
side === "right"
|
||||||
|
? "translate-x-4 translate-y-4 group-hover/frame:translate-x-6 group-hover/frame:translate-y-6"
|
||||||
|
: "-translate-x-4 translate-y-4 group-hover/frame:-translate-x-6 group-hover/frame:translate-y-6"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user