mirror of
https://github.com/Since-3/Kumasy-Marketing.git
synced 2026-10-12 19:51:17 +00:00
212 lines
7.2 KiB
TypeScript
212 lines
7.2 KiB
TypeScript
"use client";
|
|
|
|
import { AnimatePresence, motion, useScroll, useTransform } from "motion/react";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { BadgeCheck, CreditCard, ShieldCheck, Server, MapPin } from "lucide-react";
|
|
import { Eyebrow, WaitlistForm } from "./ui";
|
|
import { KineticLine, Reveal, useReducedMotion } from "./motion";
|
|
import { ShowcaseStage } from "./mockup";
|
|
|
|
const TRUST = [
|
|
{ label: "Zahlungen über Stripe", Icon: CreditCard },
|
|
{ label: "DSGVO-konform", Icon: ShieldCheck },
|
|
{ label: "Hosting in Frankfurt", Icon: MapPin },
|
|
{ label: "GoBD-konforme Rechnungen", Icon: BadgeCheck },
|
|
{ label: "Entwickelt in Deutschland", Icon: Server },
|
|
];
|
|
|
|
/**
|
|
* Ambient backdrop: a slow aurora wash plus amber and navy blobs.
|
|
* This is what the glass surfaces in front actually blur — without it
|
|
* the backdrop-filter has nothing to work with.
|
|
*/
|
|
function Ambience() {
|
|
const reduced = useReducedMotion();
|
|
const { scrollY } = useScroll();
|
|
const glowY = useTransform(scrollY, [0, 800], [0, 160]);
|
|
|
|
return (
|
|
<div
|
|
aria-hidden
|
|
className="pointer-events-none absolute inset-0 -z-10 overflow-hidden"
|
|
>
|
|
<div className="aurora absolute inset-0" />
|
|
|
|
<motion.div
|
|
className="absolute left-1/2 top-[-20%] size-[880px] -translate-x-1/2 rounded-full opacity-60"
|
|
style={{
|
|
background:
|
|
"radial-gradient(circle, rgba(244,192,12,0.55) 0%, rgba(255,216,77,0.28) 38%, transparent 70%)",
|
|
filter: "blur(90px)",
|
|
...(reduced ? {} : { y: glowY }),
|
|
}}
|
|
/>
|
|
<div
|
|
className="blob absolute -left-40 top-1/3 size-[460px] rounded-full opacity-45"
|
|
style={{
|
|
background:
|
|
"radial-gradient(circle, rgba(30,41,59,0.34) 0%, transparent 68%)",
|
|
filter: "blur(80px)",
|
|
}}
|
|
/>
|
|
<div
|
|
className="blob absolute -right-32 top-24 size-[420px] rounded-full opacity-50"
|
|
style={{
|
|
background:
|
|
"radial-gradient(circle, rgba(244,192,12,0.45) 0%, transparent 68%)",
|
|
filter: "blur(80px)",
|
|
animationDelay: "-8s",
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const REPLACED = [
|
|
"die Excel-Tabelle",
|
|
"die DM-Flut",
|
|
"den Zettel am Empfang",
|
|
"das Nachfassen per WhatsApp",
|
|
"die Bar-Kasse",
|
|
];
|
|
|
|
/** Cycles through what Kumasy replaces, one word swap at a time. */
|
|
function ReplacedWords() {
|
|
const reduced = useReducedMotion();
|
|
const [index, setIndex] = useState(0);
|
|
|
|
useEffect(() => {
|
|
if (reduced) return;
|
|
const id = window.setInterval(
|
|
() => setIndex((i) => (i + 1) % REPLACED.length),
|
|
2600,
|
|
);
|
|
return () => window.clearInterval(id);
|
|
}, [reduced]);
|
|
|
|
if (reduced) {
|
|
// Static fallback: name them all rather than animating through them.
|
|
return <span className="font-medium text-ink">{REPLACED.join(", ")}.</span>;
|
|
}
|
|
|
|
return (
|
|
// Fixed height + overflow clip so the swap never reflows the line.
|
|
<span className="relative inline-flex h-[1.6em] items-center overflow-hidden align-bottom">
|
|
<AnimatePresence mode="wait" initial={false}>
|
|
<motion.span
|
|
key={index}
|
|
className="whitespace-nowrap font-medium text-ink"
|
|
initial={{ y: "100%", opacity: 0 }}
|
|
animate={{ y: "0%", opacity: 1 }}
|
|
exit={{ y: "-100%", opacity: 0 }}
|
|
transition={{ duration: 0.42, ease: [0.22, 1, 0.36, 1] }}
|
|
>
|
|
{REPLACED[index]}
|
|
</motion.span>
|
|
</AnimatePresence>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
export function Hero() {
|
|
const ref = useRef<HTMLElement>(null);
|
|
const reduced = useReducedMotion();
|
|
|
|
const { scrollYProgress } = useScroll({
|
|
target: ref,
|
|
offset: ["start start", "end start"],
|
|
});
|
|
const copyY = useTransform(scrollYProgress, [0, 1], [0, -90]);
|
|
const copyOpacity = useTransform(scrollYProgress, [0, 0.65], [1, 0]);
|
|
|
|
return (
|
|
<section
|
|
ref={ref}
|
|
className="relative isolate overflow-hidden px-4 pb-24 pt-32 sm:pt-40"
|
|
>
|
|
<Ambience />
|
|
|
|
<div className="mx-auto max-w-[1200px]">
|
|
<motion.div
|
|
className="mx-auto max-w-[860px] text-center"
|
|
style={reduced ? undefined : { y: copyY, opacity: copyOpacity }}
|
|
>
|
|
<Reveal>
|
|
<Eyebrow>Pilotphase · Zugang über Warteliste</Eyebrow>
|
|
</Reveal>
|
|
|
|
{/* Per-character entrance. The <h1> carries the readable text for
|
|
assistive tech; the animated spans are aria-hidden. */}
|
|
{/* The floor is 2.1rem, not 2.75rem: the longest line ("und
|
|
bezahlen sich") has to fit 321px of usable width on a 353px
|
|
Samsung viewport. 10.2vw carries it from there up to the
|
|
5.25rem ceiling. */}
|
|
<h1 className="mt-7 text-balance font-display text-[clamp(2.1rem,10.2vw,5.25rem)] font-semibold leading-[0.96] tracking-[-0.055em] text-ink">
|
|
<span className="sr-only">
|
|
Deine Kurse füllen und bezahlen sich von selbst.
|
|
</span>
|
|
<span className="block">
|
|
<KineticLine text="Deine Kurse füllen" />
|
|
</span>
|
|
<span className="block">
|
|
<KineticLine text="und bezahlen sich" delay={0.16} />
|
|
</span>
|
|
<span className="block">
|
|
<KineticLine text="von " delay={0.32} />
|
|
{/* Rises as one block so the gradient spans the word — see
|
|
KineticLine for why this can't be per-character. The delay
|
|
continues the stagger of "von " so the line reads as one. */}
|
|
<KineticLine
|
|
text="selbst."
|
|
delay={0.42}
|
|
gradient
|
|
className="italic"
|
|
/>
|
|
</span>
|
|
</h1>
|
|
|
|
<Reveal delay={0.55}>
|
|
<p className="mx-auto mt-8 max-w-[540px] text-body-lg text-slate-body">
|
|
Kumasy bündelt Kurse, Räume, Trainer, Buchungen und Zahlungen in
|
|
einer Oberfläche. Deine Teilnehmer:innen buchen und bezahlen in
|
|
unter zwei Minuten selbst — ohne DM, ohne Rückfrage, ohne Excel.
|
|
</p>
|
|
</Reveal>
|
|
|
|
<Reveal delay={0.65}>
|
|
<div className="mx-auto mt-9 max-w-[560px]" id="warteliste">
|
|
<WaitlistForm source="Hero" />
|
|
</div>
|
|
</Reveal>
|
|
|
|
<Reveal delay={0.75}>
|
|
<ul className="mt-9 flex flex-wrap items-center justify-center gap-x-5 gap-y-2.5">
|
|
{TRUST.map(({ label, Icon }) => (
|
|
<li
|
|
key={label}
|
|
className="flex items-center gap-1.5 text-[13px] text-slate-helper"
|
|
>
|
|
<Icon className="size-3.5 shrink-0" strokeWidth={1.8} />
|
|
{label}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</Reveal>
|
|
|
|
{/* One live line naming what Kumasy replaces — it keeps the hero
|
|
moving after the entrance without adding another surface. */}
|
|
<Reveal delay={0.85}>
|
|
<p className="mt-10 flex flex-wrap items-center justify-center gap-x-2 text-body text-slate-helper">
|
|
<span>Ersetzt</span>
|
|
<ReplacedWords />
|
|
</p>
|
|
</Reveal>
|
|
</motion.div>
|
|
</div>
|
|
|
|
<div className="mt-20 px-2">
|
|
<ShowcaseStage />
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|