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
+152
View File
@@ -0,0 +1,152 @@
import { escapeHtml, paragraph, renderButton, renderEmailShell, renderFactBox } from "./layout";
type BaseArgs = {
name: string;
offerTitle: string;
dateLabel: string;
timeLabel: string;
};
export function bookingRequestReceivedEmail(args: BaseArgs & { appointmentTypeLabel: string }): { subject: string; html: string } {
const html = renderEmailShell({
bodyHtml: [
paragraph(`Hallo ${args.name},`),
paragraph("deine Terminanfrage bei ANOUMA wurde übermittelt.", { italic: true }),
renderFactBox([
{ label: "Angebot", value: args.offerTitle },
{ label: "Datum", value: args.dateLabel },
{ label: "Uhrzeit", value: args.timeLabel },
{ label: "Terminart", value: args.appointmentTypeLabel },
]),
paragraph("Der Termin ist noch nicht verbindlich bestätigt. Du erhältst eine weitere E-Mail, sobald Anna deine Anfrage bestätigt hat.", { small: true }),
].join("\n"),
});
return { subject: "Deine Terminanfrage bei ANOUMA", html };
}
export function newBookingRequestAdminEmail(args: {
customerName: string;
customerEmail: string;
offerTitle: string;
dateLabel: string;
timeLabel: string;
appointmentTypeLabel: string;
adminUrl: string;
}): { subject: string; html: string } {
const html = renderEmailShell({
bodyHtml: [
paragraph("Neue Terminanfrage", { italic: true }),
renderFactBox([
{ label: "Name", value: args.customerName },
{ label: "E-Mail", value: args.customerEmail },
{ label: "Angebot", value: args.offerTitle },
{ label: "Datum", value: args.dateLabel },
{ label: "Uhrzeit", value: args.timeLabel },
{ label: "Terminart", value: args.appointmentTypeLabel },
]),
renderButton("Im Admin-Bereich ansehen", args.adminUrl),
].join("\n"),
});
return { subject: "Neue Terminanfrage", html };
}
export function bookingConfirmedEmail(
args: BaseArgs & {
appointmentTypeLabel: string;
online?: { joinUrl: string; password: string };
onsite?: { address: string; mapsUrl: string };
},
): { subject: string; html: string } {
const rows = [
{ label: "Angebot", value: args.offerTitle },
{ label: "Datum", value: args.dateLabel },
{ label: "Uhrzeit", value: args.timeLabel },
{ label: "Ort", value: args.appointmentTypeLabel },
];
if (args.onsite) rows.push({ label: "Adresse", value: args.onsite.address });
const extras: string[] = [];
if (args.online) {
extras.push(renderButton("Meeting betreten", args.online.joinUrl));
extras.push(
`<p style="margin:0 0 4px;color:#66505f;font-size:14px;"><strong>Meeting-Passwort:</strong></p>` +
`<p style="margin:0 0 20px;color:#66505f;font-size:20px;letter-spacing:0.08em;font-family:'Courier New',monospace;">${escapeHtml(args.online.password)}</p>`,
);
}
if (args.onsite) {
extras.push(renderButton("Route öffnen", args.onsite.mapsUrl));
}
const html = renderEmailShell({
bodyHtml: [
paragraph(`Hallo ${args.name},`),
paragraph("dein Termin bei ANOUMA wurde bestätigt.", { italic: true }),
renderFactBox(rows),
...extras,
].join("\n"),
});
return { subject: "Dein Termin bei ANOUMA wurde bestätigt", html };
}
export function bookingRejectedEmail(args: BaseArgs): { subject: string; html: string } {
const html = renderEmailShell({
bodyHtml: [
paragraph(`Hallo ${args.name},`),
paragraph("leider kann der folgende Termin nicht stattfinden.", { italic: true }),
renderFactBox([
{ label: "Angebot", value: args.offerTitle },
{ label: "Datum", value: args.dateLabel },
{ label: "Uhrzeit", value: args.timeLabel },
]),
paragraph("Melde dich gerne für einen neuen Termin — schau einfach wieder in deinem ANOUMA-Konto vorbei.", { small: true }),
].join("\n"),
});
return { subject: "Deine Terminanfrage bei ANOUMA", html };
}
export function alternativeProposedEmail(args: {
name: string;
offerTitle: string;
originalDateLabel: string;
originalTimeLabel: string;
altDateLabel: string;
altTimeLabel: string;
accountUrl: string;
}): { subject: string; html: string } {
const html = renderEmailShell({
bodyHtml: [
paragraph(`Hallo ${args.name},`),
paragraph("Anna hat einen alternativen Termin für dich vorgeschlagen.", { italic: true }),
renderFactBox([
{ label: "Angebot", value: args.offerTitle },
{ label: "Ursprünglich angefragt", value: `${args.originalDateLabel}, ${args.originalTimeLabel}` },
{ label: "Neuer Vorschlag", value: `${args.altDateLabel}, ${args.altTimeLabel}` },
]),
renderButton("Zum Termin in meinem Konto", args.accountUrl),
paragraph("Dort kannst du den neuen Termin annehmen oder einen anderen anfragen.", { small: true }),
].join("\n"),
});
return { subject: "Alternativer Terminvorschlag von ANOUMA", html };
}
export function bookingCancelledAdminEmail(args: {
customerName: string;
offerTitle: string;
dateLabel: string;
timeLabel: string;
adminUrl: string;
}): { subject: string; html: string } {
const html = renderEmailShell({
bodyHtml: [
paragraph("Ein Termin wurde storniert.", { italic: true }),
renderFactBox([
{ label: "Name", value: args.customerName },
{ label: "Angebot", value: args.offerTitle },
{ label: "Datum", value: args.dateLabel },
{ label: "Uhrzeit", value: args.timeLabel },
]),
renderButton("Im Admin-Bereich ansehen", args.adminUrl),
].join("\n"),
});
return { subject: "Termin storniert", html };
}