Fix mail layouts, /invite form-reset, profile validation

Mail templates (6 templates touched):
- New shared `_styles.ts` exports a robust font stack starting with
  Arial/Helvetica so mail readers that lack -apple-system and
  BlinkMacSystemFont don't fall back to Monospace (observed in at least
  one webmail client).
- `fontFamily` is now set on every text-bearing element, not only on
  `<Body>`, because some clients (Outlook, several webmail readers) do
  not inherit body CSS into nested sections.
- ASCII substitutions are gone: "Ueber" → "Über", "fuer" → "für",
  "bestaetigen" → "bestätigen", "uebernehmen" → "übernehmen",
  "verlaesslich" → "verlässlich".
- InviteEmail + NewsEmail buttons switch from the off-brand yellow
  (#f0b84b) to the brand dark green so the CTA matches the header.

/invite form:
- InviteState gains an `attempt` counter plus echoed checkbox `values`,
  mirroring the /register and /onboarding fix from earlier branches. A
  failed submit no longer wipes the Datenschutz / Adressbuch-Opt-In
  ticks. Passwords stay empty for security.
- Field error <p> elements gain role="alert" + aria-live="polite" so
  screen readers announce validation failures on submit. Inputs use
  aria-describedby to link to error / helper text.

Profile validation:
- Telephone numbers must match a permissive pattern that still rejects
  obvious garbage (previously "abc-123-not-a-phone" was accepted).
- Postal codes must match an alphanumeric pattern of 3–10 chars
  (previously "ABCDE" was accepted).
- Child date-of-birth is now validated via superRefine: it must be a
  parseable date, not in the future, and not before 1900. Previously a
  kid born in 2030 was accepted.
- The three Profil server actions now surface the first Zod issue
  message instead of generic "Ungültige Eingabedaten." toasts.

Misc:
- "fuer" → "für" in the Adressbuch page subtitle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
t.indorf
2026-05-17 23:49:01 +02:00
parent 211bb9e300
commit fa06da324c
11 changed files with 304 additions and 69 deletions
+14 -8
View File
@@ -11,6 +11,8 @@ import {
Text,
} from "@react-email/components";
import { fontStack } from "./_styles";
type AlertEmailProps = {
date: string;
childName: string;
@@ -25,26 +27,26 @@ export function AlertEmail({
return (
<Html lang="de">
<Head />
<Preview>Dringender Notdienst-Alarm fuer {date}</Preview>
<Preview>Dringender Notdienst-Alarm für {date}</Preview>
<Body style={styles.body}>
<Container style={styles.container}>
<Section style={styles.alertBar}>
<Text style={styles.kicker}>Dringend</Text>
<Heading style={styles.heading}>Notdienst heute bestaetigen</Heading>
<Heading style={styles.heading}>Notdienst heute bestätigen</Heading>
<Text style={styles.lead}>
Eine Fachkraft ist ausgefallen. Fuer {childName} ist ein
Eine Fachkraft ist ausgefallen. Für {childName} ist ein
Notdienst-Einsatz am {date} hinterlegt.
</Text>
</Section>
<Section style={styles.card}>
<Text style={styles.text}>
Bitte bestaetige schnell, ob du den Notdienst uebernehmen kannst,
damit die Kita den Tag verlaesslich planen kann.
Bitte bestätige schnell, ob du den Notdienst übernehmen kannst,
damit die Kita den Tag verlässlich planen kann.
</Text>
<Button href={confirmLink} style={styles.button}>
Notdienst bestaetigen
Notdienst bestätigen
</Button>
<Section style={styles.detailBox}>
@@ -75,8 +77,7 @@ const styles = {
body: {
margin: 0,
backgroundColor: "#fff7ed",
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
fontFamily: fontStack,
color: "#30170f",
},
container: {
@@ -108,12 +109,14 @@ const styles = {
fontSize: "30px",
lineHeight: "1.16",
fontWeight: 850,
fontFamily: fontStack,
},
lead: {
margin: 0,
color: "#ffe4e6",
fontSize: "16px",
lineHeight: "1.55",
fontFamily: fontStack,
},
card: {
padding: "28px",
@@ -127,6 +130,7 @@ const styles = {
color: "#44251a",
fontSize: "16px",
lineHeight: "1.65",
fontFamily: fontStack,
},
button: {
display: "inline-block",
@@ -137,6 +141,7 @@ const styles = {
fontSize: "15px",
fontWeight: 800,
textDecoration: "none",
fontFamily: fontStack,
},
detailBox: {
margin: "28px 0 0",
@@ -182,5 +187,6 @@ const styles = {
fontSize: "12px",
lineHeight: "1.5",
textAlign: "center" as const,
fontFamily: fontStack,
},
};
+11 -2
View File
@@ -13,6 +13,7 @@ import {
contactRequestTypeLabels,
type ContactRequestInput,
} from "@/lib/contact-schema";
import { fontStack } from "./_styles";
type ContactConfirmationEmailProps = ContactRequestInput;
@@ -69,14 +70,14 @@ function InfoRow({ label, value }: { label: string; value: string }) {
const body = {
margin: 0,
backgroundColor: "#f8faf8",
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
fontFamily: fontStack,
};
const container = {
margin: "0 auto",
padding: "32px 24px",
maxWidth: "620px",
fontFamily: fontStack,
};
const heading = {
@@ -84,6 +85,7 @@ const heading = {
color: "#0f172a",
fontSize: "28px",
lineHeight: "34px",
fontFamily: fontStack,
};
const intro = {
@@ -91,6 +93,7 @@ const intro = {
color: "#475569",
fontSize: "15px",
lineHeight: "24px",
fontFamily: fontStack,
};
const summaryBox = {
@@ -98,6 +101,7 @@ const summaryBox = {
borderRadius: "10px",
backgroundColor: "#ffffff",
padding: "18px",
fontFamily: fontStack,
};
const label = {
@@ -107,6 +111,7 @@ const label = {
fontWeight: 700,
letterSpacing: "0.04em",
textTransform: "uppercase" as const,
fontFamily: fontStack,
};
const row = {
@@ -114,6 +119,7 @@ const row = {
color: "#0f172a",
fontSize: "15px",
lineHeight: "22px",
fontFamily: fontStack,
};
const messageLabel = {
@@ -123,6 +129,7 @@ const messageLabel = {
fontWeight: 700,
letterSpacing: "0.04em",
textTransform: "uppercase" as const,
fontFamily: fontStack,
};
const messageText = {
@@ -131,6 +138,7 @@ const messageText = {
fontSize: "15px",
lineHeight: "24px",
whiteSpace: "pre-wrap" as const,
fontFamily: fontStack,
};
const footer = {
@@ -138,4 +146,5 @@ const footer = {
color: "#64748b",
fontSize: "13px",
lineHeight: "20px",
fontFamily: fontStack,
};
+11 -2
View File
@@ -13,6 +13,7 @@ import {
contactRequestTypeLabels,
type ContactRequestInput,
} from "@/lib/contact-schema";
import { fontStack } from "./_styles";
type ContactRequestEmailProps = ContactRequestInput;
@@ -72,14 +73,14 @@ function InfoRow({ label, value }: { label: string; value: string }) {
const body = {
margin: 0,
backgroundColor: "#f8faf8",
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
fontFamily: fontStack,
};
const container = {
margin: "0 auto",
padding: "32px 24px",
maxWidth: "620px",
fontFamily: fontStack,
};
const heading = {
@@ -87,6 +88,7 @@ const heading = {
color: "#0f172a",
fontSize: "28px",
lineHeight: "34px",
fontFamily: fontStack,
};
const intro = {
@@ -94,6 +96,7 @@ const intro = {
color: "#475569",
fontSize: "15px",
lineHeight: "24px",
fontFamily: fontStack,
};
const details = {
@@ -101,6 +104,7 @@ const details = {
borderRadius: "10px",
backgroundColor: "#ffffff",
padding: "18px",
fontFamily: fontStack,
};
const row = {
@@ -108,6 +112,7 @@ const row = {
color: "#0f172a",
fontSize: "15px",
lineHeight: "22px",
fontFamily: fontStack,
};
const messageBox = {
@@ -116,6 +121,7 @@ const messageBox = {
borderRadius: "10px",
backgroundColor: "#ffffff",
padding: "18px",
fontFamily: fontStack,
};
const label = {
@@ -125,6 +131,7 @@ const label = {
fontWeight: 700,
letterSpacing: "0.04em",
textTransform: "uppercase" as const,
fontFamily: fontStack,
};
const messageText = {
@@ -133,6 +140,7 @@ const messageText = {
fontSize: "15px",
lineHeight: "24px",
whiteSpace: "pre-wrap" as const,
fontFamily: fontStack,
};
const footer = {
@@ -140,4 +148,5 @@ const footer = {
color: "#64748b",
fontSize: "13px",
lineHeight: "20px",
fontFamily: fontStack,
};
+15 -5
View File
@@ -10,6 +10,8 @@ import {
Text,
} from "@react-email/components";
import { fontStack } from "./_styles";
type DutyReminderEmailProps = {
familyName: string;
dutyName: string;
@@ -25,15 +27,15 @@ export function DutyReminderEmail({
<Html lang="de">
<Head />
<Preview>
Erinnerung: {familyName} ist diese Woche fuer {dutyName} eingeteilt.
Erinnerung: {familyName} ist diese Woche für {dutyName} eingeteilt.
</Preview>
<Body style={styles.body}>
<Container style={styles.container}>
<Section style={styles.header}>
<Text style={styles.kicker}>Elterndienst</Text>
<Heading style={styles.heading}>Danke fuer eure Hilfe</Heading>
<Heading style={styles.heading}>Danke für eure Hilfe</Heading>
<Text style={styles.lead}>
Hallo {familyName}, diese Woche seid ihr fuer den Dienst{" "}
Hallo {familyName}, diese Woche seid ihr für den Dienst{" "}
<strong>{dutyName}</strong> eingeteilt.
</Text>
</Section>
@@ -43,7 +45,7 @@ export function DutyReminderEmail({
Zeitraum: <strong>{weekLabel}</strong>
</Text>
<Text style={styles.text}>
Danke, dass ihr mithelft, den Kita-Alltag verlaesslich zu
Danke, dass ihr mithelft, den Kita-Alltag verlässlich zu
organisieren.
</Text>
</Section>
@@ -62,7 +64,7 @@ const styles = {
body: {
margin: 0,
backgroundColor: "#f7f5ef",
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
fontFamily: fontStack,
color: "#24231f",
},
container: {
@@ -70,11 +72,13 @@ const styles = {
maxWidth: "600px",
margin: "0 auto",
padding: "32px 20px",
fontFamily: fontStack,
},
header: {
padding: "28px",
backgroundColor: "#27423a",
borderRadius: "8px 8px 0 0",
fontFamily: fontStack,
},
kicker: {
margin: "0 0 20px",
@@ -83,6 +87,7 @@ const styles = {
fontWeight: 800,
letterSpacing: "0.08em",
textTransform: "uppercase" as const,
fontFamily: fontStack,
},
heading: {
margin: "0 0 12px",
@@ -90,12 +95,14 @@ const styles = {
fontSize: "30px",
lineHeight: "1.18",
fontWeight: 800,
fontFamily: fontStack,
},
lead: {
margin: 0,
color: "#eef4eb",
fontSize: "16px",
lineHeight: "1.55",
fontFamily: fontStack,
},
card: {
padding: "28px",
@@ -103,12 +110,14 @@ const styles = {
border: "1px solid #dde3d7",
borderTop: "0",
borderRadius: "0 0 8px 8px",
fontFamily: fontStack,
},
text: {
margin: "0 0 16px",
color: "#3d423b",
fontSize: "16px",
lineHeight: "1.65",
fontFamily: fontStack,
},
hr: {
margin: "24px 0",
@@ -120,5 +129,6 @@ const styles = {
fontSize: "12px",
lineHeight: "1.5",
textAlign: "center" as const,
fontFamily: fontStack,
},
};
+30 -23
View File
@@ -11,6 +11,8 @@ import {
Text,
} from "@react-email/components";
import { baseBodyStyle, colors, fontStack } from "./_styles";
type InviteEmailProps = {
parentName: string;
kitaName: string;
@@ -25,7 +27,7 @@ export function InviteEmail({
return (
<Html lang="de">
<Head />
<Preview>Aktiviere deinen Kita-Planer Account fuer {kitaName}</Preview>
<Preview>Aktiviere deinen Kita-Planer Account für {kitaName}</Preview>
<Body style={styles.body}>
<Container style={styles.container}>
<Section style={styles.header}>
@@ -38,7 +40,7 @@ export function InviteEmail({
<Section style={styles.card}>
<Text style={styles.text}>
Ueber den folgenden Link kannst du dein Passwort setzen und deinen
Über den folgenden Link kannst du dein Passwort setzen und deinen
Account aktivieren. Danach hast du Zugriff auf die Kita-Planung,
Termine und deine Familiendaten.
</Text>
@@ -66,90 +68,95 @@ export function InviteEmail({
}
const styles = {
body: {
margin: 0,
backgroundColor: "#f6f7f2",
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
color: "#1f2a24",
},
body: baseBodyStyle,
container: {
width: "100%",
maxWidth: "600px",
margin: "0 auto",
padding: "32px 20px",
fontFamily: fontStack,
},
header: {
padding: "28px 28px 20px",
backgroundColor: "#1f3b2d",
backgroundColor: colors.brandGreen,
borderRadius: "8px 8px 0 0",
fontFamily: fontStack,
},
brand: {
margin: "0 0 28px",
color: "#d8f2bd",
color: colors.eyebrow,
fontSize: "13px",
fontWeight: 700,
letterSpacing: "0.08em",
textTransform: "uppercase" as const,
fontFamily: fontStack,
},
heading: {
margin: "0 0 12px",
color: "#ffffff",
color: colors.textHeader,
fontSize: "30px",
lineHeight: "1.18",
fontWeight: 800,
fontFamily: fontStack,
},
lead: {
margin: 0,
color: "#e7f3e6",
color: colors.lead,
fontSize: "16px",
lineHeight: "1.55",
fontFamily: fontStack,
},
card: {
padding: "28px",
backgroundColor: "#ffffff",
backgroundColor: colors.cardBg,
borderRadius: "0 0 8px 8px",
border: "1px solid #e0e5dc",
border: `1px solid ${colors.border}`,
borderTop: "0",
fontFamily: fontStack,
},
text: {
margin: "0 0 24px",
color: "#344139",
color: colors.textBody,
fontSize: "16px",
lineHeight: "1.65",
fontFamily: fontStack,
},
button: {
display: "inline-block",
padding: "14px 22px",
backgroundColor: "#f0b84b",
color: "#172119",
backgroundColor: colors.brandGreen,
color: "#ffffff",
borderRadius: "6px",
fontSize: "15px",
fontWeight: 800,
fontWeight: 700,
textDecoration: "none",
fontFamily: fontStack,
},
fallbackText: {
margin: "28px 0 8px",
color: "#66736b",
color: colors.textMuted,
fontSize: "13px",
lineHeight: "1.5",
fontFamily: fontStack,
},
linkText: {
margin: 0,
color: "#2f6b4f",
color: colors.link,
fontSize: "13px",
lineHeight: "1.5",
wordBreak: "break-all" as const,
fontFamily: fontStack,
},
hr: {
margin: "24px 0",
borderColor: "#dfe5d9",
borderColor: colors.hr,
},
footer: {
margin: 0,
color: "#7b857d",
color: colors.textFaint,
fontSize: "12px",
lineHeight: "1.5",
textAlign: "center" as const,
fontFamily: fontStack,
},
};
+14 -4
View File
@@ -11,6 +11,8 @@ import {
Text,
} from "@react-email/components";
import { fontStack } from "./_styles";
type NewsEmailProps = {
title: string;
content: string;
@@ -53,7 +55,7 @@ const styles = {
body: {
margin: 0,
backgroundColor: "#f5f3ee",
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
fontFamily: fontStack,
color: "#25231f",
},
container: {
@@ -61,11 +63,13 @@ const styles = {
maxWidth: "600px",
margin: "0 auto",
padding: "32px 20px",
fontFamily: fontStack,
},
header: {
padding: "28px",
backgroundColor: "#243b36",
borderRadius: "8px 8px 0 0",
fontFamily: fontStack,
},
kicker: {
margin: "0 0 20px",
@@ -74,6 +78,7 @@ const styles = {
fontWeight: 800,
letterSpacing: "0.08em",
textTransform: "uppercase" as const,
fontFamily: fontStack,
},
heading: {
margin: 0,
@@ -81,6 +86,7 @@ const styles = {
fontSize: "28px",
lineHeight: "1.2",
fontWeight: 800,
fontFamily: fontStack,
},
card: {
padding: "28px",
@@ -88,6 +94,7 @@ const styles = {
border: "1px solid #deded5",
borderTop: "0",
borderRadius: "0 0 8px 8px",
fontFamily: fontStack,
},
text: {
margin: "0 0 24px",
@@ -95,16 +102,18 @@ const styles = {
fontSize: "15px",
lineHeight: "1.65",
whiteSpace: "pre-wrap" as const,
fontFamily: fontStack,
},
button: {
display: "inline-block",
padding: "13px 20px",
backgroundColor: "#f0b84b",
color: "#172119",
backgroundColor: "#243b36",
color: "#ffffff",
borderRadius: "6px",
fontSize: "15px",
fontWeight: 800,
fontWeight: 700,
textDecoration: "none",
fontFamily: fontStack,
},
hr: {
margin: "24px 0",
@@ -116,5 +125,6 @@ const styles = {
fontSize: "12px",
lineHeight: "1.5",
textAlign: "center" as const,
fontFamily: fontStack,
},
};
+39
View File
@@ -0,0 +1,39 @@
// Gemeinsame Style-Tokens für alle E-Mail-Templates.
//
// Schriftart: Mail-Clients fallen unterschiedlich zurück, wenn ein Font nicht
// verfügbar ist. Manche (z.B. einige Web-Reader) wählen dann Monospace —
// deshalb erst Arial/Helvetica voranstellen, die praktisch überall existieren.
// Außerdem wird `fontFamily` auf einzelne Elemente gesetzt statt nur auf
// `<Body>`, weil Outlook & Co. die Body-CSS oft nicht erben.
export const fontStack =
'Arial, Helvetica, "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, sans-serif';
export const colors = {
brandGreen: "#1f3b2d",
brandGreenAccent: "#2f6b4f",
bgSoft: "#f6f7f2",
bgAlt: "#f8faf8",
cardBg: "#ffffff",
border: "#e0e5dc",
hr: "#dfe5d9",
textHeader: "#ffffff",
textPrimary: "#1f2a24",
textBody: "#344139",
textMuted: "#66736b",
textFaint: "#7b857d",
link: "#2f6b4f",
eyebrow: "#d8f2bd",
lead: "#e7f3e6",
};
export const baseBodyStyle = {
margin: 0,
backgroundColor: colors.bgSoft,
fontFamily: fontStack,
color: colors.textPrimary,
} as const;
export const baseTextStyle = {
fontFamily: fontStack,
color: colors.textBody,
} as const;