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:
1 parent
a37e48e4ef
commit
f825e74222
20 files changed
+268
-400
No files matched your search
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user