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]>
234 lines
9.1 KiB
TypeScript
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>
|
|
);
|
|
}
|