From 4bde54ae5564e28289ec865a031480475fdd5e31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matth=C3=A4us=20Mayer?= <7984982+theMattCode@users.noreply.github.com> Date: Wed, 15 Oct 2025 12:57:35 +0200 Subject: [PATCH 1/2] Add nodemailer dependency --- package.json | 2 +- pnpm-lock.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1a37b3f3..a0e5f5a5 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "@types/mdx": "^2.0.10", "@types/node": "^22.15.21", "@types/node-fetch": "^2.6.12", - "@types/nodemailer": "^6.4.14", + "@types/nodemailer": "^6.4.17", "@types/react": "19.0.10", "@types/react-dom": "19.0.4", "@types/ws": "^8.18.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1c06439a..af04f3ca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -145,7 +145,7 @@ importers: specifier: ^2.6.12 version: 2.6.12 '@types/nodemailer': - specifier: ^6.4.14 + specifier: ^6.4.17 version: 6.4.17 '@types/react': specifier: 19.0.10 From 47a4b7036a0475758b79cf9e0d69d4fcb4bdea13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matth=C3=A4us=20Mayer?= <7984982+theMattCode@users.noreply.github.com> Date: Wed, 15 Oct 2025 13:56:32 +0200 Subject: [PATCH 2/2] Implement suggestion form and API --- .../verein/ehrenamtler-des-jahres/page.tsx | 22 +++++++++ .../suggest-volunteer-of-the-year/route.ts | 48 +++++++++++++++++++ .../{CallToActionButton.tsx => Button.tsx} | 12 ++++- components/cms/input/TextField.tsx | 11 ++++- components/form/EhrenamtlerForm.tsx | 43 +++++++++++++++++ content/sitemap.ts | 2 + 6 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 app/(web)/verein/ehrenamtler-des-jahres/page.tsx create mode 100644 app/api/suggest-volunteer-of-the-year/route.ts rename components/button/{CallToActionButton.tsx => Button.tsx} (56%) create mode 100644 components/form/EhrenamtlerForm.tsx diff --git a/app/(web)/verein/ehrenamtler-des-jahres/page.tsx b/app/(web)/verein/ehrenamtler-des-jahres/page.tsx new file mode 100644 index 00000000..06507365 --- /dev/null +++ b/app/(web)/verein/ehrenamtler-des-jahres/page.tsx @@ -0,0 +1,22 @@ +import { Metadata } from "next"; +import { getTitle } from "#/lib/page"; +import { ehrenamtlerDesJahres } from "#/content/sitemap"; +import { PageContent } from "#/components/web/page/PageContent"; +import { SectionTitle } from "#/components/web/section/SectionTitle"; +import { EhrenamtlerForm } from "#/components/form/EhrenamtlerForm"; + +export const metadata: Metadata = { + title: getTitle(ehrenamtlerDesJahres.name), +}; + +export default function EhrenamtlerDesJahres() { + return ( + + +
+

Schlage einen Ehrenamtler des Jahres vor und beschreibe kurz warum er eine Auszeichnung verdient hat.

+ +
+
+ ); +} diff --git a/app/api/suggest-volunteer-of-the-year/route.ts b/app/api/suggest-volunteer-of-the-year/route.ts new file mode 100644 index 00000000..b544b6af --- /dev/null +++ b/app/api/suggest-volunteer-of-the-year/route.ts @@ -0,0 +1,48 @@ +import { NextRequest } from "next/server"; +import { createTransport } from "nodemailer"; + +export async function POST(request: NextRequest) { + const formData = await request.formData(); + + const suggesterName = formData.get("suggester-name"); + if (!suggesterName) { + return Response.json({ error: "Name is required", sent: false }, { status: 400 }); + } + + const suggesterEmail = formData.get("suggester-email"); + if (!suggesterEmail) { + return Response.json({ error: "Email is required", sent: false }, { status: 400 }); + } + + const volunteerName = formData.get("volunteer-name"); + if (!volunteerName) { + return Response.json({ error: "Name is required", sent: false }, { status: 400 }); + } + + const volunteerWhy = formData.get("volunteer-why"); + if (!volunteerWhy) { + return Response.json({ error: "Why is required", sent: false }, { status: 400 }); + } + + const transporter = createTransport({ + port: Number.parseInt(process.env.MAIL_CONTACT_PORT ?? "587"), + host: process.env.MAIL_CONTACT_SMTP, + auth: { + user: process.env.MAIL_CONTACT_USER, + pass: process.env.MAIL_CONTACT_PASS, + }, + requireTLS: true, + }); + + const sentMessageInfo = await transporter.sendMail({ + from: process.env.MAIL_CONTACT_FROM, + to: process.env.MAIL_VOLUNTEER_CONTACT_TO, + subject: `[svwalddorf.de] Neuer Vorschlag für Ehrenamtler des Jahres von ${suggesterName}`, + text: `${suggesterName} (${suggesterEmail}): ${volunteerName} - ${volunteerWhy}`, + html: `

${suggesterName} (${suggesterEmail}):

${volunteerName}

${volunteerWhy}

`, + }); + + console.log("info:", sentMessageInfo); + + return Response.json({ sent: true }); +} diff --git a/components/button/CallToActionButton.tsx b/components/button/Button.tsx similarity index 56% rename from components/button/CallToActionButton.tsx rename to components/button/Button.tsx index a6e7792a..c8552e73 100644 --- a/components/button/CallToActionButton.tsx +++ b/components/button/Button.tsx @@ -3,14 +3,22 @@ import { PropsWithChildren, type JSX } from "react"; type Props = { text?: string; iconPosition?: "left" | "right"; + disabled?: boolean; + type?: "button" | "submit" | "reset" | undefined; }; -export default function CallToActionButton({ +export default function Button({ text, iconPosition = "left", + disabled = false, + type = "button", children, }: PropsWithChildren): JSX.Element { return ( - + {error && {error}} + + ); +} diff --git a/content/sitemap.ts b/content/sitemap.ts index 00a2dae5..5bad70f2 100644 --- a/content/sitemap.ts +++ b/content/sitemap.ts @@ -57,6 +57,7 @@ export const formales = { url: "/verein/formales", subMenu: [satzung, ehrenordnung, geschaeftsordnung, beitragsordnung, jugendschutzordnung, datenschutz, impressum], }; +export const ehrenamtlerDesJahres = { name: "Ehrenamtler des Jahres", url: "/verein/ehrenamtler-des-jahres" }; export const verein: MenuItem = { name: "Verein", url: "/verein", @@ -73,6 +74,7 @@ export const verein: MenuItem = { historie, foerderkreis, formales, + ehrenamtlerDesJahres, ], };