feat: add CMS inquiries and group bookings
This commit is contained in:
@@ -6,6 +6,7 @@ import { ImagePlaceholder } from "@/components/ImagePlaceholder";
|
||||
import { RichText } from "@/components/RichText";
|
||||
import { CTA } from "@/components/CTA";
|
||||
import { BookingWidget } from "@/components/booking/BookingWidget";
|
||||
import { GroupRegistrationForm } from "@/components/GroupRegistrationForm";
|
||||
import { getOfferBySlug } from "@/lib/payload/content";
|
||||
import { moodForOffer } from "@/lib/angebote";
|
||||
import { mediaAlt, mediaUrl } from "@/lib/payload/media";
|
||||
@@ -23,6 +24,7 @@ import { siteConfig } from "@/lib/site";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
type Args = { params: Promise<{ slug: string }> };
|
||||
const WEEKDAY_LABELS: Record<string, string> = { monday: "Montag", tuesday: "Dienstag", wednesday: "Mittwoch", thursday: "Donnerstag", friday: "Freitag", saturday: "Samstag", sunday: "Sonntag" };
|
||||
|
||||
async function resolveOfferOrRedirect(slug: string) {
|
||||
const offer = await getOfferBySlug(slug);
|
||||
@@ -123,7 +125,23 @@ export default async function OfferDetailPage({ params }: Args) {
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{offer.bookable ? (
|
||||
{offer.bookingMode === "group" ? (
|
||||
<Section tone="cream">
|
||||
<div className="mx-auto max-w-2xl">
|
||||
<h2 className="mb-3 text-center font-serif text-3xl font-medium text-anouma-plum">Für die Gruppe anmelden</h2>
|
||||
{offer.groupInfo && <p className="mx-auto mb-6 max-w-xl text-center text-base leading-relaxed text-anouma-plum">{offer.groupInfo}</p>}
|
||||
{offer.groupSchedule && (
|
||||
<div className="mb-8 rounded-2xl bg-white p-5 text-sm text-anouma-plum">
|
||||
<p className="font-medium">Fester Termin</p>
|
||||
<p className="mt-1">{offer.groupSchedule.recurrence === "monthly" ? "Monatlich" : "Wöchentlich"}{offer.groupSchedule.weekday ? ` · ${WEEKDAY_LABELS[offer.groupSchedule.weekday] ?? offer.groupSchedule.weekday}` : ""}{offer.groupSchedule.startTime ? ` · ${offer.groupSchedule.startTime}` : ""}{offer.groupSchedule.endTime ? `–${offer.groupSchedule.endTime}` : ""}</p>
|
||||
</div>
|
||||
)}
|
||||
{offer.groupCapacity && <p className="mb-2 text-sm text-anouma-plum/75">Plätze: {offer.groupCapacity}</p>}
|
||||
{offer.registrationDeadline && <p className="mb-6 text-sm text-anouma-plum/75">Anmeldung möglich bis {new Date(offer.registrationDeadline).toLocaleDateString("de-DE")}</p>}
|
||||
<GroupRegistrationForm offerSlug={offer.slug!} />
|
||||
</div>
|
||||
</Section>
|
||||
) : offer.bookable ? (
|
||||
<Section tone="cream">
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<h2 className="mb-6 text-center font-serif text-3xl font-medium text-anouma-plum">Termin auswählen</h2>
|
||||
|
||||
@@ -32,9 +32,11 @@ export async function generateMetadata(): Promise<Metadata> {
|
||||
});
|
||||
}
|
||||
|
||||
export default async function KontaktPage() {
|
||||
export default async function KontaktPage({ searchParams }: { searchParams: Promise<{ subject?: string }> }) {
|
||||
const contact = await getContactGlobal();
|
||||
const siteUrl = await getSiteUrl();
|
||||
const params = await searchParams;
|
||||
const isAppointment = params.subject === "Terminanfrage";
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -58,7 +60,7 @@ export default async function KontaktPage() {
|
||||
<Section tone="plain">
|
||||
<div className="grid gap-14 lg:grid-cols-[1fr_0.8fr]">
|
||||
<Reveal>
|
||||
<ContactForm toEmail={contact.email} />
|
||||
<ContactForm subjectPrefix={isAppointment ? "Terminanfrage" : "Allgemeine Anfrage"} type={isAppointment ? "appointment" : "contact"} submitLabel={isAppointment ? "Terminanfrage senden" : "Nachricht senden"} />
|
||||
</Reveal>
|
||||
|
||||
<Reveal delay={0.1} className="space-y-8">
|
||||
@@ -70,12 +72,9 @@ export default async function KontaktPage() {
|
||||
<span className="block text-xs font-medium uppercase tracking-[0.18em] text-anouma-plum/70">
|
||||
E-Mail
|
||||
</span>
|
||||
<a
|
||||
href={`mailto:${contact.email}`}
|
||||
className="text-lg text-anouma-plum hover:text-anouma-mauve-dark"
|
||||
>
|
||||
<span className="text-lg text-anouma-plum">
|
||||
{contact.email}
|
||||
</a>
|
||||
</span>
|
||||
</p>
|
||||
{contact.phone && (
|
||||
<p>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Section, SectionHeading } from "@/components/Section";
|
||||
import { ContactForm } from "@/components/ContactForm";
|
||||
import { AngebotCard } from "@/components/AngebotCard";
|
||||
import { Reveal } from "@/components/Reveal";
|
||||
import { getBookingGlobal, getContactGlobal, getSEOSettingsGlobal } from "@/lib/payload/globals";
|
||||
import { getBookingGlobal, getSEOSettingsGlobal } from "@/lib/payload/globals";
|
||||
import { getOffers } from "@/lib/payload/content";
|
||||
import { moodForOffer } from "@/lib/angebote";
|
||||
import { mediaUrl } from "@/lib/payload/media";
|
||||
@@ -36,7 +36,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
||||
}
|
||||
|
||||
export default async function TerminBuchenPage() {
|
||||
const [booking, contact, offers] = await Promise.all([getBookingGlobal(), getContactGlobal(), getOffers()]);
|
||||
const [booking, offers] = await Promise.all([getBookingGlobal(), getOffers()]);
|
||||
const bookableOffers = offers.filter((offer) => offer.bookable);
|
||||
const siteUrl = await getSiteUrl();
|
||||
|
||||
@@ -56,8 +56,25 @@ export default async function TerminBuchenPage() {
|
||||
crumbs={[{ title: "Termin buchen" }]}
|
||||
/>
|
||||
|
||||
<Section tone="cream">
|
||||
<div className="mx-auto grid max-w-4xl gap-5 md:grid-cols-2">
|
||||
<a href="#freie-termine" className="rounded-3xl bg-white p-7 transition-transform hover:-translate-y-1">
|
||||
<p className="text-xs font-medium uppercase tracking-[0.18em] text-anouma-dustyrose">Direkt buchen</p>
|
||||
<h2 className="mt-3 font-serif text-2xl font-medium text-anouma-plum">Freien Termin auswählen</h2>
|
||||
<p className="mt-3 text-sm leading-relaxed text-anouma-plum/80">Wähle ein Angebot, einen freien Tag und eine passende Uhrzeit im Kalender.</p>
|
||||
<span className="mt-5 inline-flex rounded-full bg-anouma-mauve-dark px-5 py-2.5 text-sm font-medium text-white">Zum Kalender</span>
|
||||
</a>
|
||||
<a href="/kontakt?subject=Terminanfrage" className="rounded-3xl bg-anouma-plum p-7 transition-transform hover:-translate-y-1">
|
||||
<p className="text-xs font-medium uppercase tracking-[0.18em] text-anouma-cream-light/75">Persönlich abstimmen</p>
|
||||
<h2 className="mt-3 font-serif text-2xl font-medium text-white">Anfrage stellen</h2>
|
||||
<p className="mt-3 text-sm leading-relaxed text-white/80">Wenn du keinen freien Kalendertermin brauchst oder etwas Besonderes besprechen möchtest.</p>
|
||||
<span className="mt-5 inline-flex rounded-full bg-white px-5 py-2.5 text-sm font-medium text-anouma-plum">Zur Kontaktseite</span>
|
||||
</a>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{bookableOffers.length > 0 && (
|
||||
<Section tone="plain">
|
||||
<Section tone="plain" id="freie-termine">
|
||||
<SectionHeading eyebrow="Angebot auswählen" title="Wofür möchtest du einen Termin?" />
|
||||
<div className="mt-14 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{bookableOffers.map((offer, i) => (
|
||||
@@ -105,14 +122,11 @@ export default async function TerminBuchenPage() {
|
||||
/>
|
||||
<div className="mt-10">
|
||||
<ContactForm
|
||||
toEmail={contact.email}
|
||||
subjectPrefix="Terminanfrage über anouma.org"
|
||||
type="appointment"
|
||||
submitLabel="Terminanfrage senden"
|
||||
/>
|
||||
</div>
|
||||
{booking.formNote && (
|
||||
<p className="mt-8 text-sm leading-relaxed text-anouma-plum/80">{booking.formNote}</p>
|
||||
)}
|
||||
</div>
|
||||
</Section>
|
||||
</>
|
||||
|
||||
@@ -22,7 +22,11 @@ import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997e
|
||||
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { CalendarNavLink as CalendarNavLink_4d06fac03127b339306d5f5ecb073251 } from '../../../components/admin/CalendarNavLink'
|
||||
import { AnalyticsNavLink as AnalyticsNavLink_29fa5c324cecba6cafb11381cbfac962 } from '../../../components/admin/AnalyticsNavLink'
|
||||
import { ConversationsNavLink as ConversationsNavLink_67a2594cf28806bc76971e67212a6181 } from '../../../components/admin/ConversationsNavLink'
|
||||
import { AdminCalendarView as AdminCalendarView_a2760c3ae85f4fd51d79a465baaf023a } from '../../../components/admin/AdminCalendarView'
|
||||
import { AdminAnalyticsView as AdminAnalyticsView_55d38702e4d86d52435b5ac4df3f55d0 } from '../../../components/admin/AdminAnalyticsView'
|
||||
import { AdminConversationsView as AdminConversationsView_180f5c400a066a0a8f248da44d961055 } from '../../../components/admin/AdminConversationsView'
|
||||
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
|
||||
|
||||
/** @type import('payload').ImportMap */
|
||||
@@ -51,6 +55,10 @@ export const importMap = {
|
||||
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"/components/admin/CalendarNavLink#CalendarNavLink": CalendarNavLink_4d06fac03127b339306d5f5ecb073251,
|
||||
"/components/admin/AnalyticsNavLink#AnalyticsNavLink": AnalyticsNavLink_29fa5c324cecba6cafb11381cbfac962,
|
||||
"/components/admin/ConversationsNavLink#ConversationsNavLink": ConversationsNavLink_67a2594cf28806bc76971e67212a6181,
|
||||
"/components/admin/AdminCalendarView#AdminCalendarView": AdminCalendarView_a2760c3ae85f4fd51d79a465baaf023a,
|
||||
"/components/admin/AdminAnalyticsView#AdminAnalyticsView": AdminAnalyticsView_55d38702e4d86d52435b5ac4df3f55d0,
|
||||
"/components/admin/AdminConversationsView#AdminConversationsView": AdminConversationsView_180f5c400a066a0a8f248da44d961055,
|
||||
"@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1
|
||||
}
|
||||
|
||||
@@ -101,7 +101,10 @@ export async function POST(request: Request) {
|
||||
startTime: start.toISOString(),
|
||||
endTime: end.toISOString(),
|
||||
appointmentType,
|
||||
status: "pending",
|
||||
// Calendar bookings use already published availability, so a slot
|
||||
// selected here is booked immediately. The separate contact form
|
||||
// remains the path for requests that need personal coordination.
|
||||
status: "confirmed",
|
||||
userMessage: message || undefined,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { getPayload } from "payload";
|
||||
import config from "@payload-config";
|
||||
import { getClientIp, checkRateLimit } from "@/lib/auth/rateLimit";
|
||||
import { newConversationAdminEmail } from "@/lib/email/conversationTemplates";
|
||||
import { sendEmail } from "@/lib/email/sendBookingEmails";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
type Body = {
|
||||
name?: unknown;
|
||||
email?: unknown;
|
||||
message?: unknown;
|
||||
subject?: unknown;
|
||||
type?: unknown;
|
||||
};
|
||||
|
||||
function clean(value: unknown, max: number): string {
|
||||
return typeof value === "string" ? value.trim().slice(0, max) : "";
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
let body: Body;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Ungültige Anfrage." }, { status: 400 });
|
||||
}
|
||||
|
||||
const name = clean(body.name, 160);
|
||||
const email = clean(body.email, 254).toLowerCase();
|
||||
const message = clean(body.message, 5000);
|
||||
const subject = clean(body.subject, 180) || "Allgemeine Anfrage";
|
||||
const type = body.type === "appointment" ? "appointment" : "contact";
|
||||
|
||||
if (!name || !email || !message || !/^\S+@\S+\.\S+$/.test(email)) {
|
||||
return NextResponse.json({ error: "Bitte fülle alle Pflichtfelder korrekt aus." }, { status: 400 });
|
||||
}
|
||||
if (!checkRateLimit(`contact:ip:${getClientIp(request)}`, { max: 8, windowMs: 60 * 60 * 1000 })) {
|
||||
return NextResponse.json({ error: "Zu viele Anfragen. Bitte versuche es später erneut." }, { status: 429 });
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await getPayload({ config });
|
||||
const { user } = await payload.auth({ headers: request.headers });
|
||||
const customerId = user?.collection === "customers" ? user.id : undefined;
|
||||
const conversation = await payload.create({
|
||||
collection: "conversations",
|
||||
overrideAccess: true,
|
||||
data: {
|
||||
name,
|
||||
email,
|
||||
subject,
|
||||
type,
|
||||
status: "open",
|
||||
threadKey: randomUUID(),
|
||||
...(customerId ? { customer: customerId } : {}),
|
||||
lastMessageAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
await payload.create({
|
||||
collection: "conversation-messages",
|
||||
overrideAccess: true,
|
||||
data: {
|
||||
conversation: conversation.id,
|
||||
senderType: "visitor",
|
||||
senderName: name,
|
||||
senderEmail: email,
|
||||
body: message,
|
||||
},
|
||||
});
|
||||
|
||||
const contact = await payload.findGlobal({ slug: "contact", overrideAccess: true });
|
||||
const serverUrl = process.env.NEXT_PUBLIC_SERVER_URL || new URL(request.url).origin;
|
||||
const adminUrl = `${serverUrl}/admin/collections/conversations/${conversation.id}`;
|
||||
if (contact.email) {
|
||||
await sendEmail(contact.email, newConversationAdminEmail({ name, email, subject, body: message, adminUrl, threadKey: conversation.threadKey }), {
|
||||
replyTo: process.env.INBOUND_REPLY_TO || "reply-to@anouma.org",
|
||||
});
|
||||
}
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (error) {
|
||||
console.error("contact inquiry failed", error);
|
||||
return NextResponse.json({ error: "Die Nachricht konnte nicht gesendet werden. Bitte versuche es später erneut." }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getPayload } from "payload";
|
||||
import config from "@payload-config";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
type InboundBody = {
|
||||
from?: unknown;
|
||||
sender?: unknown;
|
||||
email?: unknown;
|
||||
name?: unknown;
|
||||
subject?: unknown;
|
||||
text?: unknown;
|
||||
body?: unknown;
|
||||
html?: unknown;
|
||||
messageId?: unknown;
|
||||
inReplyTo?: unknown;
|
||||
};
|
||||
|
||||
function stringValue(value: unknown): string {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
function extractEmail(value: unknown): string {
|
||||
if (value && typeof value === "object" && "email" in value) return extractEmail((value as { email?: unknown }).email);
|
||||
const raw = stringValue(value);
|
||||
const match = raw.match(/<([^>]+)>/) || raw.match(/([\w.+-]+@[\w.-]+\.[A-Za-z]{2,})/);
|
||||
return (match?.[1] || raw).trim().toLowerCase();
|
||||
}
|
||||
|
||||
function stripHtml(value: string): string {
|
||||
return value.replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function findThreadKey(subject: string): string | null {
|
||||
return subject.match(/\[ANOUMA-([^\]]+)\]/i)?.[1] || null;
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const secret = process.env.INBOUND_EMAIL_SECRET;
|
||||
const supplied = request.headers.get("x-inbound-secret") || request.headers.get("authorization")?.replace(/^Bearer\s+/i, "");
|
||||
if (!secret || supplied !== secret) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
|
||||
let body: InboundBody;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "JSON erwartet." }, { status: 400 });
|
||||
}
|
||||
|
||||
const fromRaw = body.from ?? body.sender ?? body.email;
|
||||
const email = extractEmail(fromRaw);
|
||||
const fromName = fromRaw && typeof fromRaw === "object" && "name" in fromRaw ? stringValue((fromRaw as { name?: unknown }).name) : "";
|
||||
const name = stringValue(body.name) || fromName || stringValue(fromRaw).replace(/<[^>]+>/, "").trim() || email;
|
||||
const subject = stringValue(body.subject) || "Antwort auf deine Anfrage";
|
||||
const messageId = stringValue(body.messageId);
|
||||
const text = stringValue(body.text) || stringValue(body.body) || stripHtml(stringValue(body.html));
|
||||
if (!email || !messageId || !text) return NextResponse.json({ error: "Absender, messageId und Inhalt sind erforderlich." }, { status: 400 });
|
||||
|
||||
try {
|
||||
const payload = await getPayload({ config });
|
||||
if (messageId) {
|
||||
const existing = await payload.find({ collection: "conversation-messages", where: { externalMessageId: { equals: messageId } }, limit: 1, overrideAccess: true });
|
||||
if (existing.docs[0]) return NextResponse.json({ ok: true, duplicate: true });
|
||||
}
|
||||
|
||||
const threadKey = findThreadKey(subject);
|
||||
let conversation = threadKey
|
||||
? (await payload.find({ collection: "conversations", where: { threadKey: { equals: threadKey } }, limit: 1, overrideAccess: true })).docs[0]
|
||||
: undefined;
|
||||
if (!conversation) {
|
||||
conversation = (await payload.find({ collection: "conversations", where: { email: { equals: email }, status: { not_equals: "closed" } }, sort: "-lastMessageAt", limit: 1, overrideAccess: true })).docs[0];
|
||||
}
|
||||
if (!conversation) return NextResponse.json({ error: "Keine passende Konversation gefunden." }, { status: 404 });
|
||||
|
||||
await payload.create({
|
||||
collection: "conversation-messages",
|
||||
overrideAccess: true,
|
||||
data: {
|
||||
conversation: conversation.id,
|
||||
senderType: "visitor",
|
||||
senderName: name,
|
||||
senderEmail: email,
|
||||
body: text.slice(0, 10000),
|
||||
externalMessageId: messageId,
|
||||
},
|
||||
});
|
||||
await payload.update({ collection: "conversations", id: conversation.id, overrideAccess: true, data: { status: "open", lastMessageAt: new Date().toISOString() } });
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (error) {
|
||||
console.error("inbound email failed", error);
|
||||
return NextResponse.json({ error: "E-Mail konnte nicht übernommen werden." }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getPayload } from "payload";
|
||||
import config from "@payload-config";
|
||||
import { checkRateLimit, getClientIp } from "@/lib/auth/rateLimit";
|
||||
import { groupRegistrationEmail } from "@/lib/email/conversationTemplates";
|
||||
import { sendEmail } from "@/lib/email/sendBookingEmails";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const value = (v: unknown, max: number) => typeof v === "string" ? v.trim().slice(0, max) : "";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (!checkRateLimit(`group-registration:${getClientIp(request)}`, { max: 5, windowMs: 60 * 60 * 1000 })) return NextResponse.json({ error: "Zu viele Anmeldungen. Bitte versuche es später erneut." }, { status: 429 });
|
||||
let body: Record<string, unknown>;
|
||||
try { body = await request.json(); } catch { return NextResponse.json({ error: "Ungültige Anfrage." }, { status: 400 }); }
|
||||
const offerSlug = value(body.offerSlug, 160);
|
||||
const name = value(body.name, 160);
|
||||
const email = value(body.email, 254).toLowerCase();
|
||||
const phone = value(body.phone, 80);
|
||||
const message = value(body.message, 3000);
|
||||
if (!offerSlug || !name || !email || !/^\S+@\S+\.\S+$/.test(email)) return NextResponse.json({ error: "Bitte fülle Name und E-Mail korrekt aus." }, { status: 400 });
|
||||
|
||||
try {
|
||||
const payload = await getPayload({ config });
|
||||
const offerResult = await payload.find({ collection: "offers", where: { slug: { equals: offerSlug } }, limit: 1, overrideAccess: false });
|
||||
const offer = offerResult.docs[0];
|
||||
if (!offer || offer.bookingMode !== "group") return NextResponse.json({ error: "Dieses Gruppenangebot ist nicht verfügbar." }, { status: 404 });
|
||||
if (offer.registrationDeadline && new Date(offer.registrationDeadline) < new Date()) return NextResponse.json({ error: "Die Anmeldefrist ist bereits abgelaufen." }, { status: 409 });
|
||||
if (offer.groupCapacity) {
|
||||
const count = await payload.count({ collection: "group-registrations", where: { and: [{ offer: { equals: offer.id } }, { status: { in: ["pending", "accepted"] } }] }, overrideAccess: true });
|
||||
if (count.totalDocs >= offer.groupCapacity && offer.groupRegistrationMode === "capacity") return NextResponse.json({ error: "Für dieses Angebot sind aktuell keine Plätze mehr frei." }, { status: 409 });
|
||||
}
|
||||
const registration = await payload.create({ collection: "group-registrations", overrideAccess: true, data: { offer: offer.id, name, email, phone, message, status: "pending" } });
|
||||
const contact = await payload.findGlobal({ slug: "contact", overrideAccess: true });
|
||||
const serverUrl = process.env.NEXT_PUBLIC_SERVER_URL || new URL(request.url).origin;
|
||||
await Promise.all([
|
||||
contact.email ? sendEmail(contact.email, groupRegistrationEmail({ name, offerTitle: offer.title, status: "admin", adminUrl: `${serverUrl}/admin/collections/group-registrations/${registration.id}` }), { replyTo: email }) : Promise.resolve(),
|
||||
sendEmail(email, groupRegistrationEmail({ name, offerTitle: offer.title, status: "received" })),
|
||||
]);
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (error) {
|
||||
console.error("group registration failed", error);
|
||||
return NextResponse.json({ error: "Die Anmeldung konnte nicht gesendet werden." }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user