Initial commit: ANOUMA website with Payload CMS, WebRTC meetings, and booking system

- Next.js 16 App Router site with the ANOUMA design system
- Payload CMS (PostgreSQL) for offers, events, posts and page content
- WebRTC video-call system with custom signaling server
- SMTP email reminders and booking-request notifications
- Customer accounts, calendar-based availability, and booking workflow
This commit is contained in:
maro
2026-08-25 16:40:51 +02:00
commit 45261a0461
138 changed files with 23263 additions and 0 deletions
+241
View File
@@ -0,0 +1,241 @@
/**
* Seeds the CMS with the original ANOUMA content (the 7 offers from
* texte.txt, plus starting text for every editable page). Safe to re-run —
* offers are matched and updated by slug, globals are simply overwritten.
*
* Usage: npm run seed (requires DATABASE_URI + PAYLOAD_SECRET in .env)
*
* Deliberately does NOT create a Users document — the first admin account is
* created through Payload's own "create first user" screen at /admin, so no
* password is ever hardcoded here.
*/
import { getPayload } from "payload";
import config from "../payload.config";
import {
prozessbegleitung,
doula,
erdenkinder,
maedchenkreis,
singenImKreis,
singenFuerSchwangere,
mamaBabySingkreis,
ueberMich,
} from "../lib/texte";
import { lexicalFromParagraphs, lexicalFromSections } from "./lexical";
async function seedOffers(payload: Awaited<ReturnType<typeof getPayload>>) {
const offers = [
{
slug: "prozessbegleitung",
title: "Prozessbegleitung",
category: "prozessbegleitung" as const,
order: 1,
shortDescription: "Persönliche Begleitung in Lebensphasen, Veränderungen und inneren Prozessen.",
description: lexicalFromSections(prozessbegleitung.intro, prozessbegleitung.sections),
},
{
slug: "doula-begleitung",
title: "Doula-Begleitung",
category: "doula-begleitung" as const,
order: 2,
shortDescription: "Begleitung vor, während und nach der Geburt.",
description: lexicalFromSections(doula.intro, doula.sections),
},
{
slug: "erdenkinder",
title: "Erdenkinder",
category: "kindergruppen" as const,
order: 3,
shortDescription: "Naturverbunden. Kreativ. Frei. Für Kinder.",
description: lexicalFromParagraphs(erdenkinder.paragraphs),
},
{
slug: "maedchenkreis",
title: "Mädchenkreis",
category: "kindergruppen" as const,
order: 4,
shortDescription: "Stärkung. Vertrauen. Gemeinschaft. Für Mädchen.",
description: lexicalFromParagraphs(maedchenkreis.paragraphs),
},
{
slug: "singen-im-kreis",
title: "Singen im Kreis",
category: "singkreise" as const,
order: 5,
shortDescription: "Gemeinsam singen. Verbunden sein.",
description: lexicalFromParagraphs(singenImKreis.paragraphs),
},
{
slug: "singen-fuer-schwangere",
title: "Singen für Schwangere",
category: "singkreise" as const,
order: 6,
shortDescription: "Lieder, die tragen. Für dich und dein Baby.",
description: lexicalFromParagraphs(singenFuerSchwangere.paragraphs),
},
{
slug: "mama-baby-singkreis",
title: "Singen für Mamas mit Baby",
category: "singkreise" as const,
order: 7,
shortDescription: "Zeit für dich und dein Baby. Singen. Austauschen. Auftanken.",
description: lexicalFromParagraphs(mamaBabySingkreis.paragraphs),
},
{
// A private, bookable single-session offer — not listed in the menu
// or on /angebote (see `visibility`), reachable only via its direct
// link or a booking Anna creates herself. Content reuses the
// "Individuelle Begleitung" section of Prozessbegleitung, which is
// exactly this kind of 1:1 session in the original texte.txt.
slug: "einzelbegleitung",
title: "Einzelbegleitung",
category: "prozessbegleitung" as const,
order: 8,
visibility: "private" as const,
bookable: true,
price: "auf Anfrage",
durationMinutes: 60,
shortDescription: "Persönliche Einzelbegleitung — vor Ort oder online.",
description: lexicalFromParagraphs(prozessbegleitung.sections[0].paragraphs),
},
];
for (const entry of offers) {
const data = { visibility: "public" as const, ...entry };
const existing = await payload.find({
collection: "offers",
where: { slug: { equals: data.slug } },
limit: 1,
});
if (existing.docs[0]) {
await payload.update({ collection: "offers", id: existing.docs[0].id, data, draft: false });
console.log(` ↺ Angebot aktualisiert: ${data.title}`);
} else {
await payload.create({ collection: "offers", data, draft: false });
console.log(` + Angebot angelegt: ${data.title}`);
}
}
}
async function seedGlobals(payload: Awaited<ReturnType<typeof getPayload>>) {
await payload.updateGlobal({
slug: "home",
data: {
heroEyebrow: "Prozessbegleitung · Doula · Gemeinschaft",
heroTitle: "Willkommen bei Anouma",
heroHighlight: ueberMich.closing.highlight,
heroSupporting: ueberMich.closing.paragraph,
aboutEyebrow: "Über mich",
aboutTitle: "Mein Weg, meine Werte und was mich bewegt",
aboutText: lexicalFromParagraphs(ueberMich.paragraphs.slice(0, 2)),
valuesEyebrow: "Philosophie",
valuesTitle: "Räume, in denen wir uns erinnern dürfen, wer wir sind",
values: [
{
title: "Vertrauen",
quote: "Alles, was wir für unseren Weg brauchen, ist bereits in uns angelegt.",
},
{
title: "Verbindung",
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." },
],
ctaTitle: "Kennenlernen & Termine vereinbaren",
ctaLead:
"Ich freue mich, von dir zu hören — schreib mir oder buche direkt ein unverbindliches Kennenlerngespräch.",
},
});
console.log(" ↺ Global „Startseite“ gesetzt");
await payload.updateGlobal({
slug: "about",
data: {
eyebrow: "Über mich",
title: "Mein Weg, meine Werte und was mich bewegt",
body: lexicalFromParagraphs(ueberMich.paragraphs),
closingLead: ueberMich.closing.lead,
closingHighlight: ueberMich.closing.highlight,
closingParagraph: ueberMich.closing.paragraph,
},
});
console.log(" ↺ Global „Über mich“ gesetzt");
await payload.updateGlobal({
slug: "angebote-intro",
data: {
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.",
},
});
console.log(" ↺ Global „Angebote (Einleitung)“ gesetzt");
await payload.updateGlobal({
slug: "aktuelles-intro",
data: {
eyebrow: "Aktuelles",
title: "Neuigkeiten, Termine und Inspiration",
lead: "Hier findest du künftig aktuelle Termine, Ankündigungen und Impulse rund um die Angebote von Anouma.",
},
});
console.log(" ↺ Global „Aktuelles (Einleitung)“ gesetzt");
await payload.updateGlobal({
slug: "contact",
data: {
eyebrow: "Kontakt",
title: "Ich freue mich, von dir zu hören",
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.
email: "hallo@anouma.org",
phone: "+49 000 00 00 00",
region: "Platzhalter — Ort / Region",
},
});
console.log(" ↺ Global „Kontakt“ gesetzt (mit Platzhalter-Kontaktdaten — bitte im Admin ersetzen)");
await payload.updateGlobal({
slug: "booking",
data: {
eyebrow: "Termin buchen",
title: "Kennenlernen & Termine vereinbaren",
lead: "Der erste Schritt ist oft der wichtigste. Melde dich gerne — unverbindlich und in deinem Tempo.",
steps: [
{
title: "Nachricht schreiben",
text: "Schildere kurz dein Anliegen und welches Angebot dich interessiert — über das Formular unten oder direkt per E-Mail.",
},
{
title: "Kennenlernen",
text: "In einem ersten, unverbindlichen Gespräch schauen wir gemeinsam, was du gerade brauchst und welcher Weg passt.",
},
{
title: "Termin vereinbaren",
text: "Passt es für euch beide, vereinbaren wir gemeinsam die weiteren Termine.",
},
],
formNote:
"Aktuell öffnet dieses Formular dein E-Mail-Programm. Sobald ein Buchungstool (z. B. für Online-Terminvergabe) ausgewählt ist, kann es hier eingebunden werden.",
},
});
console.log(" ↺ Global „Termin buchen“ gesetzt");
}
async function run() {
const payload = await getPayload({ config });
console.log("Angebote:");
await seedOffers(payload);
console.log("Seiteninhalte:");
await seedGlobals(payload);
console.log("\nFertig. Richte jetzt unter /admin den ersten Admin-Zugang ein (dort wird kein Passwort vorgegeben).");
process.exit(0);
}
run().catch((err) => {
console.error(err);
process.exit(1);
});