441 lines
17 KiB
TypeScript
Executable file
441 lines
17 KiB
TypeScript
Executable file
"use client";
|
||
|
||
import { useState, useEffect, useRef } from "react";
|
||
|
||
interface Step {
|
||
title: string;
|
||
sub: string;
|
||
}
|
||
|
||
const STEPS: Step[] = [
|
||
{ title: "Klare Wunschkunden-Persona", sub: "+ vollst\u00e4ndiger Marketing-Audit" },
|
||
{ title: "Was verkaufst du?", sub: "EIN klares Startangebot \u2014 konkret und buchbar" },
|
||
{ title: "Angebot ausgebaut + Positionierung", sub: "Positionierungsformel in einem Satz" },
|
||
{ title: "Verkaufsseite + 90-Tage-Plan", sub: "2 fokussierte Kan\u00e4le entschieden" },
|
||
{ title: "Content-Plan 4 Wochen", sub: "Was, wann und warum du postest" },
|
||
{ title: "Social Media Verkauf", sub: "+ professionelles Video-Setup mit dem Handy" },
|
||
{ title: "Dein Verkaufsskript", sub: "F\u00fcr Direktansprache und Netzwerkveranstaltungen" },
|
||
{ title: "Google Business optimiert", sub: "+ SEO-Grundlagen f\u00fcr deine Website" },
|
||
];
|
||
|
||
// Waypoints along the mountain ridge (SVG user units, viewBox 940 × 580)
|
||
const WP = [
|
||
{ x: 65, y: 520, lean: 8 },
|
||
{ x: 195, y: 488, lean: 12 },
|
||
{ x: 325, y: 450, lean: 15 },
|
||
{ x: 450, y: 407, lean: 17 },
|
||
{ x: 565, y: 358, lean: 20 },
|
||
{ x: 670, y: 303, lean: 22 },
|
||
{ x: 757, y: 248, lean: 24 },
|
||
{ x: 840, y: 192, lean: 0 },
|
||
] as const;
|
||
|
||
const VW = 940;
|
||
const VH = 580;
|
||
|
||
// Smooth quadratic-bezier path through all waypoints (midpoint technique)
|
||
const RIDGE =
|
||
"M 65 520" +
|
||
" Q 130 504 195 488" +
|
||
" Q 260 469 325 450" +
|
||
" Q 387.5 428.5 450 407" +
|
||
" Q 507.5 382.5 565 358" +
|
||
" Q 617.5 330.5 670 303" +
|
||
" Q 713.5 275.5 757 248" +
|
||
" Q 798.5 220 840 192";
|
||
|
||
// Filled mountain silhouette (ridge + right slope + base)
|
||
const FILL =
|
||
`M 0 ${VH} L 0 540 L ` +
|
||
RIDGE.slice(2) +
|
||
` L 940 265 L 940 ${VH} Z`;
|
||
|
||
const CARD_W = 234;
|
||
const CARD_H = 110;
|
||
const SUMMIT_W = 294;
|
||
const SUMMIT_H = 80;
|
||
|
||
export default function MountainClimb() {
|
||
const [step, setStep] = useState(-1);
|
||
const [paused, setPaused] = useState(false);
|
||
const sectionRef = useRef<HTMLDivElement>(null);
|
||
const started = useRef(false);
|
||
|
||
// Auto-start when section scrolls into view
|
||
useEffect(() => {
|
||
const el = sectionRef.current;
|
||
if (!el) return;
|
||
const obs = new IntersectionObserver(
|
||
([e]) => {
|
||
if (e.isIntersecting && !started.current) {
|
||
started.current = true;
|
||
setStep(0);
|
||
}
|
||
},
|
||
{ threshold: 0.25 }
|
||
);
|
||
obs.observe(el);
|
||
return () => obs.disconnect();
|
||
}, []);
|
||
|
||
// Advance one step every 1.6 s — paused when user clicked
|
||
useEffect(() => {
|
||
if (step < 0 || step >= STEPS.length - 1 || paused) return;
|
||
const t = setTimeout(() => setStep((s) => s + 1), 1600);
|
||
return () => clearTimeout(t);
|
||
}, [step, paused]);
|
||
|
||
const atSummit = step === STEPS.length - 1;
|
||
const wp = step >= 0 ? WP[step] : WP[0];
|
||
|
||
// Regular card positioning
|
||
const rawCX = wp.x - CARD_W / 2;
|
||
const cardX = Math.min(Math.max(rawCX, 8), VW - CARD_W - 8);
|
||
const cardY = Math.max(wp.y - CARD_H - 110, 4);
|
||
|
||
// Summit card positioning
|
||
const summitRawCX = wp.x - SUMMIT_W / 2;
|
||
const summitCardX = Math.min(Math.max(summitRawCX, 8), VW - SUMMIT_W - 8);
|
||
const summitCardY = Math.max(wp.y - SUMMIT_H - 120, 4);
|
||
|
||
// Mobile pan: viewport 380×440 follows the person
|
||
const MOB_W = 380, MOB_H = 440;
|
||
const mobPanX = Math.min(0, Math.max(-(VW - MOB_W), -(wp.x - MOB_W / 2)));
|
||
const mobPanY = Math.min(0, Math.max(-(VH - MOB_H), -(wp.y - Math.round(MOB_H * 0.68))));
|
||
|
||
const replay = () => {
|
||
started.current = true;
|
||
setStep(0);
|
||
};
|
||
|
||
return (
|
||
<section className="py-20" ref={sectionRef}>
|
||
<div className="max-w-5xl mx-auto px-6">
|
||
{/* Header */}
|
||
<div className="text-center mb-12">
|
||
<span className="inline-flex items-center gap-2 bg-indigo-50 text-indigo-700 text-xs font-bold uppercase tracking-widest rounded-full px-4 py-2 border border-indigo-100">
|
||
<span className="w-2 h-2 rounded-full bg-indigo-500 animate-pulse" />
|
||
Deine Ergebnisse
|
||
</span>
|
||
<h2 className="mt-4 text-3xl md:text-4xl font-extrabold text-slate-900">
|
||
Nach 8 Wochen nimmst du mit:
|
||
</h2>
|
||
</div>
|
||
|
||
{/* ── Mountain animation (desktop only) ── */}
|
||
<div
|
||
className="hidden md:block"
|
||
onClick={() => !atSummit && setPaused((p) => !p)}
|
||
style={{ cursor: atSummit ? "default" : "pointer" }}
|
||
title={paused ? "Klicken um fortzusetzen" : "Klicken zum Pausieren"}
|
||
>
|
||
<svg
|
||
viewBox={`0 0 ${VW} ${VH}`}
|
||
className="w-full"
|
||
style={{ display: "block", overflow: "visible" }}
|
||
aria-hidden="true"
|
||
>
|
||
<defs>
|
||
<linearGradient id="mgFill" x1="0" y1="0" x2="0" y2="1">
|
||
<stop offset="0%" stopColor="#e0e7ff" stopOpacity="0.55" />
|
||
<stop offset="100%" stopColor="#ede9fe" stopOpacity="0.12" />
|
||
</linearGradient>
|
||
</defs>
|
||
|
||
{/* Mountain silhouette */}
|
||
<path d={FILL} fill="url(#mgFill)" />
|
||
{/* Ridge line */}
|
||
<path d={RIDGE} fill="none" stroke="#a5b4fc" strokeWidth="2.5" strokeLinecap="round" />
|
||
|
||
{/* Step dots */}
|
||
{WP.map((w, i) => (
|
||
<circle
|
||
key={i}
|
||
cx={w.x}
|
||
cy={w.y}
|
||
r={step >= 0 && i <= step ? 5.5 : 3.5}
|
||
fill={step >= 0 && i <= step ? "#6366f1" : "#c7d2fe"}
|
||
style={{ transition: "r 0.3s, fill 0.3s" }}
|
||
/>
|
||
))}
|
||
|
||
{/* ── Stick figure ── */}
|
||
{step >= 0 && (
|
||
<g
|
||
transform={`translate(${wp.x}, ${wp.y}) rotate(${wp.lean})`}
|
||
style={{ transition: "transform 0.75s cubic-bezier(0.4,0.2,0.2,1)" }}
|
||
>
|
||
{/* Inner group — 3× scale + bounce at summit */}
|
||
<g
|
||
transform="scale(3)"
|
||
className={atSummit ? "mc-bounce" : ""}
|
||
style={{ transformBox: "fill-box", transformOrigin: "50% 100%" }}
|
||
>
|
||
{/* strokeWidth 0.5 = visually 1.5 after scale(3) */}
|
||
<circle cx="0" cy="-29" r="7.5" fill="none" stroke="#4f46e5" strokeWidth="0.5" />
|
||
<line x1="0" y1="-21" x2="0" y2="-3" stroke="#4f46e5" strokeWidth="0.5" strokeLinecap="round" />
|
||
<line x1="0" y1="-16" x2="-10" y2="-8" stroke="#4f46e5" strokeWidth="0.5" strokeLinecap="round" />
|
||
<line x1="0" y1="-16" x2="10" y2="-24" stroke="#4f46e5" strokeWidth="0.5" strokeLinecap="round" />
|
||
<line x1="0" y1="-3" x2="-7" y2="10" stroke="#4f46e5" strokeWidth="0.5" strokeLinecap="round" />
|
||
<line x1="0" y1="-3" x2="7" y2="10" stroke="#4f46e5" strokeWidth="0.5" strokeLinecap="round" />
|
||
|
||
{atSummit && (
|
||
<g className="mc-fade-in" transform="translate(9, -25)">
|
||
<line x1="0" y1="0" x2="0" y2="-22" stroke="#6366f1" strokeWidth="0.5" strokeLinecap="round" />
|
||
<polygon points="0,-22 18,-15 0,-8" fill="#6366f1" />
|
||
</g>
|
||
)}
|
||
</g>
|
||
</g>
|
||
)}
|
||
|
||
{/* ── Regular step card ── */}
|
||
{step >= 0 && !atSummit && (
|
||
<>
|
||
{/* Dashed connector: card → figure head */}
|
||
<line
|
||
x1={cardX + CARD_W / 2}
|
||
y1={cardY + CARD_H}
|
||
x2={wp.x}
|
||
y2={wp.y - 90}
|
||
stroke="#c7d2fe"
|
||
strokeWidth="1"
|
||
strokeDasharray="4 3"
|
||
/>
|
||
|
||
{/* foreignObject re-mounts on each step → re-triggers mc-fade-in */}
|
||
<foreignObject
|
||
key={step}
|
||
x={cardX}
|
||
y={cardY}
|
||
width={CARD_W}
|
||
height={CARD_H}
|
||
className="mc-fade-in"
|
||
style={{ overflow: "visible" }}
|
||
>
|
||
<div
|
||
className="bg-white rounded-xl border border-indigo-100 shadow-lg"
|
||
style={{ padding: "10px 12px" }}
|
||
>
|
||
<div
|
||
style={{
|
||
fontSize: "9px",
|
||
fontWeight: 800,
|
||
letterSpacing: "0.12em",
|
||
textTransform: "uppercase",
|
||
color: "#6366f1",
|
||
marginBottom: "4px",
|
||
}}
|
||
>
|
||
Schritt {step + 1} / {STEPS.length}
|
||
</div>
|
||
<div
|
||
style={{
|
||
fontSize: "13px",
|
||
color: "#0f172a",
|
||
fontWeight: 700,
|
||
lineHeight: 1.3,
|
||
marginBottom: "4px",
|
||
}}
|
||
>
|
||
{STEPS[step].title}
|
||
</div>
|
||
<div
|
||
style={{
|
||
fontSize: "11px",
|
||
color: "#64748b",
|
||
lineHeight: 1.4,
|
||
}}
|
||
>
|
||
{STEPS[step].sub}
|
||
</div>
|
||
</div>
|
||
</foreignObject>
|
||
</>
|
||
)}
|
||
|
||
{/* ── Summit celebration card ── */}
|
||
{atSummit && (
|
||
<>
|
||
<line
|
||
x1={summitCardX + SUMMIT_W / 2}
|
||
y1={summitCardY + SUMMIT_H}
|
||
x2={wp.x}
|
||
y2={wp.y - 90}
|
||
stroke="#a5b4fc"
|
||
strokeWidth="1.5"
|
||
strokeDasharray="4 3"
|
||
/>
|
||
<foreignObject
|
||
key="summit"
|
||
x={summitCardX}
|
||
y={summitCardY}
|
||
width={SUMMIT_W}
|
||
height={SUMMIT_H}
|
||
className="mc-fade-in"
|
||
style={{ overflow: "visible" }}
|
||
>
|
||
<div
|
||
style={{
|
||
background: "linear-gradient(135deg, #6366f1, #8b5cf6)",
|
||
borderRadius: "14px",
|
||
padding: "16px 18px",
|
||
boxShadow: "0 8px 32px rgba(99,102,241,0.45)",
|
||
textAlign: "center",
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
fontSize: "15px",
|
||
fontWeight: 800,
|
||
color: "#ffffff",
|
||
lineHeight: 1.35,
|
||
}}
|
||
>
|
||
Ich habe jetzt ein Kundengewinnungssystem
|
||
</div>
|
||
</div>
|
||
</foreignObject>
|
||
</>
|
||
)}
|
||
</svg>
|
||
|
||
{/* Pause hint */}
|
||
{step >= 0 && !atSummit && (
|
||
<p className="text-center text-xs text-slate-400 mt-2 select-none">
|
||
{paused ? "\u25B6 Klicken zum Fortsetzen" : "\u23F8 Klicken zum Pausieren"}
|
||
</p>
|
||
)}
|
||
|
||
{/* Replay button */}
|
||
{atSummit && (
|
||
<div className="text-center mt-4">
|
||
<button
|
||
onClick={replay}
|
||
className="text-sm text-indigo-600 border border-indigo-200 rounded-full px-5 py-2 hover:bg-indigo-50 transition-colors"
|
||
>
|
||
Nochmal ansehen \u21BA
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* ── Mobile: same SVG, viewport follows the person ── */}
|
||
<div
|
||
className="md:hidden"
|
||
onClick={() => !atSummit && setPaused((p) => !p)}
|
||
style={{ cursor: atSummit ? "default" : "pointer" }}
|
||
>
|
||
<svg
|
||
viewBox={`0 0 ${MOB_W} ${MOB_H}`}
|
||
className="w-full rounded-2xl"
|
||
style={{ display: "block", overflow: "hidden", background: "linear-gradient(160deg, #f5f4ff, #f4f7fa)" }}
|
||
aria-hidden="true"
|
||
>
|
||
<defs>
|
||
<linearGradient id="mgFillMob" x1="0" y1="0" x2="0" y2="1">
|
||
<stop offset="0%" stopColor="#e0e7ff" stopOpacity="0.55" />
|
||
<stop offset="100%" stopColor="#ede9fe" stopOpacity="0.12" />
|
||
</linearGradient>
|
||
</defs>
|
||
|
||
{/* Pan group — smoothly follows the person */}
|
||
<g style={{ transform: `translate(${mobPanX}px, ${mobPanY}px)`, transition: "transform 0.75s cubic-bezier(0.4,0.2,0.2,1)" }}>
|
||
|
||
{/* Mountain */}
|
||
<path d={FILL} fill="url(#mgFillMob)" />
|
||
<path d={RIDGE} fill="none" stroke="#a5b4fc" strokeWidth="2.5" strokeLinecap="round" />
|
||
|
||
{/* Dots */}
|
||
{WP.map((w, i) => (
|
||
<circle key={i} cx={w.x} cy={w.y}
|
||
r={step >= 0 && i <= step ? 5.5 : 3.5}
|
||
fill={step >= 0 && i <= step ? "#6366f1" : "#c7d2fe"}
|
||
style={{ transition: "r 0.3s, fill 0.3s" }}
|
||
/>
|
||
))}
|
||
|
||
{/* Stick figure */}
|
||
{step >= 0 && (
|
||
<g
|
||
transform={`translate(${wp.x}, ${wp.y}) rotate(${wp.lean})`}
|
||
style={{ transition: "transform 0.75s cubic-bezier(0.4,0.2,0.2,1)" }}
|
||
>
|
||
<g transform="scale(3)" className={atSummit ? "mc-bounce" : ""}
|
||
style={{ transformBox: "fill-box", transformOrigin: "50% 100%" }}>
|
||
<circle cx="0" cy="-29" r="7.5" fill="none" stroke="#4f46e5" strokeWidth="0.5" />
|
||
<line x1="0" y1="-21" x2="0" y2="-3" stroke="#4f46e5" strokeWidth="0.5" strokeLinecap="round" />
|
||
<line x1="0" y1="-16" x2="-10" y2="-8" stroke="#4f46e5" strokeWidth="0.5" strokeLinecap="round" />
|
||
<line x1="0" y1="-16" x2="10" y2="-24" stroke="#4f46e5" strokeWidth="0.5" strokeLinecap="round" />
|
||
<line x1="0" y1="-3" x2="-7" y2="10" stroke="#4f46e5" strokeWidth="0.5" strokeLinecap="round" />
|
||
<line x1="0" y1="-3" x2="7" y2="10" stroke="#4f46e5" strokeWidth="0.5" strokeLinecap="round" />
|
||
{atSummit && (
|
||
<g className="mc-fade-in" transform="translate(9, -25)">
|
||
<line x1="0" y1="0" x2="0" y2="-22" stroke="#6366f1" strokeWidth="0.5" strokeLinecap="round" />
|
||
<polygon points="0,-22 18,-15 0,-8" fill="#6366f1" />
|
||
</g>
|
||
)}
|
||
</g>
|
||
</g>
|
||
)}
|
||
|
||
{/* Step card */}
|
||
{step >= 0 && !atSummit && (
|
||
<>
|
||
<line x1={cardX + CARD_W / 2} y1={cardY + CARD_H} x2={wp.x} y2={wp.y - 90}
|
||
stroke="#c7d2fe" strokeWidth="1" strokeDasharray="4 3" />
|
||
<foreignObject key={step} x={cardX} y={cardY} width={CARD_W} height={CARD_H}
|
||
className="mc-fade-in" style={{ overflow: "visible" }}>
|
||
<div className="bg-white rounded-xl border border-indigo-100 shadow-lg" style={{ padding: "10px 12px" }}>
|
||
<div style={{ fontSize: "9px", fontWeight: 800, letterSpacing: "0.12em", textTransform: "uppercase", color: "#6366f1", marginBottom: "4px" }}>
|
||
Schritt {step + 1} / {STEPS.length}
|
||
</div>
|
||
<div style={{ fontSize: "13px", color: "#0f172a", fontWeight: 700, lineHeight: 1.3, marginBottom: "4px" }}>
|
||
{STEPS[step].title}
|
||
</div>
|
||
<div style={{ fontSize: "11px", color: "#64748b", lineHeight: 1.4 }}>
|
||
{STEPS[step].sub}
|
||
</div>
|
||
</div>
|
||
</foreignObject>
|
||
</>
|
||
)}
|
||
|
||
{/* Summit card */}
|
||
{atSummit && (
|
||
<>
|
||
<line x1={summitCardX + SUMMIT_W / 2} y1={summitCardY + SUMMIT_H} x2={wp.x} y2={wp.y - 90}
|
||
stroke="#a5b4fc" strokeWidth="1.5" strokeDasharray="4 3" />
|
||
<foreignObject key="summit-mob" x={summitCardX} y={summitCardY} width={SUMMIT_W} height={SUMMIT_H}
|
||
className="mc-fade-in" style={{ overflow: "visible" }}>
|
||
<div style={{ background: "linear-gradient(135deg, #6366f1, #8b5cf6)", borderRadius: "14px", padding: "16px 18px", boxShadow: "0 8px 32px rgba(99,102,241,0.45)", textAlign: "center" }}>
|
||
<div style={{ fontSize: "15px", fontWeight: 800, color: "#ffffff", lineHeight: 1.35 }}>
|
||
Ich habe jetzt ein Kundengewinnungssystem
|
||
</div>
|
||
</div>
|
||
</foreignObject>
|
||
</>
|
||
)}
|
||
|
||
</g>
|
||
</svg>
|
||
|
||
{/* Hints */}
|
||
{step >= 0 && !atSummit && (
|
||
<p className="text-center text-xs text-slate-400 mt-2 select-none">
|
||
{paused ? "▶ Tippen zum Fortsetzen" : "⏸ Tippen zum Pausieren"}
|
||
</p>
|
||
)}
|
||
{atSummit && (
|
||
<div className="text-center mt-3">
|
||
<button onClick={replay} className="text-sm text-indigo-600 border border-indigo-200 rounded-full px-5 py-2 hover:bg-indigo-50 transition-colors">
|
||
Nochmal ansehen ↺
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|