Trainings + Keynotes: Terminanfrage statt Kontakt-Band, Doppeltes raus

Beide Leistungsseiten enden jetzt mit der Terminanfrage: dasselbe Formular wie
auf der Startseite (an /api/contact), mit zwei freiwilligen Feldern –
Trainings: Wunschtermin/Zeitraum und Teamgröße, Keynotes: Datum und
Veranstaltung & Ort. Das Anliegen („Terminanfrage Training“ / „… Keynote“)
geht als `topic` mit und wird beim Hoster zum Mail-Betreff, die Felder als
`details` in die Mail. Der Knopf im Hero („Training anfragen“, „Keynote
anfragen“) springt dorthin statt ein Mailprogramm zu öffnen.

Entfallen, weil es schon auf der Seite stand:
- Trainings: „Typische Ziele“ (Spiegel der drei Bremsen), der Rahmen-Abschnitt
  (= Hero-Fakten), die Randspalte im Ablauf (wiederholte die Schritte), die FAQ
  zur Teilnehmerzahl; Subline ohne die Format-/Sprach-Wiederholung.
- Keynotes: die Unterbrechung „Mut zum Erfolg.“ (= H1 und Vortragstitel), die
  Speaker-Facts (= Hero-Fakten; „Technik“ steht jetzt dort), die FAQ „Auf
  Englisch?“; die Termine hängen direkt unter dem Vortrag.

Datenschutzerklärung Ziffer 2 und 5 (DE/EN): mehrere Formulare, die neuen
freiwilligen Felder. Ungenutzt und entfernt: TrainingsGoals, TrainingsRahmen,
TrainingsBand, FactsStrip, Interruption, `mailto` in site.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5.5 committed 2026-10-02 13:07:00 +00:00
1 parent a37e48e4ef
commit f825e74222
20 files changed
+268 -400

No files matched your search

+49 -1
View File
@@ -60,6 +60,18 @@ export interface ContactCoupon {
line: string;
}
/**
* Ein zusätzliches, freiwilliges Feld der Terminanfrage (z. B. Wunschtermin).
* `label` steht über dem Feld in der Sprache der Seite; `mailLabel` beschriftet den
* Wert in der Mail an Anelia – auf Deutsch, auch wenn die Anfrage von /en/ kommt.
*/
export interface ContactField {
name: string;
label: string;
mailLabel: string;
autoComplete?: string;
}
type Status = "idle" | "sending" | "sent" | "error";
export function ContactForm({
@@ -67,6 +79,8 @@ export function ContactForm({
content: form = contactDe.form,
privacyHref = routes.datenschutz,
coupon,
fields = [],
topic,
}: {
className?: string;
/** Ziel des Datenschutz-Links – in der englischen Fassung /en/privacy/. */
@@ -75,6 +89,13 @@ export function ContactForm({
content?: HomeContent["contact"]["form"];
/** Vorteil, der schon an der Anfrage hängt. Ohne Angabe entfällt das Feld. */
coupon?: ContactCoupon;
/** Zusätzliche freiwillige Felder (Terminanfrage der Leistungsseiten). */
fields?: readonly ContactField[];
/**
* Anliegen der Seite, z. B. „Terminanfrage Training“. `/api/contact` macht daraus
* den Betreff, damit die Anfrage im Postfach ohne Öffnen einzuordnen ist.
*/
topic?: string;
}) {
const uid = useId();
const [status, setStatus] = useState<Status>("idle");
@@ -94,12 +115,19 @@ export function ContactForm({
er im Postfach unter dem, was der Absender geschrieben hat, und nicht davor. */
const written = value("message");
/* Nur ausgefüllte Zusatzfelder reisen mit – in der Mail steht keine leere Zeile. */
const details = fields
.map((f) => ({ label: f.mailLabel, value: value(f.name) }))
.filter((d) => d.value);
const payload = {
name: value("name"),
email: value("email"),
company: value("company"),
message: coupon ? `${written}\n\n${coupon.line}` : written,
page: typeof window === "undefined" ? "" : window.location.pathname,
...(topic ? { topic } : {}),
...(details.length ? { details } : {}),
// Honeypot: Menschen sehen das Feld nicht, Bots füllen es aus.
website: value("website"),
};
@@ -120,13 +148,14 @@ export function ContactForm({
`${form.fields.name}: ${payload.name}`,
`${form.fields.email}: ${payload.email}`,
payload.company ? `${form.fields.company}: ${payload.company}` : null,
...details.map((d) => `${d.label}: ${d.value}`),
"",
payload.message,
]
.filter((line) => line !== null)
.join("\r\n");
setMailtoHref(
`mailto:${site.email}?subject=${encodeURIComponent(form.subject)}&body=${encodeURIComponent(body)}`,
`mailto:${site.email}?subject=${encodeURIComponent(topic ?? form.subject)}&body=${encodeURIComponent(body)}`,
);
setStatus("error");
}
@@ -198,6 +227,25 @@ export function ContactForm({
<input id={field("company")} name="company" type="text" autoComplete="organization" className={input} />
</div>
{fields.length > 0 && (
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2">
{fields.map((f) => (
<div key={f.name}>
<label htmlFor={field(f.name)} className={label}>
{f.label}
</label>
<input
id={field(f.name)}
name={f.name}
type="text"
autoComplete={f.autoComplete ?? "off"}
className={input}
/>
</div>
))}
</div>
)}
<div>
<label htmlFor={field("message")} className={label}>
{form.fields.message}
-39
View File
@@ -1,39 +0,0 @@
import { cn } from "@/lib/utils";
import type { FactItem } from "@/content/home";
import { PlaceholderText, visibleFacts } from "./Facts";
export interface FactsStripProps {
items: readonly FactItem[];
className?: string;
}
/**
* Speaker facts: one full-width row of equal cells separated by 1 px
* vertical hairlines. Each cell = DM Mono label (muted) + Jost value.
* Wraps to 2 / 3 columns below 1024 px.
*/
export function FactsStrip({ items, className }: FactsStripProps) {
const rows = visibleFacts(items);
if (!rows.length) return null;
return (
<dl
className={cn(
"grid grid-cols-2 border-y border-line sm:grid-cols-3 lg:grid-cols-6",
"[&>div]:border-l [&>div]:border-line [&>div:first-child]:border-l-0",
"[&>div:nth-child(2n+1)]:max-sm:border-l-0 [&>div:nth-child(3n+1)]:sm:max-lg:border-l-0",
className,
)}
>
{rows.map((f) => (
<div key={f.label} className="flex flex-col gap-2 px-5 py-6 lg:px-6 lg:py-7">
<dt className="font-mono text-xs tracking-[0.06em] text-fg-muted">{f.label}</dt>
<dd className="font-sans text-[1.0625rem] leading-snug text-fg">
<PlaceholderText text={f.value} />
</dd>
</div>
))}
</dl>
);
}
export default FactsStrip;
-50
View File
@@ -1,50 +0,0 @@
import { cn } from "@/lib/utils";
import { Container } from "@/components/layout/Container";
import { GRID } from "@/components/layout/Section";
import { Lines } from "@/components/motion/Lines";
import { Reveal } from "@/components/motion/Reveal";
export interface InterruptionProps {
/** „Mut zum Erfolg.“ */
text: string;
/** DM Mono line below, e.g. „Keynote · herCAREER Expo 2026 · 23.10. · 10:30 · Auditorium 2“. */
mono?: string;
id?: string;
className?: string;
}
/**
* Typographic interruption (keynote page): 100svh ink, one giant Bodoni line
* `clamp(4rem, 2rem + 12vw, 14rem)` in cols 2–12, mono meta in gold below.
* Background stays transparent – <body> paints the ground.
*/
export function Interruption({ text, mono, id, className }: InterruptionProps) {
return (
<section
id={id}
data-theme="dark"
aria-label={text}
className={cn("relative flex min-h-svh items-center bg-transparent py-24 text-fg", className)}
>
<Container className="w-full">
<div className={GRID}>
<div className="col-span-12 lg:col-start-2 lg:col-span-11">
<Lines
as="p"
className="font-display text-[clamp(4rem,2rem+12vw,14rem)] font-normal leading-[0.94] tracking-[-0.015em] text-ivory [text-wrap:balance]"
>
{text}
</Lines>
{mono && (
<Reveal as="p" delay={0.3} className="mt-8 font-mono text-xs tracking-[0.06em] tabular-nums text-gold">
{mono}
</Reveal>
)}
</div>
</div>
</Container>
</section>
);
}
export default Interruption;