Files
Wabenhain/src/components/home-sections-b.tsx
T
Nick SzaunigandClaude Opus 5.5 1fa8ca246a refactor(ui): Kicker-Zeilen ueber Ueberschriften entfernt
Jede zweite Ueberschrift trug eine kleine Versalien-Zeile mit Wabenpunkt
("• STIMMEN", "• AKTUELL VERFUEGBAR", "• KUNDENKONTO" ...). Auf 29 Seiten
und Abschnitten wiederholt las sich das wie eine Vorlage. SectionHeading
rendert sie nicht mehr, die Prop ist entfernt; die Ueberschrift traegt den
Abschnitt allein.

Die eyebrow-Texte, die weiterhin als Seitentitel, Bildbeschriftung oder
Breadcrumb dienen, bleiben; 18 danach ungenutzte Eintraege sind aus Typ und
beiden Woerterbuechern entfernt.

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

234 lines
9.1 KiB
TypeScript

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 { ContentImage } from "@/components/content-image";
import { StarRating } from "@/components/star-rating";
import { fetchGoogleReviews } from "@/lib/google-reviews";
import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary";
import { getContentValues, parseListTitled, parseListNumbered } from "@/lib/content";
export async function ProcessTimeline() {
const locale = await getLocale();
const t = getDictionary(locale);
const content = await getContentValues(["home.processTimeline.steps"], locale);
const steps = parseListNumbered(content["home.processTimeline.steps"]);
return (
<section className="py-24 bg-cream/60">
<Container>
<ScrollReveal>
<SectionHeading
title={t.home.processTimeline.title}
align="center"
className="mx-auto"
/>
</ScrollReveal>
<ProcessPath steps={steps} />
</Container>
</section>
);
}
export async function RegionalSection() {
const locale = await getLocale();
const t = getDictionary(locale);
const content = await getContentValues(
[
"home.regionalSection.paragraph1",
"home.regionalSection.paragraph2",
"home.regionalSection.image",
],
locale
);
return (
<section className="py-24">
<Container className="grid md:grid-cols-2 gap-16 lg:gap-24 items-center">
<ScrollReveal className="order-2 md:order-1">
<SectionHeading
title={t.home.regionalSection.title}
/>
<p className="mt-6 text-ink/75 leading-relaxed">{content["home.regionalSection.paragraph1"]}</p>
<p className="mt-4 text-ink/75 leading-relaxed">{content["home.regionalSection.paragraph2"]}</p>
<div className="mt-8 flex flex-wrap gap-4">
<LinkButton href="/imkerei" variant="secondary">
{t.home.regionalSection.ctaAbout}
</LinkButton>
<LinkButton href="/kontakt" variant="ghost">
{t.home.regionalSection.ctaPickup}
</LinkButton>
</div>
</ScrollReveal>
<ScrollReveal className="order-1 md:order-2" delay={0.1}>
<FramedMedia side="left">
<ContentImage
src={content["home.regionalSection.image"] || null}
alt=""
tone="forest"
className="aspect-[4/5] w-full"
label={t.home.regionalSection.eyebrow}
sizes="(min-width: 768px) 50vw, 100vw"
/>
</FramedMedia>
</ScrollReveal>
</Container>
</section>
);
}
export async function Sustainability() {
const locale = await getLocale();
const t = getDictionary(locale);
const content = await getContentValues(["home.sustainability.items"], locale);
const items = parseListTitled(content["home.sustainability.items"]);
return (
<section className="py-28 bg-cream/60">
{/* Geteilt statt Viererreihe: die Ueberschrift laeuft mit, die Punkte
werden als ruhige Liste gelesen. */}
<Container className="grid gap-12 lg:grid-cols-12 lg:gap-16">
<ScrollReveal className="lg:col-span-5">
<div className="lg:sticky lg:top-32">
<SectionHeading title={t.home.sustainability.title} />
</div>
</ScrollReveal>
<ul className="divide-y divide-ink/10 border-y border-ink/10 lg:col-span-7">
{items.map((item, i) => (
<li key={item.title}>
<ScrollReveal delay={i * 0.08}>
<div className="group grid grid-cols-[auto_1fr] gap-x-5 py-7">
<HexMark className="mt-2 h-3 w-3 text-honey transition-transform duration-500 ease-[var(--ease-arrive)] group-hover:rotate-90" />
<div>
<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>
))}
</ul>
</Container>
</section>
);
}
export async function BusinessTeaser() {
const locale = await getLocale();
const t = getDictionary(locale);
const content = await getContentValues(
["home.businessTeaser.paragraph", "home.businessTeaser.image"],
locale
);
return (
<section className="py-24">
<Container className="grid md:grid-cols-2 gap-16 lg:gap-24 items-center">
<ScrollReveal>
<FramedMedia>
<ContentImage
src={content["home.businessTeaser.image"] || null}
alt=""
tone="honey"
className="aspect-[4/5] w-full"
label={t.home.businessTeaser.eyebrow}
sizes="(min-width: 768px) 50vw, 100vw"
/>
</FramedMedia>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<SectionHeading
title={t.home.businessTeaser.title}
/>
<p className="mt-6 text-ink/75 leading-relaxed">{content["home.businessTeaser.paragraph"]}</p>
<LinkButton href="/unternehmen" className="mt-8">
{t.home.businessTeaser.cta}
</LinkButton>
</ScrollReveal>
</Container>
</section>
);
}
export async function Reviews() {
const t = getDictionary(await getLocale());
const google = await fetchGoogleReviews();
const placeId = process.env.GOOGLE_PLACE_ID;
const mapsUrl = placeId ? `https://www.google.com/maps/place/?q=place_id:${placeId}` : null;
// Echte Google-Rezensionen sind Zitate realer Kund:innen und werden nie
// uebersetzt oder umformuliert — nur die eigenen Fallback-Testimonials
// (ohne konfigurierte Google-Anbindung) existieren in beiden Sprachen.
const items = google && google.reviews.length > 0
? google.reviews.map((r) => ({
quote: r.text,
author: r.author,
rating: r.rating,
authorUrl: r.authorUrl,
}))
: t.home.reviews.testimonials.map((r) => ({ ...r, rating: 5, authorUrl: null }));
return (
<section className="relative overflow-hidden py-28 bg-forest text-cream">
<HoneycombPattern className="text-honey-light/[0.07]" size={30} />
<Container className="relative">
<ScrollReveal>
<SectionHeading
title={t.home.reviews.title}
align="center"
className="mx-auto"
tone="inverted"
/>
{google?.rating ? (
<p className="mt-4 text-center text-cream/60 text-sm">
{t.home.reviews.googleReviewsCount(google.rating.toFixed(1), google.totalCount ?? 0)}
</p>
) : null}
</ScrollReveal>
<div className="mt-14 grid md:grid-cols-3 gap-10">
{items.slice(0, 6).map((review, i) => (
<ScrollReveal key={`${review.author}-${i}`} delay={i * 0.08}>
{/* Gezeichnet statt als Schriftzeichen: ein grosses "„" sitzt auf
der Grundlinie und ragte mit enger Zeilenhoehe in die Sterne. */}
<svg
viewBox="0 0 40 28"
className="mb-4 h-7 w-10 text-honey-light/70"
fill="currentColor"
aria-hidden="true"
>
<path d="M8.5 2a7.5 7.5 0 0 1 7.5 7.5c0 7.4-4.6 13.9-11.6 16.5l-1-2.2c4.2-2.1 6.7-5.5 7.1-9.3A7.5 7.5 0 1 1 8.5 2z" />
<path d="M30 2a7.5 7.5 0 0 1 7.5 7.5c0 7.4-4.6 13.9-11.6 16.5l-1-2.2c4.2-2.1 6.7-5.5 7.1-9.3A7.5 7.5 0 1 1 30 2z" />
</svg>
<StarRating value={review.rating} />
<span className="sr-only">{t.home.reviews.starsOutOf5(review.rating)}</span>
<p className="mt-4 font-serif text-xl text-cream/90 leading-relaxed italic">
{review.quote}
</p>
{/* Zuordnung zur Quelle: die Nutzungsbedingungen der Places API
verlangen, dass eine angezeigte Rezension zu Google
zurueckfuehrt. Bei den eigenen Beispiel-Stimmen gibt es
nichts zu verlinken — dort bleibt es beim reinen Namen. */}
{review.authorUrl ? (
<a
href={review.authorUrl}
target="_blank"
rel="noopener noreferrer nofollow"
className="mt-4 inline-block text-sm text-cream/70 underline decoration-cream/30 hover:decoration-cream"
>
{review.author}
</a>
) : (
<p className="mt-4 text-sm text-cream/70">{review.author}</p>
)}
</ScrollReveal>
))}
</div>
{mapsUrl ? (
<p className="mt-14 text-center">
<a href={mapsUrl} target="_blank" rel="noopener noreferrer" className="text-sm text-cream/70 underline">
{t.home.reviews.seeAllOnGoogle}
</a>
</p>
) : null}
</Container>
</section>
);
}