mirror of
https://github.com/Since-3/Kumasy-Marketing.git
synced 2026-10-12 19:51:17 +00:00
230 lines
6.9 KiB
TypeScript
230 lines
6.9 KiB
TypeScript
"use client";
|
|
|
|
import Image from "next/image";
|
|
import { useRef, type ReactNode } from "react";
|
|
import {
|
|
motion,
|
|
useMotionTemplate,
|
|
useReducedMotion,
|
|
useScroll,
|
|
useSpring,
|
|
useTransform,
|
|
} from "motion/react";
|
|
import { Lock, TrendingUp, Users } from "lucide-react";
|
|
|
|
/* ------------------------------------------------------------------ *
|
|
* Browser chrome — glass bar over the screenshot
|
|
* ------------------------------------------------------------------ */
|
|
|
|
export function BrowserFrame({
|
|
children,
|
|
url = "app.kumasy.de",
|
|
className = "",
|
|
}: {
|
|
children: ReactNode;
|
|
url?: string;
|
|
className?: string;
|
|
}) {
|
|
return (
|
|
<div
|
|
className={`glass-rim overflow-hidden rounded-[16px] border border-white/60 bg-white/40 shadow-elevated backdrop-blur-xl ${className}`}
|
|
>
|
|
<div className="flex items-center gap-3 border-b border-white/50 bg-white/55 px-4 py-2.5 backdrop-blur-xl">
|
|
<span className="flex gap-1.5" aria-hidden>
|
|
<span className="size-[9px] rounded-full bg-[#ff5f57]" />
|
|
<span className="size-[9px] rounded-full bg-[#febc2e]" />
|
|
<span className="size-[9px] rounded-full bg-[#28c840]" />
|
|
</span>
|
|
<span
|
|
className="mx-auto flex items-center gap-1.5 rounded-full border border-white/70 bg-white/60 px-3 py-1 text-[11px] text-slate-helper"
|
|
aria-hidden
|
|
>
|
|
<Lock className="size-[11px]" strokeWidth={2.2} />
|
|
{url}
|
|
</span>
|
|
<span className="w-[42px]" aria-hidden />
|
|
</div>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ *
|
|
* Floating stat chip
|
|
*
|
|
* Deliberately opaque, unlike the rest of the glass family. These chips
|
|
* sit directly on top of the dashboard screenshot, so a translucent
|
|
* surface lets the sidebar labels and the chart line read straight
|
|
* through the chip's own text — backdrop-blur cannot fix that, because
|
|
* what is behind is small sharp type rather than a soft wash. Same call
|
|
* the app makes for dialogs: darken behind, never blur, so the content
|
|
* in front stays legible.
|
|
* ------------------------------------------------------------------ */
|
|
|
|
function GlassStat({
|
|
Icon,
|
|
label,
|
|
value,
|
|
tone,
|
|
}: {
|
|
Icon: typeof Users;
|
|
label: string;
|
|
value: string;
|
|
tone: "mint" | "amber";
|
|
}) {
|
|
return (
|
|
<div className="glass-rim w-[196px] rounded-[20px] border border-white/70 bg-paper p-4 shadow-elevated">
|
|
<div className="flex items-center gap-2">
|
|
<span
|
|
className={`grid size-7 place-items-center rounded-full ${
|
|
tone === "mint"
|
|
? "bg-mint-wash text-mint-ink"
|
|
: "bg-amber-soft text-[#8a6d00]"
|
|
}`}
|
|
>
|
|
<Icon className="size-4" strokeWidth={2} />
|
|
</span>
|
|
<span className="text-caption font-semibold uppercase text-slate-helper">
|
|
{label}
|
|
</span>
|
|
</div>
|
|
<p className="mt-2.5 font-display text-subheading font-semibold leading-tight text-ink">
|
|
{value}
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ *
|
|
* ParallaxLayer
|
|
* ------------------------------------------------------------------ */
|
|
|
|
export function ParallaxLayer({
|
|
children,
|
|
depth = 60,
|
|
className = "",
|
|
}: {
|
|
children: ReactNode;
|
|
depth?: number;
|
|
className?: string;
|
|
}) {
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
const reduced = useReducedMotion();
|
|
|
|
const { scrollYProgress } = useScroll({
|
|
target: ref,
|
|
offset: ["start end", "end start"],
|
|
});
|
|
|
|
const y = useSpring(useTransform(scrollYProgress, [0, 1], [depth, -depth]), {
|
|
stiffness: 120,
|
|
damping: 28,
|
|
mass: 0.4,
|
|
});
|
|
|
|
return (
|
|
<motion.div
|
|
ref={ref}
|
|
className={className}
|
|
style={reduced ? undefined : { y }}
|
|
>
|
|
{children}
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ *
|
|
* ShowcaseStage — one product shot, presented cleanly
|
|
* ------------------------------------------------------------------ */
|
|
|
|
export function ShowcaseStage() {
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
const reduced = useReducedMotion();
|
|
|
|
const { scrollYProgress } = useScroll({
|
|
target: ref,
|
|
offset: ["start 0.95", "end 0.2"],
|
|
});
|
|
|
|
// The window tips up from a shallow angle and settles flat, as if being
|
|
// set down on the pedestal. One gesture, not five competing ones.
|
|
const rotateX = useSpring(useTransform(scrollYProgress, [0, 0.6], [12, 0]), {
|
|
stiffness: 90,
|
|
damping: 26,
|
|
});
|
|
const scale = useSpring(useTransform(scrollYProgress, [0, 0.6], [0.94, 1]), {
|
|
stiffness: 90,
|
|
damping: 26,
|
|
});
|
|
const lift = useTransform(scrollYProgress, [0, 0.6], [50, 0]);
|
|
const glowY = useTransform(scrollYProgress, [0, 1], [40, -40]);
|
|
const chipY = useTransform(scrollYProgress, [0, 1], [-46, 46]);
|
|
|
|
const transform = useMotionTemplate`perspective(1600px) rotateX(${rotateX}deg) scale(${scale})`;
|
|
|
|
return (
|
|
<div ref={ref} className="relative mx-auto max-w-[1080px] px-4">
|
|
{/* Pedestal: a warm plinth the window stands on, so it reads as
|
|
placed rather than pasted onto the page. */}
|
|
<div aria-hidden className="pointer-events-none absolute inset-0 -z-10">
|
|
<motion.div
|
|
className="absolute left-1/2 top-[38%] size-[720px] -translate-x-1/2 -translate-y-1/2 rounded-full opacity-70"
|
|
style={{
|
|
background:
|
|
"radial-gradient(circle, rgba(244,192,12,0.45) 0%, rgba(244,192,12,0.12) 45%, transparent 70%)",
|
|
filter: "blur(80px)",
|
|
...(reduced ? {} : { y: glowY }),
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* The product shot. Full opacity, nothing covering it. */}
|
|
<motion.div
|
|
className="relative z-10"
|
|
style={
|
|
reduced
|
|
? undefined
|
|
: { transform, y: lift, transformOrigin: "50% 100%" }
|
|
}
|
|
>
|
|
<BrowserFrame>
|
|
<Image
|
|
src="/dashboard.png"
|
|
alt="Kumasy Dashboard mit Umsatzverlauf über zwölf Monate und Kurs-Kennzahlen"
|
|
width={1899}
|
|
height={905}
|
|
priority
|
|
sizes="(max-width: 1120px) 94vw, 1080px"
|
|
className="h-auto w-full"
|
|
/>
|
|
</BrowserFrame>
|
|
</motion.div>
|
|
|
|
{/* Two chips, clear of the window on either side. They annotate the
|
|
product; they never sit on top of the UI they point at. */}
|
|
<motion.div
|
|
className="absolute -left-4 top-[26%] z-20 hidden 2xl:block"
|
|
style={reduced ? undefined : { y: chipY }}
|
|
>
|
|
<GlassStat
|
|
Icon={Users}
|
|
tone="mint"
|
|
label="Warteliste"
|
|
value="Platz rückt nach"
|
|
/>
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
className="absolute -right-4 top-[58%] z-20 hidden 2xl:block"
|
|
style={reduced ? undefined : { y: chipY }}
|
|
>
|
|
<GlassStat
|
|
Icon={TrendingUp}
|
|
tone="amber"
|
|
label="Bezahlt"
|
|
value="In Echtzeit"
|
|
/>
|
|
</motion.div>
|
|
</div>
|
|
);
|
|
}
|