From 526a908c8044c566ab24c85e6076cd3c4e4a7245 Mon Sep 17 00:00:00 2001 From: maro Date: Sun, 6 Sep 2026 22:00:28 +0200 Subject: [PATCH] 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. --- app/(frontend)/aktuelles/page.tsx | 7 +- app/(frontend)/angebote/page.tsx | 4 +- app/(frontend)/angebote/singkreise/page.tsx | 2 +- .../auth/verify-email/[token]/page.tsx | 1 + app/(frontend)/datenschutz/page.tsx | 8 +- app/(frontend)/impressionen/page.tsx | 6 +- app/(frontend)/impressum/page.tsx | 12 +- app/(frontend)/kontakt/page.tsx | 3 + app/(frontend)/konto/layout.tsx | 4 +- app/(frontend)/konto/page.tsx | 6 +- app/(frontend)/konto/profil/page.tsx | 2 +- app/(frontend)/login/page.tsx | 2 +- app/(frontend)/page.tsx | 139 ++++++++++-------- app/(frontend)/registrieren/page.tsx | 1 + app/(frontend)/termin-buchen/page.tsx | 2 +- app/(frontend)/termine/[slug]/page.tsx | 2 + app/(frontend)/termine/page.tsx | 16 +- components/AngebotCard.tsx | 5 +- components/ArrowIcon.tsx | 15 ++ components/ArrowLink.tsx | 43 ++++++ components/ContactForm.tsx | 31 ++-- components/EventTeaserCard.tsx | 18 +-- components/Footer.tsx | 2 +- components/FormField.tsx | 62 ++++++++ components/Hero.tsx | 2 +- components/Navbar.tsx | 105 +++++++------ components/PageHeader.tsx | 25 +++- components/PlaceholderNote.tsx | 2 +- components/auth/LoginForm.tsx | 24 +-- components/auth/ProfileForm.tsx | 18 +-- components/auth/RegisterForm.tsx | 47 ++---- components/auth/ResendVerificationButton.tsx | 14 +- components/booking/BookingCard.tsx | 4 +- components/booking/BookingWidget.tsx | 32 ++-- components/booking/MonthCalendar.tsx | 10 +- components/meeting/RegisterForm.tsx | 58 +++----- lib/email/bookingTemplates.ts | 2 +- lib/email/layout.ts | 2 +- lib/email/reminderTemplate.ts | 2 +- lib/site.ts | 4 +- scripts/seed.ts | 18 +-- 41 files changed, 425 insertions(+), 337 deletions(-) create mode 100644 components/ArrowIcon.tsx create mode 100644 components/ArrowLink.tsx create mode 100644 components/FormField.tsx diff --git a/app/(frontend)/aktuelles/page.tsx b/app/(frontend)/aktuelles/page.tsx index 638a5fb..f8dcce7 100644 --- a/app/(frontend)/aktuelles/page.tsx +++ b/app/(frontend)/aktuelles/page.tsx @@ -55,9 +55,10 @@ export default async function AktuellesPage() {
{posts.length === 0 ? ( -

- Hier erscheinen bald die ersten Neuigkeiten — schau gerne wieder vorbei. -

+
+

Hier erscheinen bald die ersten Neuigkeiten

+

Schau gerne wieder vorbei.

+
) : (
{posts.map((post, i) => ( diff --git a/app/(frontend)/angebote/page.tsx b/app/(frontend)/angebote/page.tsx index 5b28a14..2e7a73e 100644 --- a/app/(frontend)/angebote/page.tsx +++ b/app/(frontend)/angebote/page.tsx @@ -19,7 +19,7 @@ import { siteConfig } from "@/lib/site"; export const dynamic = "force-dynamic"; 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 { const [intro, seoSettings] = await Promise.all([getAngeboteIntroGlobal(), getSEOSettingsGlobal()]); @@ -170,7 +170,7 @@ export default async function AngebotePage() { ); diff --git a/app/(frontend)/angebote/singkreise/page.tsx b/app/(frontend)/angebote/singkreise/page.tsx index 94c9e5b..f9399ca 100644 --- a/app/(frontend)/angebote/singkreise/page.tsx +++ b/app/(frontend)/angebote/singkreise/page.tsx @@ -53,7 +53,7 @@ export default async function SingkreisePage() { ); diff --git a/app/(frontend)/auth/verify-email/[token]/page.tsx b/app/(frontend)/auth/verify-email/[token]/page.tsx index 363631f..8a7b052 100644 --- a/app/(frontend)/auth/verify-email/[token]/page.tsx +++ b/app/(frontend)/auth/verify-email/[token]/page.tsx @@ -21,6 +21,7 @@ export default async function VerifyEmailPage({ params }: Args) { eyebrow="Konto" title={verified ? "E-Mail-Adresse bestätigt" : "Bestätigung fehlgeschlagen"} crumbs={[{ title: "E-Mail bestätigen" }]} + compact />
diff --git a/app/(frontend)/datenschutz/page.tsx b/app/(frontend)/datenschutz/page.tsx index 79499fc..0395a68 100644 --- a/app/(frontend)/datenschutz/page.tsx +++ b/app/(frontend)/datenschutz/page.tsx @@ -21,7 +21,7 @@ export default async function DatenschutzPage() { const contact = await getContactGlobal(); return ( <> - +
@@ -33,9 +33,9 @@ export default async function DatenschutzPage() {

Verantwortliche Stelle

- [Platzhalter — Vor- und Nachname] + [Platzhalter – Vor- und Nachname]
- [Platzhalter — Anschrift] + [Platzhalter – Anschrift]
E-Mail: {contact.email}

@@ -48,7 +48,7 @@ export default async function DatenschutzPage() {

Beim Besuch dieser Website werden aus technischen Gründen automatisch Informationen 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.]

diff --git a/app/(frontend)/impressionen/page.tsx b/app/(frontend)/impressionen/page.tsx index 78619e5..db12bf8 100644 --- a/app/(frontend)/impressionen/page.tsx +++ b/app/(frontend)/impressionen/page.tsx @@ -10,7 +10,7 @@ import { JsonLd, webPageJsonLd } from "@/lib/seo/jsonld"; 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 { return buildMetadata({ title: "Impressionen", description: DESCRIPTION, path: "/impressionen" }); @@ -36,11 +36,11 @@ export default async function ImpressionenPage() {
-
+
{gallery.map((item, i) => ( diff --git a/app/(frontend)/impressum/page.tsx b/app/(frontend)/impressum/page.tsx index 202f994..23b8459 100644 --- a/app/(frontend)/impressum/page.tsx +++ b/app/(frontend)/impressum/page.tsx @@ -23,7 +23,7 @@ export default async function ImpressumPage() { const contact = await getContactGlobal(); return ( <> - +
@@ -35,11 +35,11 @@ export default async function ImpressumPage() {

Angaben gemäß § 5 TMG

- [Platzhalter — Vor- und Nachname] + [Platzhalter – Vor- und Nachname]
- [Platzhalter — Straße und Hausnummer] + [Platzhalter – Straße und Hausnummer]
- [Platzhalter — PLZ und Ort] + [Platzhalter – PLZ und Ort]

@@ -61,7 +61,7 @@ export default async function ImpressumPage() { Umsatzsteuer-Identifikationsnummer

- [Platzhalter — sofern vorhanden, gemäß § 27 a Umsatzsteuergesetz] + [Platzhalter – sofern vorhanden, gemäß § 27 a Umsatzsteuergesetz]

@@ -70,7 +70,7 @@ export default async function ImpressumPage() { Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV

- [Platzhalter — Vor- und Nachname, Anschrift wie oben] + [Platzhalter – Vor- und Nachname, Anschrift wie oben]

diff --git a/app/(frontend)/kontakt/page.tsx b/app/(frontend)/kontakt/page.tsx index 8455b95..21758eb 100644 --- a/app/(frontend)/kontakt/page.tsx +++ b/app/(frontend)/kontakt/page.tsx @@ -50,6 +50,9 @@ export default async function KontaktPage() { title={contact.title || "Ich freue mich, von dir zu hören"} lead={contact.lead} crumbs={[{ title: "Kontakt" }]} + blobTone="sage" + blobPosition="top-left" + compact />
diff --git a/app/(frontend)/konto/layout.tsx b/app/(frontend)/konto/layout.tsx index 0056fc4..f3838a1 100644 --- a/app/(frontend)/konto/layout.tsx +++ b/app/(frontend)/konto/layout.tsx @@ -44,12 +44,12 @@ export default async function KontoLayout({ children }: { children: ReactNode }) {item.title} ))} - +
{children}
diff --git a/app/(frontend)/konto/page.tsx b/app/(frontend)/konto/page.tsx index ae6ccd6..ffed82e 100644 --- a/app/(frontend)/konto/page.tsx +++ b/app/(frontend)/konto/page.tsx @@ -1,8 +1,8 @@ import type { Metadata } from "next"; -import Link from "next/link"; import { getCurrentCustomer } from "@/lib/auth/customer"; import { getCustomerBookings } from "@/lib/booking/queries"; import { BookingCard } from "@/components/booking/BookingCard"; +import { ArrowLink } from "@/components/ArrowLink"; export const metadata: Metadata = { title: "Mein Konto" }; @@ -61,9 +61,7 @@ export default async function KontoOverviewPage() { {past.length > 0 && (
- - Alle Termine (inkl. vergangene) ansehen → - + Alle Termine (inkl. vergangene) ansehen
)}
diff --git a/app/(frontend)/konto/profil/page.tsx b/app/(frontend)/konto/profil/page.tsx index 7df4662..cbbec28 100644 --- a/app/(frontend)/konto/profil/page.tsx +++ b/app/(frontend)/konto/profil/page.tsx @@ -14,7 +14,7 @@ export default async function KontoProfilPage() {

- 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.

diff --git a/app/(frontend)/login/page.tsx b/app/(frontend)/login/page.tsx index a9c3566..a2fae54 100644 --- a/app/(frontend)/login/page.tsx +++ b/app/(frontend)/login/page.tsx @@ -20,7 +20,7 @@ export default async function LoginPage() { return ( <> - +
diff --git a/app/(frontend)/page.tsx b/app/(frontend)/page.tsx index 9020654..0dedc80 100644 --- a/app/(frontend)/page.tsx +++ b/app/(frontend)/page.tsx @@ -2,9 +2,11 @@ import type { Metadata } from "next"; import Link from "next/link"; import { Hero } from "@/components/Hero"; import { Button } from "@/components/Button"; +import { ArrowLink } from "@/components/ArrowLink"; import { Section, SectionHeading } from "@/components/Section"; import { AngebotCard } from "@/components/AngebotCard"; import { ImagePlaceholder } from "@/components/ImagePlaceholder"; +import { OrganicBlob } from "@/components/OrganicBlob"; import { RichText } from "@/components/RichText"; import { CTA } from "@/components/CTA"; import { Reveal } from "@/components/Reveal"; @@ -29,7 +31,7 @@ export async function generateMetadata(): Promise { fallbackDescription: () => home.heroSupporting || 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, defaultOgImage: seoSettings.defaultOgImage, }); @@ -53,6 +55,8 @@ export default async function Home() { getUpcomingEvents(3), ]); 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(); return ( @@ -73,9 +77,9 @@ export default async function Home() { actions={ <> - + } > @@ -89,24 +93,39 @@ export default async function Home() { -
- {groups.map((group, i) => { - const isSingle = group.offers.length === 1; - const first = group.offers[0]; - return ( - - - - ); - })} +
+ {/* Prozessbegleitung ist das Kernangebot – bekommt deshalb den größeren, ersten Platz statt eines gleichwertigen 4er-Rasters. */} + {featuredGroup && ( + + + + )} +
+ {remainingGroups.map((group, i) => { + const isSingle = group.offers.length === 1; + const first = group.offers[0]; + return ( + + + + ); + })} +
@@ -141,9 +160,7 @@ export default async function Home() {
)}
- + Mehr über mich lesen
@@ -151,24 +168,27 @@ export default async function Home() { )} {home.values && home.values.length > 0 && ( -
- -
- {home.values.map((wert, i) => ( - -

- {wert.title} -

-

- „{wert.quote}“ -

-
- ))} +
+ +
+ +
+ {home.values.map((wert, i) => ( + +

+ {wert.title} +

+

+ „{wert.quote}“ +

+
+ ))} +
)} @@ -177,9 +197,7 @@ export default async function Home() {
- + Alle Termine
{events.map((event, i) => ( @@ -195,23 +213,31 @@ export default async function Home() {
- + Alle Neuigkeiten
{posts.map((post, i) => ( - -

+ +

+ +
+

{new Date(post.publishDate ?? post.createdAt).toLocaleDateString("de-DE", { day: "2-digit", month: "long", year: "numeric", })}

-

{post.title}

-

{post.teaser}

+

{post.title}

+

{post.teaser}

))} @@ -228,13 +254,8 @@ export default async function Home() { ))}
-
- - Alle Impressionen ansehen - +
+ Alle Impressionen ansehen
@@ -242,7 +263,7 @@ export default async function Home() { 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." + "Ich freue mich, von dir zu hören – schreib mir oder buche direkt ein unverbindliches Kennenlerngespräch." } /> diff --git a/app/(frontend)/registrieren/page.tsx b/app/(frontend)/registrieren/page.tsx index 3c09ef2..e92909a 100644 --- a/app/(frontend)/registrieren/page.tsx +++ b/app/(frontend)/registrieren/page.tsx @@ -24,6 +24,7 @@ export default async function RegisterPage() { title="Konto erstellen" lead="Mit einem Konto kannst du Termine anfragen und behältst alle deine Buchungen im Blick." crumbs={[{ title: "Konto erstellen" }]} + compact />
diff --git a/app/(frontend)/termin-buchen/page.tsx b/app/(frontend)/termin-buchen/page.tsx index a8e4f14..750f5ee 100644 --- a/app/(frontend)/termin-buchen/page.tsx +++ b/app/(frontend)/termin-buchen/page.tsx @@ -98,7 +98,7 @@ export default async function TerminBuchenPage() { 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." + ? "Für einen konkreten Termin wähle oben ein Angebot – hier kannst du auch einfach eine allgemeine Nachricht schreiben." : undefined } align="left" diff --git a/app/(frontend)/termine/[slug]/page.tsx b/app/(frontend)/termine/[slug]/page.tsx index 02bcadf..1f61041 100644 --- a/app/(frontend)/termine/[slug]/page.tsx +++ b/app/(frontend)/termine/[slug]/page.tsx @@ -126,6 +126,8 @@ export default async function EventDetailPage({ params }: Args) { eyebrow={categoryLabel} title={event.title} crumbs={[{ title: "Termine", href: "/termine" }, { title: event.title }]} + blobTone="mauve" + blobPosition="top-left" />
diff --git a/app/(frontend)/termine/page.tsx b/app/(frontend)/termine/page.tsx index f94edd8..6f86114 100644 --- a/app/(frontend)/termine/page.tsx +++ b/app/(frontend)/termine/page.tsx @@ -3,6 +3,7 @@ import { PageHeader } from "@/components/PageHeader"; import { Section } from "@/components/Section"; import { Reveal } from "@/components/Reveal"; import { EventTeaserCard } from "@/components/EventTeaserCard"; +import { ArrowLink } from "@/components/ArrowLink"; import { getAllEvents } from "@/lib/payload/content"; import { buildMetadata } from "@/lib/seo/metadata"; import { getSiteUrl } from "@/lib/seo/config"; @@ -32,14 +33,21 @@ export default async function TerminePage() { title="Aktuelle Termine" lead="Ein Überblick über anstehende Kreise, Begleitungen und Veranstaltungen." crumbs={[{ title: "Termine" }]} + blobTone="mauve" + blobPosition="top-left" />
{upcoming.length === 0 ? ( -

- Aktuell sind keine Termine geplant — schau gerne bald wieder vorbei oder melde dich - direkt über die Kontaktseite. -

+
+

Aktuell sind keine Termine geplant

+

+ Schau gerne bald wieder vorbei, oder melde dich direkt – ich freue mich von dir zu hören. +

+
+ Zur Kontaktseite +
+
) : (
{upcoming.map((event, i) => ( diff --git a/components/AngebotCard.tsx b/components/AngebotCard.tsx index 6e0c59b..a8de20d 100644 --- a/components/AngebotCard.tsx +++ b/components/AngebotCard.tsx @@ -1,5 +1,6 @@ import Link from "next/link"; import { ImagePlaceholder } from "@/components/ImagePlaceholder"; +import { ArrowIcon } from "@/components/ArrowIcon"; import type { ImageMood } from "@/lib/angebote"; type AngebotCardProps = { @@ -42,9 +43,7 @@ export function AngebotCard({

{tagline}

Mehr erfahren - +
diff --git a/components/ArrowIcon.tsx b/components/ArrowIcon.tsx new file mode 100644 index 0000000..e9cabbe --- /dev/null +++ b/components/ArrowIcon.tsx @@ -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 ( + + ); +} diff --git a/components/ArrowLink.tsx b/components/ArrowLink.tsx new file mode 100644 index 0000000..020169a --- /dev/null +++ b/components/ArrowLink.tsx @@ -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 = { + mauve: "text-anouma-mauve-dark", + plum: "text-anouma-plum", + white: "text-white", +}; + +const sizes: Record = { + 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 ( + + {children} + + + ); +} diff --git a/components/ContactForm.tsx b/components/ContactForm.tsx index 8eee741..d990bdc 100644 --- a/components/ContactForm.tsx +++ b/components/ContactForm.tsx @@ -1,9 +1,8 @@ "use client"; import { useState, type FormEvent } from "react"; - -const fieldClass = - "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"; +import { track } from "@/lib/analytics/client"; +import { TextField, TextAreaField } from "@/components/FormField"; type ContactFormProps = { toEmail: string; @@ -31,31 +30,19 @@ export function ContactForm({ const message = String(data.get("message") ?? ""); 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}`; + // 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); } return (
-
- - -
-
- - -
-
- -