Replaces em dashes with en dashes across all hardcoded copy, emails, and seed content, then reworks the site's visual language so it reads as deliberately designed rather than assembled from generic components: - New ArrowLink component replaces ad hoc "Mehr erfahren ->" links and ghost buttons; one shared FormField (TextField/TextAreaField) replaces 7 near-duplicate field styles across every form - Navbar loses its glassmorphism blur for a solid warm background, gets a subtle active-link underline and a softer, warmer mega-menu - PageHeader supports per-page blob tone/position and a compact mode instead of an identical band on every interior page - Homepage: the core offer is featured large instead of four equal cards, the philosophy section gets a single calmer accent blob, blog teasers now show their cover image like the real /aktuelles page does - Booking calendar and widget: 44px touch targets throughout (month nav, day cells, time slots, cancel action), a visible "today" ring, and responsive padding so the calendar isn't cramped on small screens - Quieter, warmer empty states on /termine and /aktuelles No functional changes: all interaction logic (nav menus, forms, booking, CMS data fetching) is untouched, only markup and styling.
121 lines
4.5 KiB
TypeScript
121 lines
4.5 KiB
TypeScript
import type { Metadata } from "next";
|
||
import { PageHeader } from "@/components/PageHeader";
|
||
import { Section, SectionHeading } from "@/components/Section";
|
||
import { ContactForm } from "@/components/ContactForm";
|
||
import { AngebotCard } from "@/components/AngebotCard";
|
||
import { Reveal } from "@/components/Reveal";
|
||
import { getBookingGlobal, getContactGlobal, getSEOSettingsGlobal } from "@/lib/payload/globals";
|
||
import { getOffers } from "@/lib/payload/content";
|
||
import { moodForOffer } from "@/lib/angebote";
|
||
import { mediaUrl } from "@/lib/payload/media";
|
||
import { resolveSeo } from "@/lib/seo/resolve";
|
||
import { buildMetadata } from "@/lib/seo/metadata";
|
||
import { getSiteUrl } from "@/lib/seo/config";
|
||
import { JsonLd, webPageJsonLd } from "@/lib/seo/jsonld";
|
||
import { siteConfig } from "@/lib/site";
|
||
|
||
export const dynamic = "force-dynamic";
|
||
|
||
const FALLBACK_DESCRIPTION = "Kennenlernen & Termine vereinbaren.";
|
||
|
||
export async function generateMetadata(): Promise<Metadata> {
|
||
const [booking, seoSettings] = await Promise.all([getBookingGlobal(), getSEOSettingsGlobal()]);
|
||
const resolved = resolveSeo({
|
||
seo: booking.seo,
|
||
fallbackTitle: `Termin buchen – ${siteConfig.name}`,
|
||
fallbackDescription: () => booking.lead || seoSettings.defaultDescription || FALLBACK_DESCRIPTION,
|
||
defaultOgImage: seoSettings.defaultOgImage,
|
||
});
|
||
return buildMetadata({
|
||
title: resolved.title,
|
||
description: resolved.description,
|
||
path: "/termin-buchen",
|
||
ogImageUrl: resolved.ogImageUrl,
|
||
keywords: resolved.keywords,
|
||
});
|
||
}
|
||
|
||
export default async function TerminBuchenPage() {
|
||
const [booking, contact, offers] = await Promise.all([getBookingGlobal(), getContactGlobal(), getOffers()]);
|
||
const bookableOffers = offers.filter((offer) => offer.bookable);
|
||
const siteUrl = await getSiteUrl();
|
||
|
||
return (
|
||
<>
|
||
<JsonLd
|
||
data={webPageJsonLd({
|
||
name: booking.title || "Termin buchen",
|
||
description: booking.lead || FALLBACK_DESCRIPTION,
|
||
url: `${siteUrl}/termin-buchen`,
|
||
})}
|
||
/>
|
||
<PageHeader
|
||
eyebrow={booking.eyebrow || "Termin buchen"}
|
||
title={booking.title || "Kennenlernen & Termine vereinbaren"}
|
||
lead={booking.lead}
|
||
crumbs={[{ title: "Termin buchen" }]}
|
||
/>
|
||
|
||
{bookableOffers.length > 0 && (
|
||
<Section tone="plain">
|
||
<SectionHeading eyebrow="Angebot auswählen" title="Wofür möchtest du einen Termin?" />
|
||
<div className="mt-14 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||
{bookableOffers.map((offer, i) => (
|
||
<Reveal key={offer.slug} delay={i * 0.08}>
|
||
<AngebotCard
|
||
href={`/angebote/${offer.slug}`}
|
||
title={offer.title}
|
||
tagline={offer.shortDescription}
|
||
mood={moodForOffer(offer)}
|
||
imageSrc={mediaUrl(offer.image, "card")}
|
||
/>
|
||
</Reveal>
|
||
))}
|
||
</div>
|
||
</Section>
|
||
)}
|
||
|
||
{booking.steps && booking.steps.length > 0 && (
|
||
<Section tone="cream">
|
||
<SectionHeading eyebrow="Ablauf" title="So findest du zu deinem Termin" />
|
||
<div className="mt-14 grid gap-8 sm:grid-cols-3">
|
||
{booking.steps.map((s, i) => (
|
||
<Reveal key={s.id ?? s.title} delay={i * 0.08}>
|
||
<p className="font-serif text-5xl font-medium text-anouma-rose">
|
||
{String(i + 1).padStart(2, "0")}
|
||
</p>
|
||
<h3 className="mt-4 font-serif text-xl font-medium text-anouma-plum">{s.title}</h3>
|
||
<p className="mt-3 text-base leading-relaxed text-anouma-plum">{s.text}</p>
|
||
</Reveal>
|
||
))}
|
||
</div>
|
||
</Section>
|
||
)}
|
||
|
||
<Section tone="plain">
|
||
<div className="mx-auto max-w-xl">
|
||
<SectionHeading
|
||
title="Allgemeine Anfrage"
|
||
lead={
|
||
bookableOffers.length > 0
|
||
? "Für einen konkreten Termin wähle oben ein Angebot – hier kannst du auch einfach eine allgemeine Nachricht schreiben."
|
||
: undefined
|
||
}
|
||
align="left"
|
||
/>
|
||
<div className="mt-10">
|
||
<ContactForm
|
||
toEmail={contact.email}
|
||
subjectPrefix="Terminanfrage über anouma.org"
|
||
submitLabel="Terminanfrage senden"
|
||
/>
|
||
</div>
|
||
{booking.formNote && (
|
||
<p className="mt-8 text-sm leading-relaxed text-anouma-plum/80">{booking.formNote}</p>
|
||
)}
|
||
</div>
|
||
</Section>
|
||
</>
|
||
);
|
||
}
|