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
+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,
},
};