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:
@@ -0,0 +1,247 @@
|
||||
import type { CollectionBeforeChangeHook, CollectionConfig } from "payload";
|
||||
import { isAdmin, publishedOrAdmin } from "@/access";
|
||||
import { slugField } from "@/fields/slug";
|
||||
import { generateMeetingPassword } from "@/lib/meeting/password";
|
||||
|
||||
export const EVENT_CATEGORIES = [
|
||||
{ label: "Prozessbegleitung", value: "prozessbegleitung" },
|
||||
{ label: "Doula", value: "doula" },
|
||||
{ label: "Erdenkinder", value: "erdenkinder" },
|
||||
{ label: "Mädchenkreis", value: "maedchenkreis" },
|
||||
{ label: "Singen im Kreis", value: "singen-im-kreis" },
|
||||
{ label: "Singen für Schwangere", value: "singen-fuer-schwangere" },
|
||||
{ label: "Singen für Mamas mit Baby", value: "singen-fuer-mamas-mit-baby" },
|
||||
{ label: "Sonstiges", value: "sonstiges" },
|
||||
] as const;
|
||||
|
||||
// Auto-generates the meeting password for new online events, and
|
||||
// regenerates it whenever the admin ticks "Neues Passwort generieren".
|
||||
// Never derived from the event id/slug — always a fresh random value.
|
||||
const setMeetingPassword: CollectionBeforeChangeHook = ({ data }) => {
|
||||
if (!data?.isOnline) return data;
|
||||
if (!data.meetingPassword || data.regeneratePassword) {
|
||||
data.meetingPassword = generateMeetingPassword();
|
||||
}
|
||||
data.regeneratePassword = false;
|
||||
return data;
|
||||
};
|
||||
|
||||
export const Events: CollectionConfig = {
|
||||
slug: "events",
|
||||
labels: {
|
||||
singular: "Termin",
|
||||
plural: "Termine",
|
||||
},
|
||||
admin: {
|
||||
useAsTitle: "title",
|
||||
defaultColumns: ["title", "date", "category", "_status"],
|
||||
group: "Inhalte",
|
||||
description: "Termine und Veranstaltungen — erscheinen automatisch unter /termine.",
|
||||
},
|
||||
versions: {
|
||||
drafts: {
|
||||
autosave: { interval: 1500 },
|
||||
},
|
||||
},
|
||||
access: {
|
||||
read: publishedOrAdmin,
|
||||
create: isAdmin,
|
||||
update: isAdmin,
|
||||
delete: isAdmin,
|
||||
},
|
||||
hooks: {
|
||||
beforeChange: [setMeetingPassword],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "title",
|
||||
type: "text",
|
||||
label: "Titel",
|
||||
required: true,
|
||||
},
|
||||
slugField(),
|
||||
{
|
||||
name: "description",
|
||||
type: "richText",
|
||||
label: "Beschreibung",
|
||||
},
|
||||
{
|
||||
type: "row",
|
||||
fields: [
|
||||
{
|
||||
name: "date",
|
||||
type: "date",
|
||||
label: "Datum",
|
||||
required: true,
|
||||
admin: {
|
||||
date: { pickerAppearance: "dayOnly", displayFormat: "dd.MM.yyyy" },
|
||||
width: "34%",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "startTime",
|
||||
type: "date",
|
||||
label: "Startzeit",
|
||||
admin: {
|
||||
date: { pickerAppearance: "timeOnly", displayFormat: "HH:mm" },
|
||||
width: "33%",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "endTime",
|
||||
type: "date",
|
||||
label: "Endzeit",
|
||||
admin: {
|
||||
date: { pickerAppearance: "timeOnly", displayFormat: "HH:mm" },
|
||||
width: "33%",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "location",
|
||||
type: "text",
|
||||
label: "Ort",
|
||||
},
|
||||
{
|
||||
name: "category",
|
||||
type: "select",
|
||||
label: "Kategorie",
|
||||
required: true,
|
||||
defaultValue: "sonstiges",
|
||||
options: [...EVENT_CATEGORIES],
|
||||
},
|
||||
{
|
||||
name: "image",
|
||||
type: "upload",
|
||||
relationTo: "media",
|
||||
label: "Bild",
|
||||
},
|
||||
{
|
||||
type: "row",
|
||||
fields: [
|
||||
{
|
||||
name: "maxParticipants",
|
||||
type: "number",
|
||||
label: "Maximale Teilnehmerzahl",
|
||||
min: 1,
|
||||
admin: { width: "50%" },
|
||||
},
|
||||
{
|
||||
name: "registrationRequired",
|
||||
type: "checkbox",
|
||||
label: "Anmeldung erforderlich",
|
||||
defaultValue: false,
|
||||
admin: { width: "50%" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "registrationInfo",
|
||||
type: "text",
|
||||
label: "Anmeldelink bzw. Kontaktmöglichkeit",
|
||||
admin: {
|
||||
description: "z. B. eine E-Mail-Adresse, ein Link oder ein Hinweistext.",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "collapsible",
|
||||
label: "Online-Termin (Video-Call)",
|
||||
fields: [
|
||||
{
|
||||
name: "isOnline",
|
||||
type: "checkbox",
|
||||
label: "Online-Termin",
|
||||
defaultValue: false,
|
||||
admin: {
|
||||
description: "Aktiviert Meeting-Passwort, Beitrittsseite und Erinnerungs-E-Mails.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "meetingPassword",
|
||||
type: "text",
|
||||
label: "Meeting-Passwort",
|
||||
admin: {
|
||||
readOnly: true,
|
||||
condition: (data) => Boolean(data?.isOnline),
|
||||
description: "Wird automatisch erzeugt. Ist nicht Teil des Meeting-Links.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "regeneratePassword",
|
||||
type: "checkbox",
|
||||
label: "Neues Passwort generieren (beim Speichern)",
|
||||
defaultValue: false,
|
||||
admin: {
|
||||
condition: (data) => Boolean(data?.isOnline),
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "row",
|
||||
admin: { condition: (data) => Boolean(data?.isOnline) },
|
||||
fields: [
|
||||
{
|
||||
name: "reminder60Enabled",
|
||||
type: "checkbox",
|
||||
label: "Erinnerung 60 Minuten vorher",
|
||||
defaultValue: true,
|
||||
admin: { width: "34%" },
|
||||
},
|
||||
{
|
||||
name: "reminder30Enabled",
|
||||
type: "checkbox",
|
||||
label: "Erinnerung 30 Minuten vorher",
|
||||
defaultValue: true,
|
||||
admin: { width: "33%" },
|
||||
},
|
||||
{
|
||||
name: "hostReminderEnabled",
|
||||
type: "checkbox",
|
||||
label: "Auch Host per E-Mail erinnern",
|
||||
defaultValue: true,
|
||||
admin: { width: "33%" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "row",
|
||||
admin: { condition: (data) => Boolean(data?.isOnline) },
|
||||
fields: [
|
||||
{
|
||||
name: "hostReminder60Sent",
|
||||
type: "checkbox",
|
||||
label: "Host-Reminder 60 Min. gesendet",
|
||||
defaultValue: false,
|
||||
admin: { readOnly: true, width: "50%" },
|
||||
},
|
||||
{
|
||||
name: "hostReminder30Sent",
|
||||
type: "checkbox",
|
||||
label: "Host-Reminder 30 Min. gesendet",
|
||||
defaultValue: false,
|
||||
admin: { readOnly: true, width: "50%" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "isPrivateBooking",
|
||||
type: "checkbox",
|
||||
label: "Aus privater Buchungsanfrage erzeugt",
|
||||
defaultValue: false,
|
||||
admin: {
|
||||
position: "sidebar",
|
||||
readOnly: true,
|
||||
description: "Wird automatisch gesetzt, wenn dieser Termin aus einer bestätigten Einzelbuchung entstanden ist. Solche Termine bleiben unveröffentlicht.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bookingRequest",
|
||||
type: "relationship",
|
||||
relationTo: "booking-requests",
|
||||
label: "Zugehörige Buchungsanfrage",
|
||||
admin: { position: "sidebar", readOnly: true },
|
||||
},
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user