Skip to content

Commit cdb53c0

Browse files
authored
Merge pull request #330 from theMattCode/feature/ehrenamtler-des-jahres
Ehrenamtler des Jahres
2 parents fee2a21 + 47a4b70 commit cdb53c0

File tree

8 files changed

+137
-5
lines changed

8 files changed

+137
-5
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Metadata } from "next";
2+
import { getTitle } from "#/lib/page";
3+
import { ehrenamtlerDesJahres } from "#/content/sitemap";
4+
import { PageContent } from "#/components/web/page/PageContent";
5+
import { SectionTitle } from "#/components/web/section/SectionTitle";
6+
import { EhrenamtlerForm } from "#/components/form/EhrenamtlerForm";
7+
8+
export const metadata: Metadata = {
9+
title: getTitle(ehrenamtlerDesJahres.name),
10+
};
11+
12+
export default function EhrenamtlerDesJahres() {
13+
return (
14+
<PageContent>
15+
<SectionTitle title={ehrenamtlerDesJahres.name} />
16+
<div className="flex flex-col gap-4">
17+
<p>Schlage einen Ehrenamtler des Jahres vor und beschreibe kurz warum er eine Auszeichnung verdient hat.</p>
18+
<EhrenamtlerForm />
19+
</div>
20+
</PageContent>
21+
);
22+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { NextRequest } from "next/server";
2+
import { createTransport } from "nodemailer";
3+
4+
export async function POST(request: NextRequest) {
5+
const formData = await request.formData();
6+
7+
const suggesterName = formData.get("suggester-name");
8+
if (!suggesterName) {
9+
return Response.json({ error: "Name is required", sent: false }, { status: 400 });
10+
}
11+
12+
const suggesterEmail = formData.get("suggester-email");
13+
if (!suggesterEmail) {
14+
return Response.json({ error: "Email is required", sent: false }, { status: 400 });
15+
}
16+
17+
const volunteerName = formData.get("volunteer-name");
18+
if (!volunteerName) {
19+
return Response.json({ error: "Name is required", sent: false }, { status: 400 });
20+
}
21+
22+
const volunteerWhy = formData.get("volunteer-why");
23+
if (!volunteerWhy) {
24+
return Response.json({ error: "Why is required", sent: false }, { status: 400 });
25+
}
26+
27+
const transporter = createTransport({
28+
port: Number.parseInt(process.env.MAIL_CONTACT_PORT ?? "587"),
29+
host: process.env.MAIL_CONTACT_SMTP,
30+
auth: {
31+
user: process.env.MAIL_CONTACT_USER,
32+
pass: process.env.MAIL_CONTACT_PASS,
33+
},
34+
requireTLS: true,
35+
});
36+
37+
const sentMessageInfo = await transporter.sendMail({
38+
from: process.env.MAIL_CONTACT_FROM,
39+
to: process.env.MAIL_VOLUNTEER_CONTACT_TO,
40+
subject: `[svwalddorf.de] Neuer Vorschlag für Ehrenamtler des Jahres von ${suggesterName}`,
41+
text: `${suggesterName} (${suggesterEmail}): ${volunteerName} - ${volunteerWhy}`,
42+
html: `<p>${suggesterName} (${suggesterEmail}):</p><p>${volunteerName}</p><p>${volunteerWhy}</p>`,
43+
});
44+
45+
console.log("info:", sentMessageInfo);
46+
47+
return Response.json({ sent: true });
48+
}
Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,22 @@ import { PropsWithChildren, type JSX } from "react";
33
type Props = {
44
text?: string;
55
iconPosition?: "left" | "right";
6+
disabled?: boolean;
7+
type?: "button" | "submit" | "reset" | undefined;
68
};
7-
export default function CallToActionButton({
9+
export default function Button({
810
text,
911
iconPosition = "left",
12+
disabled = false,
13+
type = "button",
1014
children,
1115
}: PropsWithChildren<Props>): JSX.Element {
1216
return (
13-
<button className="hover:bg-svw-blue-darker gray-400 text-white py-1 px-2 inline-flex items-center">
17+
<button
18+
className="hover:bg-svw-blue-darker bg-svw-blue-dark gray-400 text-white py-2 px-2 inline-flex items-center justify-center"
19+
disabled={disabled}
20+
type={type}
21+
>
1422
{iconPosition === "left" ? <div className="mr-2">{children}</div> : null}
1523
{text && <span>{text}</span>}
1624
{iconPosition === "right" ? <div className="ml-2">{children}</div> : null}

components/cms/input/TextField.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ export type TextFieldMutationVariables = { value: string | null };
88
export type MutateResult = { type: "success" } | { type: "error"; message: string };
99
export type MutateFn = (variables: TextFieldMutationVariables) => Promise<MutateResult | undefined>;
1010

11-
export interface TextFieldProps extends Pick<MuiTextFieldProps, "defaultValue" | "fullWidth" | "ref" | "inputRef"> {
11+
export interface TextFieldProps
12+
extends Pick<MuiTextFieldProps, "defaultValue" | "fullWidth" | "ref" | "inputRef" | "type" | "id" | "placeholder"> {
1213
StartIcon?: IconType;
1314
label?: string;
1415
mutate?: MutateFn;
@@ -24,6 +25,9 @@ export function TextField({
2425
fullWidth = true,
2526
ref,
2627
inputRef,
28+
type,
29+
id,
30+
placeholder,
2731
}: TextFieldProps) {
2832
const [value, setValue] = useState(defaultValue ?? "");
2933
const [mutationResult, setMutationResult] = useState<MutateResult | undefined>();
@@ -41,8 +45,11 @@ export function TextField({
4145

4246
return (
4347
<MuiTextField
48+
type={type}
49+
id={id}
4450
ref={ref}
4551
value={value}
52+
placeholder={placeholder}
4653
inputRef={inputRef}
4754
onChange={onChangeHandler}
4855
onKeyDown={(event) => {
@@ -57,6 +64,8 @@ export function TextField({
5764
aria-label={label}
5865
slotProps={{
5966
input: {
67+
id,
68+
type,
6069
startAdornment: StartIcon ? (
6170
<InputAdornment position="start">
6271
<StartIcon />
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"use client";
2+
3+
import { FormEventHandler, useState } from "react";
4+
import { GrCheckmark, GrSend } from "react-icons/gr";
5+
import Button from "#/components/button/Button";
6+
import { TextField } from "@mui/material";
7+
8+
export function EhrenamtlerForm() {
9+
const [sent, setSent] = useState(false);
10+
const [error, setError] = useState<string | undefined>();
11+
12+
const sendEmail: FormEventHandler<HTMLFormElement> = (event) => {
13+
event.preventDefault();
14+
15+
if (sent) return;
16+
17+
const form = event.currentTarget;
18+
fetch("/api/suggest-volunteer-of-the-year", { method: "POST", body: new FormData(form) })
19+
.then((value) => {
20+
if (value.ok) {
21+
setError(undefined);
22+
setSent(true);
23+
form.reset();
24+
} else {
25+
value.json().then((value) => setError(value.error));
26+
}
27+
})
28+
.catch((error) => console.log("error", error));
29+
};
30+
31+
return (
32+
<form className="sm:self-center flex flex-col gap-4 sm:w-80" onSubmit={sendEmail}>
33+
<TextField type="text" name="suggester-name" placeholder="Dein Name" size="small" />
34+
<TextField type="email" name="suggester-email" placeholder="Deine E-Mail" size="small" />
35+
<TextField type="text" name="volunteer-name" placeholder="Dein Vorschlag (voller Name)" size="small" />
36+
<TextField type="text" name="volunteer-why" placeholder="Warum?" size="small" />
37+
<Button type="submit" disabled={sent} text="Abschicken">
38+
{sent ? <GrCheckmark /> : <GrSend />}
39+
</Button>
40+
{error && <span>{error}</span>}
41+
</form>
42+
);
43+
}

content/sitemap.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export const formales = {
5757
url: "/verein/formales",
5858
subMenu: [satzung, ehrenordnung, geschaeftsordnung, beitragsordnung, jugendschutzordnung, datenschutz, impressum],
5959
};
60+
export const ehrenamtlerDesJahres = { name: "Ehrenamtler des Jahres", url: "/verein/ehrenamtler-des-jahres" };
6061
export const verein: MenuItem = {
6162
name: "Verein",
6263
url: "/verein",
@@ -73,6 +74,7 @@ export const verein: MenuItem = {
7374
historie,
7475
foerderkreis,
7576
formales,
77+
ehrenamtlerDesJahres,
7678
],
7779
};
7880

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
"@types/mdx": "^2.0.10",
6767
"@types/node": "^22.15.21",
6868
"@types/node-fetch": "^2.6.12",
69-
"@types/nodemailer": "^6.4.14",
69+
"@types/nodemailer": "^6.4.17",
7070
"@types/react": "19.0.10",
7171
"@types/react-dom": "19.0.4",
7272
"@types/ws": "^8.18.1",

pnpm-lock.yaml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)