Add mountain climbing animation + fix typography scale
- MountainClimb: SVG stick figure climbs 11-step mountain, text card above head each step, flag + bounce at summit, IntersectionObserver auto-start, mobile numbered list fallback - Replace static content grid with MountainClimb animation - Add mc-fade-in + mc-bounce keyframes to globals.css - Typography: IconCard title text-sm→text-base, IconCard sub text-xs→text-sm - Bonus card title text-sm→text-base, desc text-xs→text-sm - FAQ answers text-sm→text-base - WeekTimeline card title text-base→text-lg Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1c83760afd
commit
db7d7530f4
4 changed files with 294 additions and 39 deletions
266
src/app/components/MountainClimb.tsx
Normal file
266
src/app/components/MountainClimb.tsx
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
|
||||
const STEPS = [
|
||||
"Deinen Wunschkunden klar definiert \u2014 wer er ist, was er braucht, was er schon versucht hat",
|
||||
"Dein Angebot auf den Punkt gebracht \u2014 was du anbietest, welches Problem du l\u00f6st",
|
||||
"Deine Positionierung in einem Satz \u2014 klar, sofort verst\u00e4ndlich, f\u00fcr alle Kan\u00e4le nutzbar",
|
||||
"Die komplette Struktur f\u00fcr deine Verkaufsseite \u2014 mit Feedback zur Umsetzung",
|
||||
"Deine Marketing-Systemkarte \u2014 wie Sichtbarkeit, Vertrauen, Anfrage und Kauf zusammenh\u00e4ngen",
|
||||
"Deinen 90-Tage-Marketingplan + Entscheidung welche 2 Kan\u00e4le du fokussierst",
|
||||
"Deinen 4-Wochen Content-Plan + Themen die zeigen wof\u00fcr du stehst \u2014 f\u00fcr alle Plattformen",
|
||||
"Deinen Launch-Plan \u2014 wie du dein Angebot aktiv \u00fcber Social Media in den Markt bringst",
|
||||
"Video-Setup erkl\u00e4rt \u2014 professionelle Videos mit dem Handy",
|
||||
"Dein pers\u00f6nliches Verkaufsskript f\u00fcr Direktansprache und Netzwerk",
|
||||
"Dein Google Business Profil optimiert + 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: 155, y: 500, lean: 9 },
|
||||
{ x: 248, y: 477, lean: 12 },
|
||||
{ x: 338, y: 449, lean: 13 },
|
||||
{ x: 425, y: 416, lean: 16 },
|
||||
{ x: 508, y: 378, lean: 19 },
|
||||
{ x: 587, y: 334, lean: 20 },
|
||||
{ x: 660, y: 290, lean: 19 },
|
||||
{ x: 727, y: 252, lean: 18 },
|
||||
{ x: 787, y: 220, lean: 18 },
|
||||
{ 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 110 510 155 500" +
|
||||
" Q 201.5 488.5 248 477" +
|
||||
" Q 293 463 338 449" +
|
||||
" Q 381.5 432.5 425 416" +
|
||||
" Q 466.5 397 508 378" +
|
||||
" Q 547.5 356 587 334" +
|
||||
" Q 623.5 312 660 290" +
|
||||
" Q 693.5 271 727 252" +
|
||||
" Q 757 236 787 220" +
|
||||
" Q 813.5 206 840 192";
|
||||
|
||||
// Filled mountain silhouette (ridge + right slope + base)
|
||||
const FILL =
|
||||
`M 0 ${VH} L 0 540 L ` +
|
||||
RIDGE.slice(2) + // "65 520 Q …" — continues from first waypoint to summit
|
||||
` L 940 265 L 940 ${VH} Z`;
|
||||
|
||||
const CARD_W = 234;
|
||||
const CARD_H = 128;
|
||||
|
||||
export default function MountainClimb() {
|
||||
const [step, setStep] = useState(-1);
|
||||
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
|
||||
useEffect(() => {
|
||||
if (step < 0 || step >= STEPS.length - 1) return;
|
||||
const t = setTimeout(() => setStep((s) => s + 1), 1600);
|
||||
return () => clearTimeout(t);
|
||||
}, [step]);
|
||||
|
||||
const atSummit = step === STEPS.length - 1;
|
||||
const wp = step >= 0 ? WP[step] : WP[0];
|
||||
|
||||
// Card x: centred on person, clamped inside viewBox
|
||||
const rawCX = wp.x - CARD_W / 2;
|
||||
const cardX = Math.min(Math.max(rawCX, 8), VW - CARD_W - 8);
|
||||
// Card y: above figure head, clamped to top of viewBox
|
||||
const cardY = Math.max(wp.y - CARD_H - 58, 4);
|
||||
|
||||
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" />
|
||||
Komplettpaket
|
||||
</span>
|
||||
<h2 className="mt-4 text-3xl md:text-4xl font-extrabold text-slate-900">
|
||||
Was du in 8 Wochen baust — und mit nach Hause nimmst
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* ── Mountain animation (md+) ── */}
|
||||
<div className="hidden md:block">
|
||||
<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 — receives bounce animation at summit */}
|
||||
<g
|
||||
className={atSummit ? "mc-bounce" : ""}
|
||||
style={{ transformBox: "fill-box", transformOrigin: "50% 100%" }}
|
||||
>
|
||||
{/* Head */}
|
||||
<circle cx="0" cy="-29" r="7.5" fill="none" stroke="#4f46e5" strokeWidth="1.5" />
|
||||
{/* Body */}
|
||||
<line x1="0" y1="-21" x2="0" y2="-3" stroke="#4f46e5" strokeWidth="1.5" strokeLinecap="round" />
|
||||
{/* Left arm (down, holding mountain) */}
|
||||
<line x1="0" y1="-16" x2="-10" y2="-8" stroke="#4f46e5" strokeWidth="1.5" strokeLinecap="round" />
|
||||
{/* Right arm (raised) */}
|
||||
<line x1="0" y1="-16" x2="10" y2="-24" stroke="#4f46e5" strokeWidth="1.5" strokeLinecap="round" />
|
||||
{/* Legs */}
|
||||
<line x1="0" y1="-3" x2="-7" y2="10" stroke="#4f46e5" strokeWidth="1.5" strokeLinecap="round" />
|
||||
<line x1="0" y1="-3" x2="7" y2="10" stroke="#4f46e5" strokeWidth="1.5" strokeLinecap="round" />
|
||||
|
||||
{/* Flag — appears at summit, attached to raised right arm */}
|
||||
{atSummit && (
|
||||
<g className="mc-fade-in" transform="translate(9, -25)">
|
||||
<line x1="0" y1="0" x2="0" y2="-22" stroke="#6366f1" strokeWidth="1.5" strokeLinecap="round" />
|
||||
<polygon points="0,-22 18,-15 0,-8" fill="#6366f1" />
|
||||
</g>
|
||||
)}
|
||||
</g>
|
||||
</g>
|
||||
)}
|
||||
|
||||
{/* ── Text card ── */}
|
||||
{step >= 0 && (
|
||||
<>
|
||||
{/* Dashed connector: card → figure head */}
|
||||
<line
|
||||
x1={cardX + CARD_W / 2}
|
||||
y1={cardY + CARD_H}
|
||||
x2={wp.x}
|
||||
y2={wp.y - 37}
|
||||
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: "5px",
|
||||
}}
|
||||
>
|
||||
Schritt {step + 1} / {STEPS.length}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "12.5px",
|
||||
color: "#334155",
|
||||
lineHeight: 1.55,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{STEPS[step]}
|
||||
</div>
|
||||
</div>
|
||||
</foreignObject>
|
||||
</>
|
||||
)}
|
||||
</svg>
|
||||
|
||||
{/* 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 ↺
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Mobile fallback: numbered list ── */}
|
||||
<div className="md:hidden space-y-3">
|
||||
{STEPS.map((text, i) => (
|
||||
<div key={i} className="mc-card p-4 flex items-start gap-3">
|
||||
<span className="w-7 h-7 rounded-full bg-gradient-to-br from-indigo-500 to-violet-500 text-white text-xs font-bold flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
{i + 1}
|
||||
</span>
|
||||
<p className="text-slate-700 text-sm leading-relaxed">{text}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
@ -141,7 +141,7 @@ function WeekCard({ week, isLeft }: { week: Week; isLeft: boolean }) {
|
|||
)}
|
||||
</div>
|
||||
{/* Title */}
|
||||
<h4 className="font-extrabold text-slate-900 text-base leading-snug">{week.title}</h4>
|
||||
<h4 className="font-extrabold text-slate-900 text-lg leading-snug">{week.title}</h4>
|
||||
{/* Body */}
|
||||
<p className="text-slate-500 text-sm leading-relaxed">{week.body}</p>
|
||||
{/* Vorlagen */}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,23 @@ p {
|
|||
|
||||
details summary::-webkit-details-marker { display: none; }
|
||||
|
||||
/* ── Shared micro-animations ── */
|
||||
@keyframes mc-fade-in {
|
||||
from { opacity: 0; transform: translateY(5px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.mc-fade-in { animation: mc-fade-in 0.35s ease forwards; }
|
||||
|
||||
@keyframes mc-bounce {
|
||||
0% { transform: translateY(0); }
|
||||
30% { transform: translateY(-14px); }
|
||||
55% { transform: translateY(-6px); }
|
||||
75% { transform: translateY(-10px); }
|
||||
90% { transform: translateY(-2px); }
|
||||
100% { transform: translateY(0); }
|
||||
}
|
||||
.mc-bounce { animation: mc-bounce 0.9s ease forwards; }
|
||||
|
||||
/* ── Flip Cards ── */
|
||||
.flip-card-wrapper {
|
||||
perspective: 1000px;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import Image from "next/image";
|
||||
import WeekTimeline from "./components/WeekTimeline";
|
||||
import FlipCards from "./components/FlipCards";
|
||||
import MountainClimb from "./components/MountainClimb";
|
||||
import {
|
||||
CalendarIcon, GradCapIcon, UsersIcon, ClipboardIcon,
|
||||
TargetIcon, LightbulbIcon, DocumentIcon, PhoneIcon,
|
||||
VideoCameraIcon, HandshakeIcon, SearchIcon, MapPinIcon,
|
||||
MapIcon, RocketIcon, PackageIcon, ChatIcon, CheckIcon,
|
||||
VideoCameraIcon, HandshakeIcon, SearchIcon, MapIcon,
|
||||
PackageIcon, ChatIcon, CheckIcon,
|
||||
} from "./components/Icons";
|
||||
|
||||
const CTA_HREF = "#platz-sichern";
|
||||
|
|
@ -37,8 +38,8 @@ function IconCard({ icon, title, sub }: { icon: React.ReactNode; title: string;
|
|||
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-indigo-500 to-violet-500 flex items-center justify-center shadow-lg">
|
||||
{icon}
|
||||
</div>
|
||||
<p className="font-bold text-slate-900 text-sm leading-snug">{title}</p>
|
||||
<p className="text-slate-500 text-xs leading-relaxed">{sub}</p>
|
||||
<p className="font-bold text-slate-900 text-base leading-snug">{title}</p>
|
||||
<p className="text-slate-500 text-sm leading-relaxed">{sub}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -199,38 +200,9 @@ export default function Home() {
|
|||
</section>
|
||||
|
||||
{/* ══════════════════════════════════════════
|
||||
S4 — WAS DU BAUST (Content Grid)
|
||||
S4 — WAS DU BAUST (Mountain animation)
|
||||
══════════════════════════════════════════ */}
|
||||
<section className="py-20">
|
||||
<div className="max-w-5xl mx-auto px-6">
|
||||
<div className="text-center mb-12">
|
||||
<Pill>Komplettpaket</Pill>
|
||||
<h2 className="mt-4 text-3xl md:text-4xl font-extrabold text-slate-900">Was du in 8 Wochen baust — und mit nach Hause nimmst</h2>
|
||||
</div>
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{(
|
||||
[
|
||||
{ icon: <TargetIcon className="w-5 h-5 text-indigo-500 flex-shrink-0 mt-0.5" />, text: "Deinen Wunschkunden klar definiert — wer er ist, was er braucht, was er schon versucht hat" },
|
||||
{ icon: <LightbulbIcon className="w-5 h-5 text-indigo-500 flex-shrink-0 mt-0.5" />, text: "Dein Angebot auf den Punkt gebracht — was du anbietest, welches Problem du löst" },
|
||||
{ icon: <MapPinIcon className="w-5 h-5 text-indigo-500 flex-shrink-0 mt-0.5" />, text: "Deine Positionierung in einem Satz — klar, sofort verständlich, für alle Kanäle nutzbar" },
|
||||
{ icon: <DocumentIcon className="w-5 h-5 text-indigo-500 flex-shrink-0 mt-0.5" />, text: "Die komplette Struktur für deine Verkaufsseite — mit Feedback zur Umsetzung" },
|
||||
{ icon: <MapIcon className="w-5 h-5 text-indigo-500 flex-shrink-0 mt-0.5" />, text: "Deine Marketing-Systemkarte — wie Sichtbarkeit, Vertrauen, Anfrage und Kauf zusammenhängen" },
|
||||
{ icon: <CalendarIcon className="w-5 h-5 text-indigo-500 flex-shrink-0 mt-0.5" />, text: "Deinen 90-Tage-Marketingplan + Entscheidung welche 2 Kanäle du fokussierst" },
|
||||
{ icon: <PhoneIcon className="w-5 h-5 text-indigo-500 flex-shrink-0 mt-0.5" />, text: "Deinen 4-Wochen Content-Plan + Themen die zeigen wofür du stehst — für alle Plattformen" },
|
||||
{ icon: <RocketIcon className="w-5 h-5 text-indigo-500 flex-shrink-0 mt-0.5" />, text: "Deinen Launch-Plan — wie du dein Angebot aktiv über Social Media in den Markt bringst" },
|
||||
{ icon: <VideoCameraIcon className="w-5 h-5 text-indigo-500 flex-shrink-0 mt-0.5" />, text: "Video-Setup erklärt — professionelle Videos mit dem Handy" },
|
||||
{ icon: <HandshakeIcon className="w-5 h-5 text-indigo-500 flex-shrink-0 mt-0.5" />, text: "Dein persönliches Verkaufsskript für Direktansprache und Netzwerk" },
|
||||
{ icon: <SearchIcon className="w-5 h-5 text-indigo-500 flex-shrink-0 mt-0.5" />, text: "Dein Google Business Profil optimiert + SEO-Grundlagen für deine Website" },
|
||||
] as { icon: React.ReactNode; text: string }[]
|
||||
).map(({ icon, text }, i) => (
|
||||
<div key={i} className="mc-card p-4 flex items-start gap-3">
|
||||
{icon}
|
||||
<p className="text-slate-600 text-sm leading-relaxed">{text}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<MountainClimb />
|
||||
|
||||
<WeekTimeline />
|
||||
|
||||
|
|
@ -351,8 +323,8 @@ export default function Home() {
|
|||
<span className="text-xs font-bold text-emerald-600 bg-emerald-50 border border-emerald-200 rounded-full px-2 py-0.5">Wert: {val}</span>
|
||||
</div>
|
||||
<p className="text-xs font-bold text-indigo-600 uppercase tracking-wide">Bonus {n}</p>
|
||||
<p className="font-bold text-slate-900 text-sm">{title}</p>
|
||||
<p className="text-slate-500 text-xs leading-relaxed">{desc}</p>
|
||||
<p className="font-bold text-slate-900 text-base">{title}</p>
|
||||
<p className="text-slate-500 text-sm leading-relaxed">{desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -449,7 +421,7 @@ export default function Home() {
|
|||
{q}
|
||||
<span className="text-indigo-500 group-open:rotate-180 transition-transform flex-shrink-0 ml-4 text-sm">▼</span>
|
||||
</summary>
|
||||
<div className="px-5 pb-5 text-slate-600 text-sm leading-relaxed border-t border-slate-100 pt-4">{a}</div>
|
||||
<div className="px-5 pb-5 text-slate-600 text-base leading-relaxed border-t border-slate-100 pt-4">{a}</div>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
Reference in a new issue