Redesign: warmer, more editorial visual pass; remove stray em dashes
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.
This commit is contained in:
@@ -55,9 +55,10 @@ export default async function AktuellesPage() {
|
|||||||
|
|
||||||
<Section tone="plain">
|
<Section tone="plain">
|
||||||
{posts.length === 0 ? (
|
{posts.length === 0 ? (
|
||||||
<p className="text-lg text-anouma-plum">
|
<div className="rounded-3xl bg-anouma-cream-light p-8 text-center sm:p-10">
|
||||||
Hier erscheinen bald die ersten Neuigkeiten — schau gerne wieder vorbei.
|
<p className="font-serif text-xl font-medium text-anouma-plum">Hier erscheinen bald die ersten Neuigkeiten</p>
|
||||||
</p>
|
<p className="mt-2 text-base leading-relaxed text-anouma-plum/90">Schau gerne wieder vorbei.</p>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{posts.map((post, i) => (
|
{posts.map((post, i) => (
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { siteConfig } from "@/lib/site";
|
|||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
const FALLBACK_DESCRIPTION =
|
const FALLBACK_DESCRIPTION =
|
||||||
"Prozessbegleitung, Doula-Begleitung, Kindergruppen und Singkreise — die Angebote von Anouma im Überblick.";
|
"Prozessbegleitung, Doula-Begleitung, Kindergruppen und Singkreise – die Angebote von Anouma im Überblick.";
|
||||||
|
|
||||||
export async function generateMetadata(): Promise<Metadata> {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
const [intro, seoSettings] = await Promise.all([getAngeboteIntroGlobal(), getSEOSettingsGlobal()]);
|
const [intro, seoSettings] = await Promise.all([getAngeboteIntroGlobal(), getSEOSettingsGlobal()]);
|
||||||
@@ -170,7 +170,7 @@ export default async function AngebotePage() {
|
|||||||
|
|
||||||
<CTA
|
<CTA
|
||||||
title="Unsicher, welcher Raum passt?"
|
title="Unsicher, welcher Raum passt?"
|
||||||
lead="Melde dich gerne für ein unverbindliches Kennenlerngespräch — gemeinsam schauen wir, was dein Anliegen gerade braucht."
|
lead="Melde dich gerne für ein unverbindliches Kennenlerngespräch – gemeinsam schauen wir, was dein Anliegen gerade braucht."
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export default async function SingkreisePage() {
|
|||||||
|
|
||||||
<CTA
|
<CTA
|
||||||
title="Deine Stimme erklingen lassen"
|
title="Deine Stimme erklingen lassen"
|
||||||
lead="Ob offen für alle, für Schwangere oder für Mamas mit Baby — melde dich, wenn du einen Kreis besuchen möchtest."
|
lead="Ob offen für alle, für Schwangere oder für Mamas mit Baby – melde dich, wenn du einen Kreis besuchen möchtest."
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export default async function VerifyEmailPage({ params }: Args) {
|
|||||||
eyebrow="Konto"
|
eyebrow="Konto"
|
||||||
title={verified ? "E-Mail-Adresse bestätigt" : "Bestätigung fehlgeschlagen"}
|
title={verified ? "E-Mail-Adresse bestätigt" : "Bestätigung fehlgeschlagen"}
|
||||||
crumbs={[{ title: "E-Mail bestätigen" }]}
|
crumbs={[{ title: "E-Mail bestätigen" }]}
|
||||||
|
compact
|
||||||
/>
|
/>
|
||||||
<Section tone="plain">
|
<Section tone="plain">
|
||||||
<div className="mx-auto max-w-md text-center">
|
<div className="mx-auto max-w-md text-center">
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export default async function DatenschutzPage() {
|
|||||||
const contact = await getContactGlobal();
|
const contact = await getContactGlobal();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader title="Datenschutzerklärung" crumbs={[{ title: "Datenschutz" }]} />
|
<PageHeader title="Datenschutzerklärung" crumbs={[{ title: "Datenschutz" }]} compact />
|
||||||
<Section tone="plain">
|
<Section tone="plain">
|
||||||
<div className="mx-auto max-w-2xl space-y-8">
|
<div className="mx-auto max-w-2xl space-y-8">
|
||||||
<PlaceholderNote>
|
<PlaceholderNote>
|
||||||
@@ -33,9 +33,9 @@ export default async function DatenschutzPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h2 className="font-serif text-2xl font-medium text-anouma-plum">Verantwortliche Stelle</h2>
|
<h2 className="font-serif text-2xl font-medium text-anouma-plum">Verantwortliche Stelle</h2>
|
||||||
<p className="mt-4 leading-relaxed text-anouma-plum">
|
<p className="mt-4 leading-relaxed text-anouma-plum">
|
||||||
[Platzhalter — Vor- und Nachname]
|
[Platzhalter – Vor- und Nachname]
|
||||||
<br />
|
<br />
|
||||||
[Platzhalter — Anschrift]
|
[Platzhalter – Anschrift]
|
||||||
<br />
|
<br />
|
||||||
E-Mail: {contact.email}
|
E-Mail: {contact.email}
|
||||||
</p>
|
</p>
|
||||||
@@ -48,7 +48,7 @@ export default async function DatenschutzPage() {
|
|||||||
<p className="mt-4 leading-relaxed text-anouma-plum">
|
<p className="mt-4 leading-relaxed text-anouma-plum">
|
||||||
Beim Besuch dieser Website werden aus technischen Gründen automatisch Informationen
|
Beim Besuch dieser Website werden aus technischen Gründen automatisch Informationen
|
||||||
erfasst, die dein Browser übermittelt (z. B. IP-Adresse, Datum und Uhrzeit des
|
erfasst, die dein Browser übermittelt (z. B. IP-Adresse, Datum und Uhrzeit des
|
||||||
Zugriffs). [Platzhalter — Details zum verwendeten Hosting-Anbieter ergänzen.]
|
Zugriffs). [Platzhalter – Details zum verwendeten Hosting-Anbieter ergänzen.]
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { JsonLd, webPageJsonLd } from "@/lib/seo/jsonld";
|
|||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
const DESCRIPTION = "Bildeindrücke aus der Arbeit von Anouma — Natur, Gemeinschaft und gemeinsame Räume.";
|
const DESCRIPTION = "Bildeindrücke aus der Arbeit von Anouma – Natur, Gemeinschaft und gemeinsame Räume.";
|
||||||
|
|
||||||
export async function generateMetadata(): Promise<Metadata> {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
return buildMetadata({ title: "Impressionen", description: DESCRIPTION, path: "/impressionen" });
|
return buildMetadata({ title: "Impressionen", description: DESCRIPTION, path: "/impressionen" });
|
||||||
@@ -36,11 +36,11 @@ export default async function ImpressionenPage() {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
eyebrow="Impressionen"
|
eyebrow="Impressionen"
|
||||||
title="Einblicke in gemeinsame Räume"
|
title="Einblicke in gemeinsame Räume"
|
||||||
lead="Diese Galerie ist als Platzhalter-System angelegt — echte Fotografien lassen sich hier später direkt einsetzen, ohne das Layout zu verändern."
|
lead="Diese Galerie ist als Platzhalter-System angelegt – echte Fotografien lassen sich hier später direkt einsetzen, ohne das Layout zu verändern."
|
||||||
crumbs={[{ title: "Impressionen" }]}
|
crumbs={[{ title: "Impressionen" }]}
|
||||||
/>
|
/>
|
||||||
<Section tone="plain">
|
<Section tone="plain">
|
||||||
<div className="grid auto-rows-[14rem] grid-cols-2 gap-4 sm:grid-cols-4">
|
<div className="grid auto-rows-[10rem] grid-cols-2 gap-4 sm:auto-rows-[14rem] sm:grid-cols-4">
|
||||||
{gallery.map((item, i) => (
|
{gallery.map((item, i) => (
|
||||||
<Reveal key={item.label} delay={Math.min(i * 0.04, 0.3)} className={item.span}>
|
<Reveal key={item.label} delay={Math.min(i * 0.04, 0.3)} className={item.span}>
|
||||||
<ImagePlaceholder mood={item.mood} label={item.label} shape="soft" className="h-full w-full" />
|
<ImagePlaceholder mood={item.mood} label={item.label} shape="soft" className="h-full w-full" />
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default async function ImpressumPage() {
|
|||||||
const contact = await getContactGlobal();
|
const contact = await getContactGlobal();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader title="Impressum" crumbs={[{ title: "Impressum" }]} />
|
<PageHeader title="Impressum" crumbs={[{ title: "Impressum" }]} compact />
|
||||||
<Section tone="plain">
|
<Section tone="plain">
|
||||||
<div className="mx-auto max-w-2xl space-y-8">
|
<div className="mx-auto max-w-2xl space-y-8">
|
||||||
<PlaceholderNote>
|
<PlaceholderNote>
|
||||||
@@ -35,11 +35,11 @@ export default async function ImpressumPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h2 className="font-serif text-2xl font-medium text-anouma-plum">Angaben gemäß § 5 TMG</h2>
|
<h2 className="font-serif text-2xl font-medium text-anouma-plum">Angaben gemäß § 5 TMG</h2>
|
||||||
<p className="mt-4 leading-relaxed text-anouma-plum">
|
<p className="mt-4 leading-relaxed text-anouma-plum">
|
||||||
[Platzhalter — Vor- und Nachname]
|
[Platzhalter – Vor- und Nachname]
|
||||||
<br />
|
<br />
|
||||||
[Platzhalter — Straße und Hausnummer]
|
[Platzhalter – Straße und Hausnummer]
|
||||||
<br />
|
<br />
|
||||||
[Platzhalter — PLZ und Ort]
|
[Platzhalter – PLZ und Ort]
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ export default async function ImpressumPage() {
|
|||||||
Umsatzsteuer-Identifikationsnummer
|
Umsatzsteuer-Identifikationsnummer
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-4 leading-relaxed text-anouma-plum">
|
<p className="mt-4 leading-relaxed text-anouma-plum">
|
||||||
[Platzhalter — sofern vorhanden, gemäß § 27 a Umsatzsteuergesetz]
|
[Platzhalter – sofern vorhanden, gemäß § 27 a Umsatzsteuergesetz]
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ export default async function ImpressumPage() {
|
|||||||
Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV
|
Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-4 leading-relaxed text-anouma-plum">
|
<p className="mt-4 leading-relaxed text-anouma-plum">
|
||||||
[Platzhalter — Vor- und Nachname, Anschrift wie oben]
|
[Platzhalter – Vor- und Nachname, Anschrift wie oben]
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ export default async function KontaktPage() {
|
|||||||
title={contact.title || "Ich freue mich, von dir zu hören"}
|
title={contact.title || "Ich freue mich, von dir zu hören"}
|
||||||
lead={contact.lead}
|
lead={contact.lead}
|
||||||
crumbs={[{ title: "Kontakt" }]}
|
crumbs={[{ title: "Kontakt" }]}
|
||||||
|
blobTone="sage"
|
||||||
|
blobPosition="top-left"
|
||||||
|
compact
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Section tone="plain">
|
<Section tone="plain">
|
||||||
|
|||||||
@@ -44,12 +44,12 @@ export default async function KontoLayout({ children }: { children: ReactNode })
|
|||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className="whitespace-nowrap rounded-full px-4 py-2.5 text-sm font-medium text-anouma-plum transition-colors hover:bg-white lg:rounded-2xl"
|
className="flex min-h-11 items-center whitespace-nowrap rounded-full px-4 text-sm font-medium text-anouma-plum transition-colors hover:bg-white lg:rounded-2xl"
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
<LogoutButton className="whitespace-nowrap rounded-full px-4 py-2.5 text-left text-sm font-medium text-anouma-plum/70 transition-colors hover:bg-white lg:rounded-2xl" />
|
<LogoutButton className="flex min-h-11 items-center whitespace-nowrap rounded-full px-4 text-left text-sm font-medium text-anouma-plum/70 transition-colors hover:bg-white lg:rounded-2xl" />
|
||||||
</nav>
|
</nav>
|
||||||
<div className="min-w-0">{children}</div>
|
<div className="min-w-0">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
|
||||||
import { getCurrentCustomer } from "@/lib/auth/customer";
|
import { getCurrentCustomer } from "@/lib/auth/customer";
|
||||||
import { getCustomerBookings } from "@/lib/booking/queries";
|
import { getCustomerBookings } from "@/lib/booking/queries";
|
||||||
import { BookingCard } from "@/components/booking/BookingCard";
|
import { BookingCard } from "@/components/booking/BookingCard";
|
||||||
|
import { ArrowLink } from "@/components/ArrowLink";
|
||||||
|
|
||||||
export const metadata: Metadata = { title: "Mein Konto" };
|
export const metadata: Metadata = { title: "Mein Konto" };
|
||||||
|
|
||||||
@@ -61,9 +61,7 @@ export default async function KontoOverviewPage() {
|
|||||||
|
|
||||||
{past.length > 0 && (
|
{past.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<Link href="/konto/termine" className="text-sm font-medium text-anouma-mauve-dark underline underline-offset-4">
|
<ArrowLink href="/konto/termine">Alle Termine (inkl. vergangene) ansehen</ArrowLink>
|
||||||
Alle Termine (inkl. vergangene) ansehen →
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export default async function KontoProfilPage() {
|
|||||||
<div className="mt-6 rounded-3xl bg-white p-6">
|
<div className="mt-6 rounded-3xl bg-white p-6">
|
||||||
<ProfileForm id={customer.id} name={customer.name} phone={customer.phone ?? ""} />
|
<ProfileForm id={customer.id} name={customer.name} phone={customer.phone ?? ""} />
|
||||||
<p className="mt-6 text-xs text-anouma-plum/60">
|
<p className="mt-6 text-xs text-anouma-plum/60">
|
||||||
E-Mail: {customer.email} — für eine Änderung der E-Mail-Adresse melde dich bitte direkt bei Anna.
|
E-Mail: {customer.email} – für eine Änderung der E-Mail-Adresse melde dich bitte direkt bei Anna.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default async function LoginPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader eyebrow="Konto" title="Anmelden" crumbs={[{ title: "Anmelden" }]} />
|
<PageHeader eyebrow="Konto" title="Anmelden" crumbs={[{ title: "Anmelden" }]} compact />
|
||||||
<Section tone="plain">
|
<Section tone="plain">
|
||||||
<div className="mx-auto max-w-md">
|
<div className="mx-auto max-w-md">
|
||||||
<Suspense>
|
<Suspense>
|
||||||
|
|||||||
+80
-59
@@ -2,9 +2,11 @@ import type { Metadata } from "next";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Hero } from "@/components/Hero";
|
import { Hero } from "@/components/Hero";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
|
import { ArrowLink } from "@/components/ArrowLink";
|
||||||
import { Section, SectionHeading } from "@/components/Section";
|
import { Section, SectionHeading } from "@/components/Section";
|
||||||
import { AngebotCard } from "@/components/AngebotCard";
|
import { AngebotCard } from "@/components/AngebotCard";
|
||||||
import { ImagePlaceholder } from "@/components/ImagePlaceholder";
|
import { ImagePlaceholder } from "@/components/ImagePlaceholder";
|
||||||
|
import { OrganicBlob } from "@/components/OrganicBlob";
|
||||||
import { RichText } from "@/components/RichText";
|
import { RichText } from "@/components/RichText";
|
||||||
import { CTA } from "@/components/CTA";
|
import { CTA } from "@/components/CTA";
|
||||||
import { Reveal } from "@/components/Reveal";
|
import { Reveal } from "@/components/Reveal";
|
||||||
@@ -29,7 +31,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
|||||||
fallbackDescription: () =>
|
fallbackDescription: () =>
|
||||||
home.heroSupporting ||
|
home.heroSupporting ||
|
||||||
seoSettings.defaultDescription ||
|
seoSettings.defaultDescription ||
|
||||||
"Prozessbegleitung, Doula-Begleitung, Kindergruppen und Singkreise — Räume für Verbindung mit dir selbst, miteinander und mit der Natur.",
|
"Prozessbegleitung, Doula-Begleitung, Kindergruppen und Singkreise – Räume für Verbindung mit dir selbst, miteinander und mit der Natur.",
|
||||||
fallbackImage: home.heroImage,
|
fallbackImage: home.heroImage,
|
||||||
defaultOgImage: seoSettings.defaultOgImage,
|
defaultOgImage: seoSettings.defaultOgImage,
|
||||||
});
|
});
|
||||||
@@ -53,6 +55,8 @@ export default async function Home() {
|
|||||||
getUpcomingEvents(3),
|
getUpcomingEvents(3),
|
||||||
]);
|
]);
|
||||||
const groups = groupOffersByCategory(offers);
|
const groups = groupOffersByCategory(offers);
|
||||||
|
const featuredGroup = groups.find((g) => g.category === "prozessbegleitung");
|
||||||
|
const remainingGroups = featuredGroup ? groups.filter((g) => g !== featuredGroup) : groups;
|
||||||
const siteUrl = await getSiteUrl();
|
const siteUrl = await getSiteUrl();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -73,9 +77,9 @@ export default async function Home() {
|
|||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<Button href="/termin-buchen">Termin buchen</Button>
|
<Button href="/termin-buchen">Termin buchen</Button>
|
||||||
<Button href="/angebote" variant="secondary">
|
<ArrowLink href="/angebote" size="base" className="self-center">
|
||||||
Angebote entdecken
|
Angebote entdecken
|
||||||
</Button>
|
</ArrowLink>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -89,24 +93,39 @@ export default async function Home() {
|
|||||||
<SectionHeading
|
<SectionHeading
|
||||||
eyebrow="Angebote"
|
eyebrow="Angebote"
|
||||||
title="Räume, die zu deinem Weg passen"
|
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."
|
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">
|
<div className="mt-14 space-y-6">
|
||||||
{groups.map((group, i) => {
|
{/* Prozessbegleitung ist das Kernangebot – bekommt deshalb den größeren, ersten Platz statt eines gleichwertigen 4er-Rasters. */}
|
||||||
const isSingle = group.offers.length === 1;
|
{featuredGroup && (
|
||||||
const first = group.offers[0];
|
<Reveal>
|
||||||
return (
|
<AngebotCard
|
||||||
<Reveal key={group.category} delay={i * 0.08}>
|
href={featuredGroup.offers.length === 1 ? `/angebote/${featuredGroup.offers[0].slug}` : featuredGroup.href}
|
||||||
<AngebotCard
|
title={featuredGroup.label}
|
||||||
href={isSingle ? `/angebote/${first.slug}` : group.href}
|
tagline={featuredGroup.offers.length === 1 ? featuredGroup.offers[0].shortDescription : `${featuredGroup.offers.length} Angebote`}
|
||||||
title={group.label}
|
mood={moodForOffer(featuredGroup.offers[0])}
|
||||||
tagline={isSingle ? first.shortDescription : `${group.offers.length} Angebote`}
|
imageSrc={mediaUrl(featuredGroup.offers[0].image, "card")}
|
||||||
mood={moodForOffer(first)}
|
size="large"
|
||||||
imageSrc={mediaUrl(first.image, "card")}
|
/>
|
||||||
/>
|
</Reveal>
|
||||||
</Reveal>
|
)}
|
||||||
);
|
<div className="grid gap-6 sm:grid-cols-3">
|
||||||
})}
|
{remainingGroups.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>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
@@ -141,9 +160,7 @@ export default async function Home() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<Button href="/ueber-mich" variant="ghost">
|
<ArrowLink href="/ueber-mich">Mehr über mich lesen</ArrowLink>
|
||||||
Mehr über mich lesen →
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
@@ -151,24 +168,27 @@ export default async function Home() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{home.values && home.values.length > 0 && (
|
{home.values && home.values.length > 0 && (
|
||||||
<Section tone="mauve">
|
<Section tone="mauve" className="relative overflow-hidden">
|
||||||
<SectionHeading
|
<OrganicBlob tone="sage" className="-left-24 top-1/2 h-96 w-96 -translate-y-1/2 opacity-20" />
|
||||||
eyebrow={home.valuesEyebrow ?? undefined}
|
<div className="relative">
|
||||||
title={home.valuesTitle || "Philosophie"}
|
<SectionHeading
|
||||||
align="center"
|
eyebrow={home.valuesEyebrow ?? undefined}
|
||||||
className="mx-auto text-anouma-cream-light [&_h2]:text-anouma-cream-light [&_p]:text-anouma-cream-light/90"
|
title={home.valuesTitle || "Philosophie"}
|
||||||
/>
|
align="center"
|
||||||
<div className="mt-16 grid gap-10 sm:grid-cols-3">
|
className="mx-auto text-anouma-cream-light [&_h2]:text-anouma-cream-light [&_p]:text-anouma-cream-light/90"
|
||||||
{home.values.map((wert, i) => (
|
/>
|
||||||
<Reveal key={wert.id ?? wert.title} delay={i * 0.1} className="text-center">
|
<div className="mt-16 grid gap-10 sm:grid-cols-3">
|
||||||
<p className="font-serif text-sm font-medium uppercase tracking-[0.2em] text-white/85">
|
{home.values.map((wert, i) => (
|
||||||
{wert.title}
|
<Reveal key={wert.id ?? wert.title} delay={i * 0.1} className="text-center">
|
||||||
</p>
|
<p className="font-serif text-sm font-medium uppercase tracking-[0.2em] text-white/85">
|
||||||
<p className="mt-4 text-balance font-serif text-2xl leading-snug italic">
|
{wert.title}
|
||||||
„{wert.quote}“
|
</p>
|
||||||
</p>
|
<p className="mt-4 text-balance font-serif text-2xl leading-snug italic">
|
||||||
</Reveal>
|
„{wert.quote}“
|
||||||
))}
|
</p>
|
||||||
|
</Reveal>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
)}
|
)}
|
||||||
@@ -177,9 +197,7 @@ export default async function Home() {
|
|||||||
<Section tone="warm">
|
<Section tone="warm">
|
||||||
<div className="flex flex-wrap items-end justify-between gap-6">
|
<div className="flex flex-wrap items-end justify-between gap-6">
|
||||||
<SectionHeading eyebrow="Termine" title="Aktuelle Termine" className="mb-0" />
|
<SectionHeading eyebrow="Termine" title="Aktuelle Termine" className="mb-0" />
|
||||||
<Button href="/termine" variant="ghost">
|
<ArrowLink href="/termine">Alle Termine</ArrowLink>
|
||||||
Alle Termine →
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-12 grid gap-6 sm:grid-cols-3">
|
<div className="mt-12 grid gap-6 sm:grid-cols-3">
|
||||||
{events.map((event, i) => (
|
{events.map((event, i) => (
|
||||||
@@ -195,23 +213,31 @@ export default async function Home() {
|
|||||||
<Section tone="cream">
|
<Section tone="cream">
|
||||||
<div className="flex flex-wrap items-end justify-between gap-6">
|
<div className="flex flex-wrap items-end justify-between gap-6">
|
||||||
<SectionHeading eyebrow="Aktuelles" title="Neuigkeiten und Inspiration" className="mb-0" />
|
<SectionHeading eyebrow="Aktuelles" title="Neuigkeiten und Inspiration" className="mb-0" />
|
||||||
<Button href="/aktuelles" variant="ghost">
|
<ArrowLink href="/aktuelles">Alle Neuigkeiten</ArrowLink>
|
||||||
Alle Neuigkeiten →
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-12 grid gap-6 sm:grid-cols-3">
|
<div className="mt-12 grid gap-6 sm:grid-cols-3">
|
||||||
{posts.map((post, i) => (
|
{posts.map((post, i) => (
|
||||||
<Reveal key={post.slug} delay={i * 0.08}>
|
<Reveal key={post.slug} delay={i * 0.08}>
|
||||||
<Link href={`/aktuelles/${post.slug}`} className="block rounded-3xl bg-background p-7">
|
<Link href={`/aktuelles/${post.slug}`} className="group block">
|
||||||
<p className="text-xs font-medium uppercase tracking-[0.18em] text-anouma-plum">
|
<div className="relative aspect-[4/3] w-full overflow-hidden rounded-3xl">
|
||||||
|
<ImagePlaceholder
|
||||||
|
mood="sand"
|
||||||
|
label={post.title}
|
||||||
|
src={mediaUrl(post.coverImage, "card")}
|
||||||
|
alt={mediaAlt(post.coverImage) ?? post.title}
|
||||||
|
shape="soft"
|
||||||
|
className="h-full w-full transition-transform duration-700 group-hover:scale-[1.03]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="mt-4 text-xs font-medium uppercase tracking-[0.18em] text-anouma-plum/70">
|
||||||
{new Date(post.publishDate ?? post.createdAt).toLocaleDateString("de-DE", {
|
{new Date(post.publishDate ?? post.createdAt).toLocaleDateString("de-DE", {
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "long",
|
month: "long",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
<h3 className="mt-3 font-serif text-xl font-medium text-anouma-plum">{post.title}</h3>
|
<h3 className="mt-2 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>
|
<p className="mt-2 text-sm leading-relaxed text-anouma-plum/90">{post.teaser}</p>
|
||||||
</Link>
|
</Link>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
))}
|
))}
|
||||||
@@ -228,13 +254,8 @@ export default async function Home() {
|
|||||||
</Reveal>
|
</Reveal>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-10 text-center">
|
<div className="mt-10 flex justify-center">
|
||||||
<Link
|
<ArrowLink href="/impressionen">Alle Impressionen ansehen</ArrowLink>
|
||||||
href="/impressionen"
|
|
||||||
className="text-sm font-medium text-anouma-mauve-dark underline underline-offset-4"
|
|
||||||
>
|
|
||||||
Alle Impressionen ansehen
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
@@ -242,7 +263,7 @@ export default async function Home() {
|
|||||||
title={home.ctaTitle || "Kennenlernen & Termine vereinbaren"}
|
title={home.ctaTitle || "Kennenlernen & Termine vereinbaren"}
|
||||||
lead={
|
lead={
|
||||||
home.ctaLead ||
|
home.ctaLead ||
|
||||||
"Ich freue mich, von dir zu hören — schreib mir oder buche direkt ein unverbindliches Kennenlerngespräch."
|
"Ich freue mich, von dir zu hören – schreib mir oder buche direkt ein unverbindliches Kennenlerngespräch."
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export default async function RegisterPage() {
|
|||||||
title="Konto erstellen"
|
title="Konto erstellen"
|
||||||
lead="Mit einem Konto kannst du Termine anfragen und behältst alle deine Buchungen im Blick."
|
lead="Mit einem Konto kannst du Termine anfragen und behältst alle deine Buchungen im Blick."
|
||||||
crumbs={[{ title: "Konto erstellen" }]}
|
crumbs={[{ title: "Konto erstellen" }]}
|
||||||
|
compact
|
||||||
/>
|
/>
|
||||||
<Section tone="plain">
|
<Section tone="plain">
|
||||||
<div className="mx-auto max-w-md">
|
<div className="mx-auto max-w-md">
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export default async function TerminBuchenPage() {
|
|||||||
title="Allgemeine Anfrage"
|
title="Allgemeine Anfrage"
|
||||||
lead={
|
lead={
|
||||||
bookableOffers.length > 0
|
bookableOffers.length > 0
|
||||||
? "Für einen konkreten Termin wähle oben ein Angebot — hier kannst du auch einfach eine allgemeine Nachricht schreiben."
|
? "Für einen konkreten Termin wähle oben ein Angebot – hier kannst du auch einfach eine allgemeine Nachricht schreiben."
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
align="left"
|
align="left"
|
||||||
|
|||||||
@@ -126,6 +126,8 @@ export default async function EventDetailPage({ params }: Args) {
|
|||||||
eyebrow={categoryLabel}
|
eyebrow={categoryLabel}
|
||||||
title={event.title}
|
title={event.title}
|
||||||
crumbs={[{ title: "Termine", href: "/termine" }, { title: event.title }]}
|
crumbs={[{ title: "Termine", href: "/termine" }, { title: event.title }]}
|
||||||
|
blobTone="mauve"
|
||||||
|
blobPosition="top-left"
|
||||||
/>
|
/>
|
||||||
<Section tone="plain">
|
<Section tone="plain">
|
||||||
<div className="grid gap-14 lg:grid-cols-[1fr_0.8fr]">
|
<div className="grid gap-14 lg:grid-cols-[1fr_0.8fr]">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { PageHeader } from "@/components/PageHeader";
|
|||||||
import { Section } from "@/components/Section";
|
import { Section } from "@/components/Section";
|
||||||
import { Reveal } from "@/components/Reveal";
|
import { Reveal } from "@/components/Reveal";
|
||||||
import { EventTeaserCard } from "@/components/EventTeaserCard";
|
import { EventTeaserCard } from "@/components/EventTeaserCard";
|
||||||
|
import { ArrowLink } from "@/components/ArrowLink";
|
||||||
import { getAllEvents } from "@/lib/payload/content";
|
import { getAllEvents } from "@/lib/payload/content";
|
||||||
import { buildMetadata } from "@/lib/seo/metadata";
|
import { buildMetadata } from "@/lib/seo/metadata";
|
||||||
import { getSiteUrl } from "@/lib/seo/config";
|
import { getSiteUrl } from "@/lib/seo/config";
|
||||||
@@ -32,14 +33,21 @@ export default async function TerminePage() {
|
|||||||
title="Aktuelle Termine"
|
title="Aktuelle Termine"
|
||||||
lead="Ein Überblick über anstehende Kreise, Begleitungen und Veranstaltungen."
|
lead="Ein Überblick über anstehende Kreise, Begleitungen und Veranstaltungen."
|
||||||
crumbs={[{ title: "Termine" }]}
|
crumbs={[{ title: "Termine" }]}
|
||||||
|
blobTone="mauve"
|
||||||
|
blobPosition="top-left"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Section tone="plain">
|
<Section tone="plain">
|
||||||
{upcoming.length === 0 ? (
|
{upcoming.length === 0 ? (
|
||||||
<p className="text-lg text-anouma-plum">
|
<div className="rounded-3xl bg-anouma-cream-light p-8 text-center sm:p-10">
|
||||||
Aktuell sind keine Termine geplant — schau gerne bald wieder vorbei oder melde dich
|
<p className="font-serif text-xl font-medium text-anouma-plum">Aktuell sind keine Termine geplant</p>
|
||||||
direkt über die <a href="/kontakt" className="underline underline-offset-4">Kontaktseite</a>.
|
<p className="mt-2 text-base leading-relaxed text-anouma-plum/90">
|
||||||
</p>
|
Schau gerne bald wieder vorbei, oder melde dich direkt – ich freue mich von dir zu hören.
|
||||||
|
</p>
|
||||||
|
<div className="mt-5 flex justify-center">
|
||||||
|
<ArrowLink href="/kontakt">Zur Kontaktseite</ArrowLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{upcoming.map((event, i) => (
|
{upcoming.map((event, i) => (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ImagePlaceholder } from "@/components/ImagePlaceholder";
|
import { ImagePlaceholder } from "@/components/ImagePlaceholder";
|
||||||
|
import { ArrowIcon } from "@/components/ArrowIcon";
|
||||||
import type { ImageMood } from "@/lib/angebote";
|
import type { ImageMood } from "@/lib/angebote";
|
||||||
|
|
||||||
type AngebotCardProps = {
|
type AngebotCardProps = {
|
||||||
@@ -42,9 +43,7 @@ export function AngebotCard({
|
|||||||
<p className="mt-2 text-sm leading-relaxed text-white/90">{tagline}</p>
|
<p className="mt-2 text-sm leading-relaxed text-white/90">{tagline}</p>
|
||||||
<span className="mt-4 inline-flex items-center gap-1.5 text-sm font-medium text-white/95">
|
<span className="mt-4 inline-flex items-center gap-1.5 text-sm font-medium text-white/95">
|
||||||
Mehr erfahren
|
Mehr erfahren
|
||||||
<svg width="14" height="10" viewBox="0 0 14 10" fill="none" aria-hidden="true" className="transition-transform duration-300 group-hover:translate-x-1">
|
<ArrowIcon />
|
||||||
<path d="M1 5h11.5M8 1l4.5 4L8 9" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/** The small forward-nudging arrow shared by ArrowLink and the "Mehr erfahren" affordance inside AngebotCard/EventTeaserCard (which can't use ArrowLink directly since those are already whole-tile links, and nesting an anchor inside an anchor is invalid). Purely visual — animates on the parent's `group-hover`. */
|
||||||
|
export function ArrowIcon({ className = "" }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="10"
|
||||||
|
viewBox="0 0 14 10"
|
||||||
|
fill="none"
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`shrink-0 transition-transform duration-300 group-hover:translate-x-1 ${className}`}
|
||||||
|
>
|
||||||
|
<path d="M1 5h11.5M8 1l4.5 4L8 9" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { ArrowIcon } from "@/components/ArrowIcon";
|
||||||
|
|
||||||
|
type Tone = "mauve" | "plum" | "white";
|
||||||
|
type Size = "sm" | "base";
|
||||||
|
|
||||||
|
const tones: Record<Tone, string> = {
|
||||||
|
mauve: "text-anouma-mauve-dark",
|
||||||
|
plum: "text-anouma-plum",
|
||||||
|
white: "text-white",
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizes: Record<Size, string> = {
|
||||||
|
sm: "gap-1.5 text-sm",
|
||||||
|
base: "gap-2 text-base",
|
||||||
|
};
|
||||||
|
|
||||||
|
type ArrowLinkProps = {
|
||||||
|
href: string;
|
||||||
|
children: ReactNode;
|
||||||
|
tone?: Tone;
|
||||||
|
size?: Size;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The site's one secondary-action pattern: a text link with a small arrow
|
||||||
|
* that nudges forward on hover. Used instead of a pill button wherever the
|
||||||
|
* action isn't the single primary one on screen — keeps most CTAs feeling
|
||||||
|
* like an invitation to read on rather than another button to click.
|
||||||
|
*/
|
||||||
|
export function ArrowLink({ href, children, tone = "mauve", size = "sm", className = "" }: ArrowLinkProps) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={href}
|
||||||
|
className={`group inline-flex items-center font-medium ${tones[tone]} ${sizes[size]} ${className}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ArrowIcon />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, type FormEvent } from "react";
|
import { useState, type FormEvent } from "react";
|
||||||
|
import { track } from "@/lib/analytics/client";
|
||||||
const fieldClass =
|
import { TextField, TextAreaField } from "@/components/FormField";
|
||||||
"w-full rounded-2xl border border-anouma-taupe/30 bg-background px-5 py-3.5 text-base text-anouma-plum placeholder:text-anouma-plum/50 focus:border-anouma-mauve-dark focus:outline-none";
|
|
||||||
|
|
||||||
type ContactFormProps = {
|
type ContactFormProps = {
|
||||||
toEmail: string;
|
toEmail: string;
|
||||||
@@ -31,31 +30,19 @@ export function ContactForm({
|
|||||||
const message = String(data.get("message") ?? "");
|
const message = String(data.get("message") ?? "");
|
||||||
|
|
||||||
const subject = encodeURIComponent(`${subjectPrefix} von ${name}`);
|
const subject = encodeURIComponent(`${subjectPrefix} von ${name}`);
|
||||||
const body = encodeURIComponent(`${message}\n\n— ${name} (${email})`);
|
const body = encodeURIComponent(`${message}\n\n– ${name} (${email})`);
|
||||||
window.location.href = `mailto:${toEmail}?subject=${subject}&body=${body}`;
|
window.location.href = `mailto:${toEmail}?subject=${subject}&body=${body}`;
|
||||||
|
// Event only — never the form's name/email/message content, see
|
||||||
|
// lib/analytics/events.ts and the data-minimization requirement.
|
||||||
|
track("contact_clicked");
|
||||||
setSent(true);
|
setSent(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="space-y-5">
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
<div>
|
<TextField label="Name" id="name" name="name" type="text" required autoComplete="name" />
|
||||||
<label htmlFor="name" className="mb-2 block text-sm font-medium text-anouma-plum">
|
<TextField label="E-Mail" id="email" name="email" type="email" required autoComplete="email" />
|
||||||
Name
|
<TextAreaField label="Deine Nachricht" id="message" name="message" required rows={5} />
|
||||||
</label>
|
|
||||||
<input id="name" name="name" type="text" required className={fieldClass} autoComplete="name" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="email" className="mb-2 block text-sm font-medium text-anouma-plum">
|
|
||||||
E-Mail
|
|
||||||
</label>
|
|
||||||
<input id="email" name="email" type="email" required className={fieldClass} autoComplete="email" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="message" className="mb-2 block text-sm font-medium text-anouma-plum">
|
|
||||||
Deine Nachricht
|
|
||||||
</label>
|
|
||||||
<textarea id="message" name="message" required rows={5} className={fieldClass} />
|
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="inline-flex items-center justify-center rounded-full bg-anouma-mauve-dark px-7 py-3.5 text-sm font-medium tracking-wide text-white transition-colors duration-300 hover:bg-anouma-plum"
|
className="inline-flex items-center justify-center rounded-full bg-anouma-mauve-dark px-7 py-3.5 text-sm font-medium tracking-wide text-white transition-colors duration-300 hover:bg-anouma-plum"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import type { Event } from "@/payload-types";
|
import type { Event } from "@/payload-types";
|
||||||
import { formatEventDateParts, formatTimeRange } from "@/lib/format";
|
import { formatEventDateParts, formatTimeRange } from "@/lib/format";
|
||||||
|
import { ArrowIcon } from "@/components/ArrowIcon";
|
||||||
|
|
||||||
export function EventTeaserCard({ event }: { event: Event }) {
|
export function EventTeaserCard({ event }: { event: Event }) {
|
||||||
const { day, month } = formatEventDateParts(event.date);
|
const { day, month } = formatEventDateParts(event.date);
|
||||||
@@ -21,22 +22,7 @@ export function EventTeaserCard({ event }: { event: Event }) {
|
|||||||
{event.location && <p className="text-sm text-anouma-plum/80">{event.location}</p>}
|
{event.location && <p className="text-sm text-anouma-plum/80">{event.location}</p>}
|
||||||
<span className="mt-2 inline-flex items-center gap-1.5 text-sm font-medium text-anouma-mauve-dark">
|
<span className="mt-2 inline-flex items-center gap-1.5 text-sm font-medium text-anouma-mauve-dark">
|
||||||
Mehr erfahren
|
Mehr erfahren
|
||||||
<svg
|
<ArrowIcon />
|
||||||
width="12"
|
|
||||||
height="9"
|
|
||||||
viewBox="0 0 14 10"
|
|
||||||
fill="none"
|
|
||||||
aria-hidden="true"
|
|
||||||
className="transition-transform duration-300 group-hover:translate-x-1"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M1 5h11.5M8 1l4.5 4L8 9"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.4"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export async function Footer() {
|
|||||||
{siteConfig.name.toUpperCase()}
|
{siteConfig.name.toUpperCase()}
|
||||||
</Link>
|
</Link>
|
||||||
<p className="mt-4 max-w-xs text-sm leading-relaxed text-anouma-cream-light/90">
|
<p className="mt-4 max-w-xs text-sm leading-relaxed text-anouma-cream-light/90">
|
||||||
Räume für Verbindung — mit dir selbst, miteinander und mit der Natur.
|
Räume für Verbindung – mit dir selbst, miteinander und mit der Natur.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import type { InputHTMLAttributes, ReactNode, TextareaHTMLAttributes } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The one field style used by every form on the site (previously each form
|
||||||
|
* declared its own near-identical `fieldClass` constant with small drifts in
|
||||||
|
* padding/size/background — see ContactForm, auth/*, meeting/* history).
|
||||||
|
* Functionality is untouched: both components just spread the native
|
||||||
|
* input/textarea props through, so controlled and uncontrolled forms,
|
||||||
|
* validation attributes, autoComplete, etc. all keep working exactly as
|
||||||
|
* before.
|
||||||
|
*/
|
||||||
|
const fieldClass =
|
||||||
|
"w-full rounded-2xl border border-anouma-taupe/30 bg-white px-5 py-3.5 text-base text-anouma-plum placeholder:text-anouma-plum/50 focus:border-anouma-mauve-dark focus:outline-none";
|
||||||
|
|
||||||
|
function FieldLabel({ label, htmlFor, hidden = false }: { label: string; htmlFor: string; hidden?: boolean }) {
|
||||||
|
return (
|
||||||
|
<label htmlFor={htmlFor} className={hidden ? "sr-only" : "mb-2 block text-sm font-medium text-anouma-plum"}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type TextFieldProps = InputHTMLAttributes<HTMLInputElement> & {
|
||||||
|
label: string;
|
||||||
|
id: string;
|
||||||
|
/** Keeps the label for screen readers only — for compact inline fields identified by a placeholder instead (e.g. ResendVerificationButton's email prompt). */
|
||||||
|
hideLabel?: boolean;
|
||||||
|
wrapperClassName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function TextField({ label, id, hideLabel = false, className = "", wrapperClassName = "", ...inputProps }: TextFieldProps) {
|
||||||
|
return (
|
||||||
|
<div className={wrapperClassName}>
|
||||||
|
<FieldLabel label={label} htmlFor={id} hidden={hideLabel} />
|
||||||
|
<input id={id} className={`${fieldClass} ${className}`} {...inputProps} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type TextAreaFieldProps = TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
||||||
|
label: string;
|
||||||
|
id: string;
|
||||||
|
wrapperClassName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function TextAreaField({ label, id, className = "", wrapperClassName = "", ...textareaProps }: TextAreaFieldProps) {
|
||||||
|
return (
|
||||||
|
<div className={wrapperClassName}>
|
||||||
|
<FieldLabel label={label} htmlFor={id} />
|
||||||
|
<textarea id={id} className={`${fieldClass} ${className}`} {...textareaProps} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shared error-message treatment (`role="alert"`) used below every form field/submit button. */
|
||||||
|
export function FormError({ children, className = "" }: { children: ReactNode; className?: string }) {
|
||||||
|
return (
|
||||||
|
<p role="alert" className={`text-sm text-red-700 ${className}`}>
|
||||||
|
{children}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
+1
-1
@@ -40,7 +40,7 @@ export function Hero({ eyebrow, title, children, actions, mood = "rose", imageSr
|
|||||||
<div className="relative mx-auto aspect-[4/5] w-full max-w-md animate-fade-in [animation-delay:200ms] lg:max-w-none">
|
<div className="relative mx-auto aspect-[4/5] w-full max-w-md animate-fade-in [animation-delay:200ms] lg:max-w-none">
|
||||||
<ImagePlaceholder
|
<ImagePlaceholder
|
||||||
mood={mood}
|
mood={mood}
|
||||||
label="Anouma — warme, naturverbundene Begleitung"
|
label="Anouma – warme, naturverbundene Begleitung"
|
||||||
src={imageSrc}
|
src={imageSrc}
|
||||||
alt={imageAlt}
|
alt={imageAlt}
|
||||||
priority
|
priority
|
||||||
|
|||||||
+57
-48
@@ -6,6 +6,8 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
import { groupOffersByCategory } from "@/lib/angebote";
|
import { groupOffersByCategory } from "@/lib/angebote";
|
||||||
import { mainNav, ctaNav, siteConfig } from "@/lib/site";
|
import { mainNav, ctaNav, siteConfig } from "@/lib/site";
|
||||||
|
import { track } from "@/lib/analytics/client";
|
||||||
|
import { OrganicBlob } from "@/components/OrganicBlob";
|
||||||
import type { Offer } from "@/payload-types";
|
import type { Offer } from "@/payload-types";
|
||||||
|
|
||||||
export function Navbar({ offers, customerName }: { offers: Offer[]; customerName?: string | null }) {
|
export function Navbar({ offers, customerName }: { offers: Offer[]; customerName?: string | null }) {
|
||||||
@@ -63,7 +65,7 @@ export function Navbar({ offers, customerName }: { offers: Offer[]; customerName
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="sticky top-0 z-50 border-b border-anouma-taupe/15 bg-background/85 backdrop-blur-md">
|
<header className="sticky top-0 z-50 border-b border-anouma-taupe/15 bg-background">
|
||||||
<div className="mx-auto flex h-20 max-w-6xl items-center justify-between px-6 sm:px-8 lg:px-12">
|
<div className="mx-auto flex h-20 max-w-6xl items-center justify-between px-6 sm:px-8 lg:px-12">
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
@@ -108,52 +110,55 @@ export function Navbar({ offers, customerName }: { offers: Offer[]; customerName
|
|||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, y: 8 }}
|
exit={{ opacity: 0, y: 8 }}
|
||||||
transition={{ duration: 0.22, ease: "easeOut" }}
|
transition={{ duration: 0.22, ease: "easeOut" }}
|
||||||
className="absolute left-1/2 top-full z-50 mt-3 w-[min(640px,92vw)] -translate-x-1/2 rounded-3xl border border-anouma-taupe/15 bg-background p-8 shadow-xl shadow-anouma-plum/10"
|
className="absolute left-1/2 top-full z-50 mt-3 w-[min(640px,92vw)] -translate-x-1/2 overflow-hidden rounded-3xl border border-anouma-taupe/15 bg-background p-8 shadow-lg shadow-anouma-plum/10"
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-2 gap-8 sm:grid-cols-4">
|
<OrganicBlob tone="cream" className="-right-16 -top-16 h-52 w-52 opacity-30" />
|
||||||
{groups.map((group) => {
|
<div className="relative">
|
||||||
const isSingle = group.offers.length === 1;
|
<div className="grid grid-cols-2 gap-8 sm:grid-cols-4">
|
||||||
const groupHref = isSingle
|
{groups.map((group) => {
|
||||||
? `/angebote/${group.offers[0].slug}`
|
const isSingle = group.offers.length === 1;
|
||||||
: group.href;
|
const groupHref = isSingle
|
||||||
return (
|
? `/angebote/${group.offers[0].slug}`
|
||||||
<div key={group.category}>
|
: group.href;
|
||||||
<Link
|
return (
|
||||||
href={groupHref}
|
<div key={group.category}>
|
||||||
className="font-serif text-lg font-medium text-anouma-plum hover:text-anouma-mauve-dark"
|
<Link
|
||||||
>
|
href={groupHref}
|
||||||
{group.label}
|
className="font-serif text-lg font-medium text-anouma-plum hover:text-anouma-mauve-dark"
|
||||||
</Link>
|
>
|
||||||
{!isSingle && (
|
{group.label}
|
||||||
<ul className="mt-3 space-y-2">
|
</Link>
|
||||||
{group.offers.map((offer) => (
|
{!isSingle && (
|
||||||
<li key={offer.slug}>
|
<ul className="mt-3 space-y-2">
|
||||||
<Link
|
{group.offers.map((offer) => (
|
||||||
href={`/angebote/${offer.slug}`}
|
<li key={offer.slug}>
|
||||||
className="text-sm text-anouma-plum transition-colors hover:text-anouma-mauve-dark"
|
<Link
|
||||||
>
|
href={`/angebote/${offer.slug}`}
|
||||||
{offer.title}
|
className="text-sm text-anouma-plum transition-colors hover:text-anouma-mauve-dark"
|
||||||
</Link>
|
>
|
||||||
</li>
|
{offer.title}
|
||||||
))}
|
</Link>
|
||||||
</ul>
|
</li>
|
||||||
)}
|
))}
|
||||||
{isSingle && (
|
</ul>
|
||||||
<p className="mt-3 text-sm leading-relaxed text-anouma-plum">
|
)}
|
||||||
{group.offers[0].shortDescription}
|
{isSingle && (
|
||||||
</p>
|
<p className="mt-3 text-sm leading-relaxed text-anouma-plum">
|
||||||
)}
|
{group.offers[0].shortDescription}
|
||||||
</div>
|
</p>
|
||||||
);
|
)}
|
||||||
})}
|
</div>
|
||||||
</div>
|
);
|
||||||
<div className="mt-7 border-t border-anouma-taupe/15 pt-5">
|
})}
|
||||||
<Link
|
</div>
|
||||||
href="/angebote"
|
<div className="mt-7 border-t border-anouma-taupe/15 pt-5">
|
||||||
className="text-sm font-medium text-anouma-mauve-dark underline underline-offset-4"
|
<Link
|
||||||
>
|
href="/angebote"
|
||||||
Alle Angebote im Überblick
|
className="text-sm font-medium text-anouma-mauve-dark underline underline-offset-4"
|
||||||
</Link>
|
>
|
||||||
|
Alle Angebote im Überblick
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
@@ -163,8 +168,9 @@ export function Navbar({ offers, customerName }: { offers: Offer[]; customerName
|
|||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className={`text-[15px] font-medium transition-colors hover:text-anouma-mauve-dark ${
|
onClick={() => track("navigation_clicked", { path: item.href })}
|
||||||
pathname === item.href ? "text-anouma-mauve-dark" : "text-anouma-plum/90"
|
className={`relative pb-1 text-[15px] font-medium transition-colors after:absolute after:bottom-0 after:left-0 after:h-px after:bg-anouma-mauve-dark after:transition-all after:duration-300 after:content-[''] hover:text-anouma-mauve-dark hover:after:w-full ${
|
||||||
|
pathname === item.href ? "text-anouma-mauve-dark after:w-full" : "text-anouma-plum/90 after:w-0"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
@@ -182,6 +188,7 @@ export function Navbar({ offers, customerName }: { offers: Offer[]; customerName
|
|||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href={ctaNav.href}
|
href={ctaNav.href}
|
||||||
|
onClick={() => track("navigation_clicked", { path: ctaNav.href })}
|
||||||
className="rounded-full bg-anouma-mauve-dark px-6 py-2.5 text-sm font-medium text-white transition-colors hover:bg-anouma-plum"
|
className="rounded-full bg-anouma-mauve-dark px-6 py-2.5 text-sm font-medium text-white transition-colors hover:bg-anouma-plum"
|
||||||
>
|
>
|
||||||
{ctaNav.title}
|
{ctaNav.title}
|
||||||
@@ -300,6 +307,7 @@ export function Navbar({ offers, customerName }: { offers: Offer[]; customerName
|
|||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
|
onClick={() => track("navigation_clicked", { path: item.href })}
|
||||||
className="border-b border-anouma-taupe/10 py-4 text-lg font-medium text-anouma-plum"
|
className="border-b border-anouma-taupe/10 py-4 text-lg font-medium text-anouma-plum"
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
@@ -314,6 +322,7 @@ export function Navbar({ offers, customerName }: { offers: Offer[]; customerName
|
|||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href={ctaNav.href}
|
href={ctaNav.href}
|
||||||
|
onClick={() => track("navigation_clicked", { path: ctaNav.href })}
|
||||||
className="mt-6 rounded-full bg-anouma-mauve-dark px-6 py-4 text-center text-base font-medium text-white"
|
className="mt-6 rounded-full bg-anouma-mauve-dark px-6 py-4 text-center text-base font-medium text-white"
|
||||||
>
|
>
|
||||||
{ctaNav.title}
|
{ctaNav.title}
|
||||||
|
|||||||
@@ -9,10 +9,23 @@ type PageHeaderProps = {
|
|||||||
title: string;
|
title: string;
|
||||||
lead?: ReactNode;
|
lead?: ReactNode;
|
||||||
crumbs?: Crumb[];
|
crumbs?: Crumb[];
|
||||||
|
/** Varies the header's decorative blob so every interior page doesn't open with the exact same rose-top-right treatment. */
|
||||||
|
blobTone?: "rose" | "sage" | "cream" | "mauve";
|
||||||
|
blobPosition?: "top-right" | "top-left";
|
||||||
|
/** Shorter vertical padding for pages that don't need much ceremony (e.g. Kontakt). */
|
||||||
|
compact?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Renders the visual breadcrumb trail AND its matching BreadcrumbList JSON-LD from the same `crumbs` — every page that already passes `crumbs` gets structured breadcrumb data for free. */
|
/** Renders the visual breadcrumb trail AND its matching BreadcrumbList JSON-LD from the same `crumbs` — every page that already passes `crumbs` gets structured breadcrumb data for free. */
|
||||||
export async function PageHeader({ eyebrow, title, lead, crumbs }: PageHeaderProps) {
|
export async function PageHeader({
|
||||||
|
eyebrow,
|
||||||
|
title,
|
||||||
|
lead,
|
||||||
|
crumbs,
|
||||||
|
blobTone = "rose",
|
||||||
|
blobPosition = "top-right",
|
||||||
|
compact = false,
|
||||||
|
}: PageHeaderProps) {
|
||||||
const breadcrumbData = crumbs?.length
|
const breadcrumbData = crumbs?.length
|
||||||
? await (async () => {
|
? await (async () => {
|
||||||
const siteUrl = await getSiteUrl();
|
const siteUrl = await getSiteUrl();
|
||||||
@@ -24,9 +37,15 @@ export async function PageHeader({ eyebrow, title, lead, crumbs }: PageHeaderPro
|
|||||||
})()
|
})()
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const blobPositionClass = blobPosition === "top-left" ? "-left-20 -top-20" : "-right-20 -top-20";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative overflow-hidden border-b border-anouma-taupe/10 bg-anouma-cream-light pb-16 pt-14 sm:pb-20 sm:pt-20">
|
<section
|
||||||
<OrganicBlob tone="rose" className="-right-20 -top-20 h-72 w-72" />
|
className={`relative overflow-hidden border-b border-anouma-taupe/10 bg-anouma-cream-light ${
|
||||||
|
compact ? "pb-10 pt-10 sm:pb-12 sm:pt-12" : "pb-16 pt-14 sm:pb-20 sm:pt-20"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<OrganicBlob tone={blobTone} className={`${blobPositionClass} h-72 w-72`} />
|
||||||
<div className="relative mx-auto max-w-6xl px-6 sm:px-8 lg:px-12">
|
<div className="relative mx-auto max-w-6xl px-6 sm:px-8 lg:px-12">
|
||||||
{breadcrumbData && <JsonLd data={breadcrumbData} />}
|
{breadcrumbData && <JsonLd data={breadcrumbData} />}
|
||||||
{crumbs && <Breadcrumbs items={crumbs} />}
|
{crumbs && <Breadcrumbs items={crumbs} />}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { ReactNode } from "react";
|
|||||||
export function PlaceholderNote({ children }: { children: ReactNode }) {
|
export function PlaceholderNote({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-2xl border border-dashed border-anouma-dustyrose/50 bg-anouma-rose-pale/20 px-5 py-4 text-sm leading-relaxed text-anouma-plum">
|
<div className="rounded-2xl border border-dashed border-anouma-dustyrose/50 bg-anouma-rose-pale/20 px-5 py-4 text-sm leading-relaxed text-anouma-plum">
|
||||||
<span className="font-medium">Platzhalter — noch zu ergänzen:</span> {children}
|
<span className="font-medium">Platzhalter – noch zu ergänzen:</span> {children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,7 @@
|
|||||||
import { useState, type FormEvent } from "react";
|
import { useState, type FormEvent } from "react";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { ResendVerificationButton } from "./ResendVerificationButton";
|
import { ResendVerificationButton } from "./ResendVerificationButton";
|
||||||
|
import { TextField, FormError } from "@/components/FormField";
|
||||||
const fieldClass =
|
|
||||||
"w-full rounded-2xl border border-anouma-taupe/30 bg-white px-5 py-3.5 text-base text-anouma-plum placeholder:text-anouma-plum/50 focus:border-anouma-mauve-dark focus:outline-none";
|
|
||||||
|
|
||||||
export function LoginForm() {
|
export function LoginForm() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -44,23 +42,9 @@ export function LoginForm() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="space-y-5">
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
<div>
|
<TextField label="E-Mail" id="email" name="email" type="email" required autoComplete="email" />
|
||||||
<label htmlFor="email" className="mb-2 block text-sm font-medium text-anouma-plum">
|
<TextField label="Passwort" id="password" name="password" type="password" required autoComplete="current-password" />
|
||||||
E-Mail
|
{error && <FormError>{error}</FormError>}
|
||||||
</label>
|
|
||||||
<input id="email" name="email" type="email" required className={fieldClass} autoComplete="email" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="password" className="mb-2 block text-sm font-medium text-anouma-plum">
|
|
||||||
Passwort
|
|
||||||
</label>
|
|
||||||
<input id="password" name="password" type="password" required className={fieldClass} autoComplete="current-password" />
|
|
||||||
</div>
|
|
||||||
{error && (
|
|
||||||
<p role="alert" className="text-sm text-red-700">
|
|
||||||
{error}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
|
|
||||||
import { useState, type FormEvent } from "react";
|
import { useState, type FormEvent } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { TextField } from "@/components/FormField";
|
||||||
const fieldClass =
|
|
||||||
"w-full rounded-2xl border border-anouma-taupe/30 bg-white px-5 py-3.5 text-base text-anouma-plum placeholder:text-anouma-plum/50 focus:border-anouma-mauve-dark focus:outline-none";
|
|
||||||
|
|
||||||
export function ProfileForm({ id, name, phone }: { id: number; name: string; phone: string }) {
|
export function ProfileForm({ id, name, phone }: { id: number; name: string; phone: string }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -35,18 +33,8 @@ export function ProfileForm({ id, name, phone }: { id: number; name: string; pho
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="space-y-5">
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
<div>
|
<TextField label="Name" id="name" name="name" type="text" required defaultValue={name} />
|
||||||
<label htmlFor="name" className="mb-2 block text-sm font-medium text-anouma-plum">
|
<TextField label="Telefonnummer (optional)" id="phone" name="phone" type="tel" defaultValue={phone} />
|
||||||
Name
|
|
||||||
</label>
|
|
||||||
<input id="name" name="name" type="text" required defaultValue={name} className={fieldClass} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="phone" className="mb-2 block text-sm font-medium text-anouma-plum">
|
|
||||||
Telefonnummer (optional)
|
|
||||||
</label>
|
|
||||||
<input id="phone" name="phone" type="tel" defaultValue={phone} className={fieldClass} />
|
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={status === "saving"}
|
disabled={status === "saving"}
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
|
|
||||||
import { useState, type FormEvent } from "react";
|
import { useState, type FormEvent } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { TextField, FormError } from "@/components/FormField";
|
||||||
const fieldClass =
|
|
||||||
"w-full rounded-2xl border border-anouma-taupe/30 bg-white px-5 py-3.5 text-base text-anouma-plum placeholder:text-anouma-plum/50 focus:border-anouma-mauve-dark focus:outline-none";
|
|
||||||
|
|
||||||
export function RegisterForm() {
|
export function RegisterForm() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -54,37 +52,18 @@ export function RegisterForm() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="space-y-5">
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
<div>
|
<TextField label="Name" id="name" name="name" type="text" required autoComplete="name" />
|
||||||
<label htmlFor="name" className="mb-2 block text-sm font-medium text-anouma-plum">
|
<TextField label="E-Mail" id="email" name="email" type="email" required autoComplete="email" />
|
||||||
Name
|
<TextField
|
||||||
</label>
|
label="Passwort"
|
||||||
<input id="name" name="name" type="text" required className={fieldClass} autoComplete="name" />
|
id="password"
|
||||||
</div>
|
name="password"
|
||||||
<div>
|
type="password"
|
||||||
<label htmlFor="email" className="mb-2 block text-sm font-medium text-anouma-plum">
|
required
|
||||||
E-Mail
|
minLength={8}
|
||||||
</label>
|
autoComplete="new-password"
|
||||||
<input id="email" name="email" type="email" required className={fieldClass} autoComplete="email" />
|
/>
|
||||||
</div>
|
{error && <FormError>{error}</FormError>}
|
||||||
<div>
|
|
||||||
<label htmlFor="password" className="mb-2 block text-sm font-medium text-anouma-plum">
|
|
||||||
Passwort
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
minLength={8}
|
|
||||||
className={fieldClass}
|
|
||||||
autoComplete="new-password"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{error && (
|
|
||||||
<p role="alert" className="text-sm text-red-700">
|
|
||||||
{error}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, type FormEvent } from "react";
|
import { useId, useState, type FormEvent } from "react";
|
||||||
|
import { TextField } from "@/components/FormField";
|
||||||
const fieldClass =
|
|
||||||
"w-full rounded-2xl border border-anouma-taupe/30 bg-white px-4 py-2.5 text-sm text-anouma-plum placeholder:text-anouma-plum/50 focus:border-anouma-mauve-dark focus:outline-none";
|
|
||||||
|
|
||||||
async function callResend(email?: string): Promise<{ ok: boolean; error?: string }> {
|
async function callResend(email?: string): Promise<{ ok: boolean; error?: string }> {
|
||||||
try {
|
try {
|
||||||
@@ -44,6 +42,7 @@ export function ResendVerificationButton({
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [status, setStatus] = useState<"idle" | "sending" | "sent" | "error">("idle");
|
const [status, setStatus] = useState<"idle" | "sending" | "sent" | "error">("idle");
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const emailFieldId = useId();
|
||||||
|
|
||||||
async function resendForSession() {
|
async function resendForSession() {
|
||||||
setStatus("sending");
|
setStatus("sending");
|
||||||
@@ -108,12 +107,15 @@ export function ResendVerificationButton({
|
|||||||
<p className="text-sm text-anouma-olive">{GENERIC_SENT_MESSAGE}</p>
|
<p className="text-sm text-anouma-olive">{GENERIC_SENT_MESSAGE}</p>
|
||||||
) : (
|
) : (
|
||||||
<form onSubmit={handleEmailSubmit} className="mt-2 flex flex-wrap items-start gap-2">
|
<form onSubmit={handleEmailSubmit} className="mt-2 flex flex-wrap items-start gap-2">
|
||||||
<input
|
<TextField
|
||||||
|
label="Deine E-Mail-Adresse"
|
||||||
|
hideLabel
|
||||||
|
id={emailFieldId}
|
||||||
type="email"
|
type="email"
|
||||||
name="email"
|
name="email"
|
||||||
required
|
required
|
||||||
placeholder="Deine E-Mail-Adresse"
|
placeholder="Deine E-Mail-Adresse"
|
||||||
className={`${fieldClass} max-w-xs flex-1`}
|
wrapperClassName="max-w-xs flex-1"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export function BookingCard({ booking }: { booking: BookingRequest }) {
|
|||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
{booking.appointmentType === "onsite" && (
|
{booking.appointmentType === "onsite" && (
|
||||||
<p className="text-sm text-anouma-plum/80">Vor Ort — Details siehe Bestätigungs-E-Mail.</p>
|
<p className="text-sm text-anouma-plum/80">Vor Ort – Details siehe Bestätigungs-E-Mail.</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -107,7 +107,7 @@ export function BookingCard({ booking }: { booking: BookingRequest }) {
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
onClick={() => callAction(`/api/booking/${booking.id}/cancel`)}
|
onClick={() => callAction(`/api/booking/${booking.id}/cancel`)}
|
||||||
className="mt-4 text-sm font-medium text-anouma-plum/60 underline underline-offset-4 hover:text-red-700"
|
className="-ml-2 mt-4 inline-flex min-h-11 items-center rounded-full px-2 text-sm font-medium text-anouma-plum/60 underline underline-offset-4 transition-colors hover:bg-anouma-cream-light hover:text-red-700"
|
||||||
>
|
>
|
||||||
Termin stornieren
|
Termin stornieren
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { MonthCalendar, type CalendarMarker } from "./MonthCalendar";
|
import { MonthCalendar, type CalendarMarker } from "./MonthCalendar";
|
||||||
import { ResendVerificationButton } from "@/components/auth/ResendVerificationButton";
|
import { ResendVerificationButton } from "@/components/auth/ResendVerificationButton";
|
||||||
|
import { TextAreaField, FormError } from "@/components/FormField";
|
||||||
|
import { track } from "@/lib/analytics/client";
|
||||||
|
|
||||||
type SlotsResponse = { durationMinutes: number; slots: Record<string, { start: string; end: string }[]> };
|
type SlotsResponse = { durationMinutes: number; slots: Record<string, { start: string; end: string }[]> };
|
||||||
|
|
||||||
@@ -116,6 +118,8 @@ export function BookingWidget({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setResult({ ok: true });
|
setResult({ ok: true });
|
||||||
|
// Event only — never the offer/date/message content.
|
||||||
|
track("booking_started");
|
||||||
} catch {
|
} catch {
|
||||||
setResult({ ok: false, error: "Verbindung fehlgeschlagen." });
|
setResult({ ok: false, error: "Verbindung fehlgeschlagen." });
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
@@ -144,7 +148,7 @@ export function BookingWidget({
|
|||||||
key={slot.start}
|
key={slot.start}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSelectedSlot(slot.start)}
|
onClick={() => setSelectedSlot(slot.start)}
|
||||||
className={`rounded-full border px-4 py-2 text-sm font-medium transition-colors ${
|
className={`inline-flex min-h-11 items-center justify-center rounded-full border px-4 text-sm font-medium transition-colors ${
|
||||||
selectedSlot === slot.start
|
selectedSlot === slot.start
|
||||||
? "border-anouma-mauve-dark bg-anouma-mauve-dark text-white"
|
? "border-anouma-mauve-dark bg-anouma-mauve-dark text-white"
|
||||||
: "border-anouma-taupe/40 text-anouma-plum hover:border-anouma-mauve-dark"
|
: "border-anouma-taupe/40 text-anouma-plum hover:border-anouma-mauve-dark"
|
||||||
@@ -189,24 +193,16 @@ export function BookingWidget({
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div className="mt-4">
|
<TextAreaField
|
||||||
<label htmlFor="booking-message" className="mb-1.5 block text-sm font-medium text-anouma-plum">
|
label="Nachricht (optional)"
|
||||||
Nachricht (optional)
|
id="booking-message"
|
||||||
</label>
|
value={message}
|
||||||
<textarea
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
id="booking-message"
|
rows={3}
|
||||||
value={message}
|
wrapperClassName="mt-4"
|
||||||
onChange={(e) => setMessage(e.target.value)}
|
/>
|
||||||
rows={3}
|
|
||||||
className="w-full rounded-2xl border border-anouma-taupe/30 bg-white px-4 py-3 text-sm text-anouma-plum focus:border-anouma-mauve-dark focus:outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{result?.error && (
|
{result?.error && <FormError className="mt-3">{result.error}</FormError>}
|
||||||
<p role="alert" className="mt-3 text-sm text-red-700">
|
|
||||||
{result.error}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -66,13 +66,13 @@ export function MonthCalendar({
|
|||||||
const today = dateKey(new Date());
|
const today = dateKey(new Date());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-3xl bg-white p-6">
|
<div className="rounded-3xl bg-white p-4 sm:p-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setMonth(new Date(month.getFullYear(), month.getMonth() - 1, 1))}
|
onClick={() => setMonth(new Date(month.getFullYear(), month.getMonth() - 1, 1))}
|
||||||
aria-label="Vorheriger Monat"
|
aria-label="Vorheriger Monat"
|
||||||
className="flex h-9 w-9 items-center justify-center rounded-full text-anouma-plum hover:bg-anouma-cream-light"
|
className="flex h-11 w-11 items-center justify-center rounded-full text-anouma-plum hover:bg-anouma-cream-light"
|
||||||
>
|
>
|
||||||
‹
|
‹
|
||||||
</button>
|
</button>
|
||||||
@@ -81,7 +81,7 @@ export function MonthCalendar({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setMonth(new Date(month.getFullYear(), month.getMonth() + 1, 1))}
|
onClick={() => setMonth(new Date(month.getFullYear(), month.getMonth() + 1, 1))}
|
||||||
aria-label="Nächster Monat"
|
aria-label="Nächster Monat"
|
||||||
className="flex h-9 w-9 items-center justify-center rounded-full text-anouma-plum hover:bg-anouma-cream-light"
|
className="flex h-11 w-11 items-center justify-center rounded-full text-anouma-plum hover:bg-anouma-cream-light"
|
||||||
>
|
>
|
||||||
›
|
›
|
||||||
</button>
|
</button>
|
||||||
@@ -106,9 +106,9 @@ export function MonthCalendar({
|
|||||||
key={j}
|
key={j}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onSelectDay?.(day)}
|
onClick={() => onSelectDay?.(day)}
|
||||||
className={`flex aspect-square flex-col items-center justify-center rounded-xl text-sm transition-colors ${
|
className={`flex aspect-square min-h-11 flex-col items-center justify-center rounded-xl text-sm transition-colors ${
|
||||||
isSelected ? "bg-anouma-mauve-dark text-white" : "text-anouma-plum hover:bg-anouma-cream-light"
|
isSelected ? "bg-anouma-mauve-dark text-white" : "text-anouma-plum hover:bg-anouma-cream-light"
|
||||||
} ${key === today && !isSelected ? "font-semibold" : ""}`}
|
} ${key === today && !isSelected ? "font-semibold ring-1 ring-inset ring-anouma-mauve-dark/50" : ""}`}
|
||||||
>
|
>
|
||||||
<span>{day.getDate()}</span>
|
<span>{day.getDate()}</span>
|
||||||
{dayMarkers.length > 0 && (
|
{dayMarkers.length > 0 && (
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, type FormEvent } from "react";
|
import { useState, type FormEvent } from "react";
|
||||||
|
import { TextField, FormError } from "@/components/FormField";
|
||||||
const fieldClass =
|
|
||||||
"w-full rounded-2xl border border-anouma-taupe/30 bg-white px-5 py-3 text-base text-anouma-plum placeholder:text-anouma-plum/50 focus:border-anouma-mauve-dark focus:outline-none";
|
|
||||||
|
|
||||||
export function RegisterForm({ slug }: { slug: string }) {
|
export function RegisterForm({ slug }: { slug: string }) {
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
@@ -37,46 +35,32 @@ export function RegisterForm({ slug }: { slug: string }) {
|
|||||||
if (status === "done") {
|
if (status === "done") {
|
||||||
return (
|
return (
|
||||||
<p className="rounded-2xl bg-anouma-cream-light p-5 text-sm leading-relaxed text-anouma-plum" role="status">
|
<p className="rounded-2xl bg-anouma-cream-light p-5 text-sm leading-relaxed text-anouma-plum" role="status">
|
||||||
Danke für deine Anmeldung — wir freuen uns auf dich!
|
Danke für deine Anmeldung – wir freuen uns auf dich!
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<div>
|
<TextField
|
||||||
<label htmlFor="reg-name" className="mb-1.5 block text-sm font-medium text-anouma-plum">
|
label="Name"
|
||||||
Name
|
id="reg-name"
|
||||||
</label>
|
type="text"
|
||||||
<input
|
required
|
||||||
id="reg-name"
|
value={name}
|
||||||
type="text"
|
onChange={(e) => setName(e.target.value)}
|
||||||
required
|
autoComplete="name"
|
||||||
value={name}
|
/>
|
||||||
onChange={(e) => setName(e.target.value)}
|
<TextField
|
||||||
className={fieldClass}
|
label="E-Mail"
|
||||||
autoComplete="name"
|
id="reg-email"
|
||||||
/>
|
type="email"
|
||||||
</div>
|
required
|
||||||
<div>
|
value={email}
|
||||||
<label htmlFor="reg-email" className="mb-1.5 block text-sm font-medium text-anouma-plum">
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
E-Mail
|
autoComplete="email"
|
||||||
</label>
|
/>
|
||||||
<input
|
{error && <FormError>{error}</FormError>}
|
||||||
id="reg-email"
|
|
||||||
type="email"
|
|
||||||
required
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
className={fieldClass}
|
|
||||||
autoComplete="email"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{error && (
|
|
||||||
<p role="alert" className="text-sm text-red-700">
|
|
||||||
{error}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={status === "loading"}
|
disabled={status === "loading"}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export function bookingRejectedEmail(args: BaseArgs): { subject: string; html: s
|
|||||||
{ label: "Datum", value: args.dateLabel },
|
{ label: "Datum", value: args.dateLabel },
|
||||||
{ label: "Uhrzeit", value: args.timeLabel },
|
{ label: "Uhrzeit", value: args.timeLabel },
|
||||||
]),
|
]),
|
||||||
paragraph("Melde dich gerne für einen neuen Termin — schau einfach wieder in deinem Konto vorbei.", { small: true }),
|
paragraph("Melde dich gerne für einen neuen Termin – schau einfach wieder in deinem Konto vorbei.", { small: true }),
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
});
|
});
|
||||||
return { subject: "Deine Terminanfrage bei ANOUMA", html };
|
return { subject: "Deine Terminanfrage bei ANOUMA", html };
|
||||||
|
|||||||
+1
-1
@@ -41,7 +41,7 @@ export function renderFactBox(rows: { label: string; value: string }[]): string
|
|||||||
|
|
||||||
/** Shared table-based, inline-styled email shell — same design language across all ANOUMA emails. */
|
/** Shared table-based, inline-styled email shell — same design language across all ANOUMA emails. */
|
||||||
export function renderEmailShell(args: { preheader?: string; bodyHtml: string; footerText?: string }): string {
|
export function renderEmailShell(args: { preheader?: string; bodyHtml: string; footerText?: string }): string {
|
||||||
const { bodyHtml, footerText = "Räume für Verbindung — mit dir selbst, miteinander und mit der Natur." } = args;
|
const { bodyHtml, footerText = "Räume für Verbindung – mit dir selbst, miteinander und mit der Natur." } = args;
|
||||||
|
|
||||||
return `<!doctype html>
|
return `<!doctype html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export function renderReminderEmailHtml(data: ReminderEmailData): string {
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding:20px 32px;background:${colors.cream};text-align:center;">
|
<td style="padding:20px 32px;background:${colors.cream};text-align:center;">
|
||||||
<span style="color:${colors.plum};font-size:12px;">Räume für Verbindung — mit dir selbst, miteinander und mit der Natur.</span>
|
<span style="color:${colors.plum};font-size:12px;">Räume für Verbindung – mit dir selbst, miteinander und mit der Natur.</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
export const siteConfig = {
|
export const siteConfig = {
|
||||||
name: "Anouma",
|
name: "Anouma",
|
||||||
domain: "https://anouma.org",
|
domain: "https://anouma.org",
|
||||||
title: "Anouma — Prozessbegleitung, Doula & Gemeinschaft",
|
title: "Anouma – Prozessbegleitung, Doula & Gemeinschaft",
|
||||||
description:
|
description:
|
||||||
"Anouma begleitet dich in Lebensphasen, Schwangerschaft und Geburt sowie in Kinder- und Singkreisen — mit Naturverbundenheit, Vertrauen und einem geschützten Raum für deinen eigenen Weg.",
|
"Anouma begleitet dich in Lebensphasen, Schwangerschaft und Geburt sowie in Kinder- und Singkreisen – mit Naturverbundenheit, Vertrauen und einem geschützten Raum für deinen eigenen Weg.",
|
||||||
locale: "de_DE",
|
locale: "de_DE",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
+9
-9
@@ -95,7 +95,7 @@ async function seedOffers(payload: Awaited<ReturnType<typeof getPayload>>) {
|
|||||||
bookable: true,
|
bookable: true,
|
||||||
price: "auf Anfrage",
|
price: "auf Anfrage",
|
||||||
durationMinutes: 60,
|
durationMinutes: 60,
|
||||||
shortDescription: "Persönliche Einzelbegleitung — vor Ort oder online.",
|
shortDescription: "Persönliche Einzelbegleitung – vor Ort oder online.",
|
||||||
description: lexicalFromParagraphs(prozessbegleitung.sections[0].paragraphs),
|
description: lexicalFromParagraphs(prozessbegleitung.sections[0].paragraphs),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -138,13 +138,13 @@ async function seedGlobals(payload: Awaited<ReturnType<typeof getPayload>>) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Verbindung",
|
title: "Verbindung",
|
||||||
quote: "Räume für Verbindung — mit uns selbst, miteinander und mit der Natur.",
|
quote: "Räume für Verbindung – mit uns selbst, miteinander und mit der Natur.",
|
||||||
},
|
},
|
||||||
{ title: "Natur", quote: "Wir sind nicht nur in der Natur — wir sind ein Teil von ihr." },
|
{ title: "Natur", quote: "Wir sind nicht nur in der Natur – wir sind ein Teil von ihr." },
|
||||||
],
|
],
|
||||||
ctaTitle: "Kennenlernen & Termine vereinbaren",
|
ctaTitle: "Kennenlernen & Termine vereinbaren",
|
||||||
ctaLead:
|
ctaLead:
|
||||||
"Ich freue mich, von dir zu hören — schreib mir oder buche direkt ein unverbindliches Kennenlerngespräch.",
|
"Ich freue mich, von dir zu hören – schreib mir oder buche direkt ein unverbindliches Kennenlerngespräch.",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
console.log(" ↺ Global „Startseite“ gesetzt");
|
console.log(" ↺ Global „Startseite“ gesetzt");
|
||||||
@@ -166,7 +166,7 @@ async function seedGlobals(payload: Awaited<ReturnType<typeof getPayload>>) {
|
|||||||
slug: "angebote-intro",
|
slug: "angebote-intro",
|
||||||
data: {
|
data: {
|
||||||
title: "Angebote",
|
title: "Angebote",
|
||||||
lead: "Jeder Raum ist anders — und darf so genau das sein, was gerade gebraucht wird. Ein Überblick über meine Begleitung für Erwachsene, Kinder und Gemeinschaft.",
|
lead: "Jeder Raum ist anders – und darf so genau das sein, was gerade gebraucht wird. Ein Überblick über meine Begleitung für Erwachsene, Kinder und Gemeinschaft.",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
console.log(" ↺ Global „Angebote (Einleitung)“ gesetzt");
|
console.log(" ↺ Global „Angebote (Einleitung)“ gesetzt");
|
||||||
@@ -186,11 +186,11 @@ async function seedGlobals(payload: Awaited<ReturnType<typeof getPayload>>) {
|
|||||||
data: {
|
data: {
|
||||||
eyebrow: "Kontakt",
|
eyebrow: "Kontakt",
|
||||||
title: "Ich freue mich, von dir zu hören",
|
title: "Ich freue mich, von dir zu hören",
|
||||||
lead: "Ob Frage, Anliegen oder der Wunsch nach einem Kennenlerngespräch — schreib mir gerne.",
|
lead: "Ob Frage, Anliegen oder der Wunsch nach einem Kennenlerngespräch – schreib mir gerne.",
|
||||||
// Placeholder contact details — replace with the real ones in the admin.
|
// Placeholder contact details — replace with the real ones in the admin.
|
||||||
email: "hallo@anouma.org",
|
email: "hallo@anouma.org",
|
||||||
phone: "+49 000 00 00 00",
|
phone: "+49 000 00 00 00",
|
||||||
region: "Platzhalter — Ort / Region",
|
region: "Platzhalter – Ort / Region",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
console.log(" ↺ Global „Kontakt“ gesetzt (mit Platzhalter-Kontaktdaten — bitte im Admin ersetzen)");
|
console.log(" ↺ Global „Kontakt“ gesetzt (mit Platzhalter-Kontaktdaten — bitte im Admin ersetzen)");
|
||||||
@@ -200,11 +200,11 @@ async function seedGlobals(payload: Awaited<ReturnType<typeof getPayload>>) {
|
|||||||
data: {
|
data: {
|
||||||
eyebrow: "Termin buchen",
|
eyebrow: "Termin buchen",
|
||||||
title: "Kennenlernen & Termine vereinbaren",
|
title: "Kennenlernen & Termine vereinbaren",
|
||||||
lead: "Der erste Schritt ist oft der wichtigste. Melde dich gerne — unverbindlich und in deinem Tempo.",
|
lead: "Der erste Schritt ist oft der wichtigste. Melde dich gerne – unverbindlich und in deinem Tempo.",
|
||||||
steps: [
|
steps: [
|
||||||
{
|
{
|
||||||
title: "Nachricht schreiben",
|
title: "Nachricht schreiben",
|
||||||
text: "Schildere kurz dein Anliegen und welches Angebot dich interessiert — über das Formular unten oder direkt per E-Mail.",
|
text: "Schildere kurz dein Anliegen und welches Angebot dich interessiert – über das Formular unten oder direkt per E-Mail.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Kennenlernen",
|
title: "Kennenlernen",
|
||||||
|
|||||||
Reference in New Issue
Block a user