- Aktuelle Ernte: bei ein, zwei Produkten steht die Ueberschrift als eigene Spalte daneben, statt ueber einem halb leeren Dreierraster. - Herkunft: Foto randlos ueber die halbe Seitenbreite, Text daneben, ohne Rahmen. - Philosophie: typografische Reihe mit Trennlinien und grossen Serif-Titeln statt vier Icon-Karten. - Region: breites Bild, der Text liegt als Tafel ueberlappend darueber. - Geschaeftskunden: kompakte Tafel mit randlosem Bild statt eines dritten grossen Bild/Text-Wechsels. Inhalte, Texte und Content-Schluessel bleiben unveraendert. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
234 lines
9.4 KiB
TypeScript
234 lines
9.4 KiB
TypeScript
import { Container, SectionHeading, LinkButton } 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>
|
|
{/* Breites Bild, der Text legt sich als Tafel darueber — Tiefe statt
|
|
eines weiteren Bild-neben-Text-Wechsels. */}
|
|
<ScrollReveal>
|
|
<ContentImage
|
|
src={content["home.regionalSection.image"] || null}
|
|
alt=""
|
|
tone="forest"
|
|
className="aspect-[4/3] w-full sm:aspect-[16/9]"
|
|
label={t.home.regionalSection.eyebrow}
|
|
sizes="(min-width: 1280px) 1200px, 100vw"
|
|
/>
|
|
</ScrollReveal>
|
|
<ScrollReveal
|
|
delay={0.1}
|
|
className="relative mx-4 -mt-16 max-w-2xl bg-background p-8 shadow-[0_24px_50px_-34px_rgba(36,53,43,0.45)] sm:mx-10 sm:p-10 lg:-mt-40 lg:ml-16 lg:p-12"
|
|
>
|
|
<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>
|
|
</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>
|
|
{/* Kompakte Tafel statt eines dritten grossen Bild/Text-Blocks: ein
|
|
Angebot fuer eine kleinere Zielgruppe, entsprechend zurueckhaltend. */}
|
|
<ScrollReveal className="grid overflow-hidden border border-ink/10 bg-background md:grid-cols-5">
|
|
<div className="flex flex-col justify-center p-8 sm:p-12 md:col-span-3 lg:p-16">
|
|
<SectionHeading title={t.home.businessTeaser.title} />
|
|
<p className="mt-6 max-w-[55ch] text-ink/75 leading-relaxed">{content["home.businessTeaser.paragraph"]}</p>
|
|
<LinkButton href="/unternehmen" className="mt-8 self-start">
|
|
{t.home.businessTeaser.cta}
|
|
</LinkButton>
|
|
</div>
|
|
<ContentImage
|
|
src={content["home.businessTeaser.image"] || null}
|
|
alt=""
|
|
tone="honey"
|
|
rounded={false}
|
|
className="aspect-[4/3] w-full md:col-span-2 md:aspect-auto md:h-full md:min-h-[22rem]"
|
|
label={t.home.businessTeaser.eyebrow}
|
|
sizes="(min-width: 768px) 40vw, 100vw"
|
|
/>
|
|
</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>
|
|
);
|
|
}
|