- Next.js 16 App Router site with the ANOUMA design system - Payload CMS (PostgreSQL) for offers, events, posts and page content - WebRTC video-call system with custom signaling server - SMTP email reminders and booking-request notifications - Customer accounts, calendar-based availability, and booking workflow
224 lines
8.5 KiB
TypeScript
224 lines
8.5 KiB
TypeScript
import type { Metadata } from "next";
|
|
import Link from "next/link";
|
|
import { Hero } from "@/components/Hero";
|
|
import { Button } from "@/components/Button";
|
|
import { Section, SectionHeading } from "@/components/Section";
|
|
import { AngebotCard } from "@/components/AngebotCard";
|
|
import { ImagePlaceholder } from "@/components/ImagePlaceholder";
|
|
import { RichText } from "@/components/RichText";
|
|
import { CTA } from "@/components/CTA";
|
|
import { Reveal } from "@/components/Reveal";
|
|
import { EventTeaserCard } from "@/components/EventTeaserCard";
|
|
import { getOffers, getPosts, getUpcomingEvents } from "@/lib/payload/content";
|
|
import { getHomeGlobal } from "@/lib/payload/globals";
|
|
import { groupOffersByCategory, moodForOffer } from "@/lib/angebote";
|
|
import { mediaAlt, mediaUrl } from "@/lib/payload/media";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
export async function generateMetadata(): Promise<Metadata> {
|
|
const home = await getHomeGlobal();
|
|
return {
|
|
title: { absolute: home.heroTitle || "Willkommen bei Anouma" },
|
|
description:
|
|
home.heroSupporting ||
|
|
"Prozessbegleitung, Doula-Begleitung, Kindergruppen und Singkreise — Räume für Verbindung mit dir selbst, miteinander und mit der Natur.",
|
|
};
|
|
}
|
|
|
|
export default async function Home() {
|
|
const [home, offers, posts, events] = await Promise.all([
|
|
getHomeGlobal(),
|
|
getOffers(),
|
|
getPosts(3),
|
|
getUpcomingEvents(3),
|
|
]);
|
|
const groups = groupOffersByCategory(offers);
|
|
|
|
return (
|
|
<>
|
|
<Hero
|
|
eyebrow={home.heroEyebrow ?? undefined}
|
|
title={home.heroTitle || "Willkommen bei Anouma"}
|
|
mood="rose"
|
|
imageSrc={mediaUrl(home.heroImage, "hero")}
|
|
imageAlt={mediaAlt(home.heroImage)}
|
|
actions={
|
|
<>
|
|
<Button href="/termin-buchen">Termin buchen</Button>
|
|
<Button href="/angebote" variant="secondary">
|
|
Angebote entdecken
|
|
</Button>
|
|
</>
|
|
}
|
|
>
|
|
{home.heroHighlight && <p>{home.heroHighlight}</p>}
|
|
{home.heroSupporting && (
|
|
<p className="mt-3 text-base text-anouma-plum/90">{home.heroSupporting}</p>
|
|
)}
|
|
</Hero>
|
|
|
|
<Section tone="cream">
|
|
<SectionHeading
|
|
eyebrow="Angebote"
|
|
title="Räume, die zu deinem Weg passen"
|
|
lead="Von persönlicher Prozessbegleitung über Doula-Begleitung bis zu Kinder- und Singkreisen — jedes Angebot ist ein eigener, geschützter Raum."
|
|
/>
|
|
<div className="mt-14 grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
|
{groups.map((group, i) => {
|
|
const isSingle = group.offers.length === 1;
|
|
const first = group.offers[0];
|
|
return (
|
|
<Reveal key={group.category} delay={i * 0.08}>
|
|
<AngebotCard
|
|
href={isSingle ? `/angebote/${first.slug}` : group.href}
|
|
title={group.label}
|
|
tagline={isSingle ? first.shortDescription : `${group.offers.length} Angebote`}
|
|
mood={moodForOffer(first)}
|
|
imageSrc={mediaUrl(first.image, "card")}
|
|
/>
|
|
</Reveal>
|
|
);
|
|
})}
|
|
</div>
|
|
</Section>
|
|
|
|
{(home.aboutTitle || home.aboutText) && (
|
|
<Section tone="plain">
|
|
<div className="grid gap-14 lg:grid-cols-2 lg:items-center">
|
|
<Reveal>
|
|
<div className="relative mx-auto aspect-[4/5] w-full max-w-md lg:max-w-none">
|
|
<ImagePlaceholder
|
|
mood="mauve"
|
|
label="Anouma"
|
|
src={mediaUrl(home.aboutImage, "hero")}
|
|
alt={mediaAlt(home.aboutImage)}
|
|
className="h-full w-full"
|
|
/>
|
|
</div>
|
|
</Reveal>
|
|
<Reveal delay={0.1}>
|
|
{home.aboutEyebrow && (
|
|
<p className="mb-3 text-xs font-medium uppercase tracking-[0.22em] text-anouma-plum">
|
|
{home.aboutEyebrow}
|
|
</p>
|
|
)}
|
|
{home.aboutTitle && (
|
|
<h2 className="text-balance font-serif text-4xl font-medium leading-tight text-anouma-plum sm:text-5xl">
|
|
{home.aboutTitle}
|
|
</h2>
|
|
)}
|
|
{home.aboutText && (
|
|
<div className="mt-6">
|
|
<RichText data={home.aboutText} />
|
|
</div>
|
|
)}
|
|
<div className="mt-8">
|
|
<Button href="/ueber-mich" variant="ghost">
|
|
Mehr über mich lesen →
|
|
</Button>
|
|
</div>
|
|
</Reveal>
|
|
</div>
|
|
</Section>
|
|
)}
|
|
|
|
{home.values && home.values.length > 0 && (
|
|
<Section tone="mauve">
|
|
<SectionHeading
|
|
eyebrow={home.valuesEyebrow ?? undefined}
|
|
title={home.valuesTitle || "Philosophie"}
|
|
align="center"
|
|
className="mx-auto text-anouma-cream-light [&_h2]:text-anouma-cream-light [&_p]:text-anouma-cream-light/90"
|
|
/>
|
|
<div className="mt-16 grid gap-10 sm:grid-cols-3">
|
|
{home.values.map((wert, i) => (
|
|
<Reveal key={wert.id ?? wert.title} delay={i * 0.1} className="text-center">
|
|
<p className="font-serif text-sm font-medium uppercase tracking-[0.2em] text-white/85">
|
|
{wert.title}
|
|
</p>
|
|
<p className="mt-4 text-balance font-serif text-2xl leading-snug italic">
|
|
„{wert.quote}“
|
|
</p>
|
|
</Reveal>
|
|
))}
|
|
</div>
|
|
</Section>
|
|
)}
|
|
|
|
{events.length > 0 && (
|
|
<Section tone="warm">
|
|
<div className="flex flex-wrap items-end justify-between gap-6">
|
|
<SectionHeading eyebrow="Termine" title="Aktuelle Termine" className="mb-0" />
|
|
<Button href="/termine" variant="ghost">
|
|
Alle Termine →
|
|
</Button>
|
|
</div>
|
|
<div className="mt-12 grid gap-6 sm:grid-cols-3">
|
|
{events.map((event, i) => (
|
|
<Reveal key={event.slug} delay={i * 0.08}>
|
|
<EventTeaserCard event={event} />
|
|
</Reveal>
|
|
))}
|
|
</div>
|
|
</Section>
|
|
)}
|
|
|
|
{posts.length > 0 && (
|
|
<Section tone="cream">
|
|
<div className="flex flex-wrap items-end justify-between gap-6">
|
|
<SectionHeading eyebrow="Aktuelles" title="Neuigkeiten und Inspiration" className="mb-0" />
|
|
<Button href="/aktuelles" variant="ghost">
|
|
Alle Neuigkeiten →
|
|
</Button>
|
|
</div>
|
|
<div className="mt-12 grid gap-6 sm:grid-cols-3">
|
|
{posts.map((post, i) => (
|
|
<Reveal key={post.slug} delay={i * 0.08}>
|
|
<Link href={`/aktuelles/${post.slug}`} className="block rounded-3xl bg-background p-7">
|
|
<p className="text-xs font-medium uppercase tracking-[0.18em] text-anouma-plum">
|
|
{new Date(post.publishDate ?? post.createdAt).toLocaleDateString("de-DE", {
|
|
day: "2-digit",
|
|
month: "long",
|
|
year: "numeric",
|
|
})}
|
|
</p>
|
|
<h3 className="mt-3 font-serif text-xl font-medium text-anouma-plum">{post.title}</h3>
|
|
<p className="mt-3 text-sm leading-relaxed text-anouma-plum">{post.teaser}</p>
|
|
</Link>
|
|
</Reveal>
|
|
))}
|
|
</div>
|
|
</Section>
|
|
)}
|
|
|
|
<Section tone="plain">
|
|
<SectionHeading eyebrow="Impressionen" title="Einblicke in gemeinsame Räume" align="center" className="mx-auto" />
|
|
<div className="mt-14 grid grid-cols-2 gap-4 sm:grid-cols-4">
|
|
{(["moss", "rose", "peach", "sand"] as const).map((mood, i) => (
|
|
<Reveal key={mood} delay={i * 0.06}>
|
|
<ImagePlaceholder mood={mood} label="Impression" shape="soft" className="aspect-square" />
|
|
</Reveal>
|
|
))}
|
|
</div>
|
|
<div className="mt-10 text-center">
|
|
<Link
|
|
href="/impressionen"
|
|
className="text-sm font-medium text-anouma-mauve-dark underline underline-offset-4"
|
|
>
|
|
Alle Impressionen ansehen
|
|
</Link>
|
|
</div>
|
|
</Section>
|
|
|
|
<CTA
|
|
title={home.ctaTitle || "Kennenlernen & Termine vereinbaren"}
|
|
lead={
|
|
home.ctaLead ||
|
|
"Ich freue mich, von dir zu hören — schreib mir oder buche direkt ein unverbindliches Kennenlerngespräch."
|
|
}
|
|
/>
|
|
</>
|
|
);
|
|
}
|