From 86cc0ad310761e35b5a89bee95cad7df8ce83b4b Mon Sep 17 00:00:00 2001 From: Nick Szaunig Date: Sat, 26 Sep 2026 06:35:29 +0200 Subject: [PATCH] feat(ui): Seite "Fuer Unternehmen" mit Leben - Glaeser, Biene, Zielgruppen - Hero: Titel gross auf dem Bienenfoto, darunter "Fuer ..." mit wechselnder Zielgruppe (Unternehmen, Kanzleien, Praxen, Hotels, Agenturen, Handwerksbetriebe - die Woerter aus dem Untertitel), eine Biene fliegt durchs Bild, Knopf springt zur Anfrage. - Die drei Pakete wieder als Karten, jetzt mit gezeichnetem Honigglas: Klassik mit Banderole, Geschenk im Karton mit Schleife, Individuell mit Stift und Funkeln. Beim Hover hebt sich das Glas, die Schleife wippt. Ein Klick springt zum Formular und waehlt das Paket dort vor (?paket=, nur bekannte Schluessel). - Anfrage: links Ueberschrift und das Glaeser-Foto (mitlaufend, leichte Parallaxe), rechts das Formular. Neu im Woerterbuch nur Ueberschrift, Knopftext und die Zielgruppenliste; keine neuen Aussagen. Dunkelmodus und reduzierte Bewegung beruecksichtigt. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/app/unternehmen/page.tsx | 137 ++++++++++++++++++----- src/components/audience-cycle.tsx | 63 +++++++++++ src/components/business-inquiry-form.tsx | 11 +- src/components/package-jar.tsx | 76 +++++++++++++ src/i18n/dictionaries/de.ts | 4 + src/i18n/dictionaries/en.ts | 4 + src/i18n/dictionary-type.ts | 4 + 7 files changed, 267 insertions(+), 32 deletions(-) create mode 100644 src/components/audience-cycle.tsx create mode 100644 src/components/package-jar.tsx diff --git a/src/app/unternehmen/page.tsx b/src/app/unternehmen/page.tsx index 9f40da8..70c921b 100644 --- a/src/app/unternehmen/page.tsx +++ b/src/app/unternehmen/page.tsx @@ -1,12 +1,17 @@ import type { Metadata } from "next"; -import { Container, SectionHeading } from "@/components/ui"; +import Link from "next/link"; +import { Container, SectionHeading, LinkButton, FramedMedia } from "@/components/ui"; import { HoneycombPattern } from "@/components/honeycomb"; import { ContentImage } from "@/components/content-image"; import { BusinessInquiryForm } from "@/components/business-inquiry-form"; import { ScrollReveal } from "@/components/scroll-reveal"; +import { BeeFlight } from "@/components/bee"; +import { ParallaxMedia } from "@/components/parallax-media"; +import { AudienceCycle } from "@/components/audience-cycle"; +import { PackageJar, type PackageKey } from "@/components/package-jar"; import { getLocale } from "@/i18n/locale"; import { getDictionary } from "@/i18n/get-dictionary"; -import { getContentValue } from "@/lib/content"; +import { getContentValues } from "@/lib/content"; export async function generateMetadata(): Promise { const t = getDictionary(await getLocale()); @@ -17,47 +22,119 @@ export async function generateMetadata(): Promise { }; } -export default async function UnternehmenPage() { +export default async function UnternehmenPage(props: PageProps<"/unternehmen">) { const locale = await getLocale(); const t = getDictionary(locale); - const image = await getContentValue("unternehmen.image", locale); + const tu = t.unternehmenPage; + const { paket } = await props.searchParams; + // Nur bekannte Pakete uebernehmen — der Wert landet als Vorauswahl im Formular. + const selected = tu.packages.find((p) => p.key === paket)?.key; + const content = await getContentValues(["unternehmen.image", "home.businessTeaser.image"], locale); + return (
-
-
+ {/* Hero: Titel auf dem Foto, die Zielgruppen wechseln, eine Biene fliegt durchs Bild. */} +
+
- -
- - - - + ); } diff --git a/src/components/audience-cycle.tsx b/src/components/audience-cycle.tsx new file mode 100644 index 0000000..89ad3f3 --- /dev/null +++ b/src/components/audience-cycle.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { AnimatePresence, motion, useReducedMotion } from "motion/react"; + +const INTERVAL_MS = 2200; + +/** + * "Für Kanzleien" — "Für Praxen" — ...: die Zielgruppe wechselt im Takt und + * rollt von unten herein. Screenreader lesen die ganze Liste auf einmal; + * bei reduzierter Bewegung steht die Liste still und vollstaendig da. + */ +export function AudienceCycle({ + prefix, + items, + className = "", +}: { + prefix: string; + items: string[]; + className?: string; +}) { + const reduceMotion = useReducedMotion(); + const [index, setIndex] = useState(0); + + useEffect(() => { + if (reduceMotion || items.length < 2) return; + const id = window.setInterval(() => setIndex((i) => (i + 1) % items.length), INTERVAL_MS); + return () => window.clearInterval(id); + }, [reduceMotion, items.length]); + + if (reduceMotion) { + return ( +

+ {prefix} {items.join(", ")} +

+ ); + } + + return ( +

+ + {prefix} {items.join(", ")} + + +

+ ); +} diff --git a/src/components/business-inquiry-form.tsx b/src/components/business-inquiry-form.tsx index a2f91c0..2846b56 100644 --- a/src/components/business-inquiry-form.tsx +++ b/src/components/business-inquiry-form.tsx @@ -24,7 +24,14 @@ type Status = "idle" | "loading" | "success" | "error"; * Server Component und reichen ihn als Prop weiter. Diese Datei folgt jetzt * demselben Muster. */ -export function BusinessInquiryForm({ turnstileSiteKey }: { turnstileSiteKey?: string }) { +export function BusinessInquiryForm({ + turnstileSiteKey, + defaultPackage, +}: { + turnstileSiteKey?: string; + /** Vorauswahl, wenn man ueber eine Paketkarte der Seite kommt. */ + defaultPackage?: string; +}) { const { t } = useI18n(); const [status, setStatus] = useState("idle"); const [errorMessage, setErrorMessage] = useState(null); @@ -97,7 +104,7 @@ export function BusinessInquiryForm({ turnstileSiteKey }: { turnstileSiteKey?: s key={p.key} className="group relative flex cursor-pointer flex-col border border-ink/20 bg-background p-4 transition-colors duration-300 hover:border-ink/40 has-[:checked]:border-heading has-[:checked]:bg-forest/[0.04] has-[:focus-visible]:outline has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-honey-dark" > - + {p.title}