This repository has been archived on 2026-06-19. You can view files and clone it, but cannot push or open issues or pull requests.
gso-landingpage/src/app/components/MountainClimb.tsx
Athena e2168659d4 Sync latest changes + add package-lock.json
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 18:10:26 +02:00

441 lines
17 KiB
TypeScript
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
);
}