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 (
-