- WeekTimeline Client-Component mit IntersectionObserver - Woche 1,3,5,7 erscheint von links — Woche 2,4,6,8 von rechts - Vertikale Gradient-Linie in der Mitte (indigo → violet) - Nummerierter Dot auf der Linie pro Woche - Modul-Divider (1/2/3) als animierte Trennelemente - CSS-Transitionen: opacity + translateX, 0.6s ease - Expert-Badges auf den Karten (Can, Stefan & Philipp, Manuela) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
247 lines
10 KiB
TypeScript
247 lines
10 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useRef } from "react";
|
||
|
||
interface Week {
|
||
week: string;
|
||
expert?: string;
|
||
title: string;
|
||
body: string;
|
||
vorlagen: string;
|
||
result: string;
|
||
}
|
||
|
||
interface Module {
|
||
label: string;
|
||
headline: string;
|
||
weeks: Week[];
|
||
}
|
||
|
||
const MODULES: Module[] = [
|
||
{
|
||
label: "Modul 1 — Wochen 1 & 2",
|
||
headline: "Wunschkunde, Angebot, Positionierung",
|
||
weeks: [
|
||
{
|
||
week: "Woche 1",
|
||
title: "Dein Wunschkunde + dein Markt",
|
||
body: "Du schaust konkret hin: Wer ist dein Wunschkunde wirklich? Welches Problem hat er — und was hat er schon alles versucht? Was kostet ihn dieses Problem jeden Monat? Du machst einen ehrlichen Marketing-Audit: Was läuft, was nicht — und warum.",
|
||
vorlagen: "Marketing-Audit-Vorlage · Wunschkunden-Persona-Vorlage",
|
||
result: "Klarheit über deinen Wunschkunden — konkret, nicht allgemein.",
|
||
},
|
||
{
|
||
week: "Woche 2",
|
||
title: "Dein unwiderstehliches Angebot + deine Positionierung",
|
||
body: "Du legst dein Angebot auf den Tisch: Was bietest du genau an? Welches Problem löst du? Dann entwickelst du deine Positionierungsformel — für wen, welches Problem, was macht dich anders. In einem Satz. Du übst ihn live in der Gruppe.",
|
||
vorlagen: "Positionierungsformel-Vorlage · Kurztext für Website, Instagram und LinkedIn",
|
||
result: "Dein Angebot klar auf den Punkt — erklärt in einem Satz.",
|
||
},
|
||
],
|
||
},
|
||
{
|
||
label: "Modul 2 — Wochen 3 & 4",
|
||
headline: "Dein System steht.",
|
||
weeks: [
|
||
{
|
||
week: "Woche 3",
|
||
title: "Deine Verkaufsseite",
|
||
body: "Du lernst wie eine Verkaufsseite aufgebaut sein muss — die 7 Pflicht-Elemente, ihre Reihenfolge, die Psychologie dahinter. Du bekommst die komplette Vorlage. Du setzt es direkt auf deiner Seite um — Katja gibt Feedback.",
|
||
vorlagen: "Verkaufsseiten-Vorlage + alle Textelemente · 7-Elemente-Checkliste",
|
||
result: "Vollständige Struktur für deine Verkaufsseite — fertig zum Umsetzen.",
|
||
},
|
||
{
|
||
week: "Woche 4",
|
||
title: "Deine Kanal-Strategie + 90-Tage-Marketingplan",
|
||
body: "Welche 2 Kanäle fokussierst du — auf Basis deiner Zielgruppe, nicht dem was gerade alle machen. Du baust deine Marketing-Systemkarte und startest deinen 90-Tage-Plan.",
|
||
vorlagen: "Kanal-Entscheidungsmatrix · Marketing-Systemkarte · 90-Tage-Marketingplan",
|
||
result: "Gewusst welche 2 Kanäle du fokussierst — und 90-Tage-Plan begonnen.",
|
||
},
|
||
],
|
||
},
|
||
{
|
||
label: "Modul 3 — Wochen 5–8",
|
||
headline: "Sichtbarkeit. Anfragen. Und du weißt wie es weitergeht.",
|
||
weeks: [
|
||
{
|
||
week: "Woche 5",
|
||
expert: "Can Turkdogan · Social Media",
|
||
title: "Dein Content-System",
|
||
body: "Deine 3–4 Content-Themen die zeigen wofür du stehst. Du erstellst live deinen 4-Wochen-Redaktionsplan, schreibst deinen ersten Hook und weißt ab dieser Woche genau: was du postest, wann, und warum.",
|
||
vorlagen: "Content-Säulen-Template · 4-Wochen-Redaktionsplan · 10 bewährte Hook-Formeln",
|
||
result: "Content-Plan für 4 Wochen — weißt genau was und wie du postest.",
|
||
},
|
||
{
|
||
week: "Woche 6",
|
||
expert: "Stefan & Philipp · Onlinewerbevideo",
|
||
title: "Social Media Verkauf + Video-Setup",
|
||
body: "Du lernst wie du dein Angebot aktiv über Social Media verkaufst — mit Strategie, nicht mit Druck. Die Video-Experten zeigen live: professionelle Videos mit dem Handy.",
|
||
vorlagen: "Launch-Checkliste · Equipment-Checkliste · Filming-Guide",
|
||
result: "Plan für Social Media Verkauf + professionell auf Video.",
|
||
},
|
||
{
|
||
week: "Woche 7",
|
||
expert: "Manuela Ludewig · Vertrieb",
|
||
title: "Verkauf + Direktansprache",
|
||
body: "Dein persönliches Verkaufsskript für die Direktansprache — angepasst auf deine Positionierung. Du übst es live in der Gruppe mit konkretem Plan für deine nächsten Kontakte.",
|
||
vorlagen: "Persönliches Verkaufsskript · Follow-up-Template · Event-Recherche-Guide",
|
||
result: "Dein Verkaufsskript für die Direktansprache — einmal geübt.",
|
||
},
|
||
{
|
||
week: "Woche 8",
|
||
expert: "Katja Pestereva · Market Compass",
|
||
title: "Sichtbarkeit auf Google + Abschluss",
|
||
body: "Du optimierst dein Google Business Profil Schritt für Schritt in der Gruppe. Du bringst deine Website auf die Grundlagen die Google belohnt. Abschlussrunde: Was hat sich in 8 Wochen verändert?",
|
||
vorlagen: "SEO-Checkliste · Keyword-Recherche-Vorlage · Website-Leitfaden",
|
||
result: "Google Business vollständig optimiert + Seite für dein Haupt-Keyword.",
|
||
},
|
||
],
|
||
},
|
||
];
|
||
|
||
/* Running week counter so odd/even alternates across all modules */
|
||
let globalWeekIndex = 0;
|
||
|
||
function WeekCard({ week, isLeft }: { week: Week; isLeft: boolean }) {
|
||
const ref = useRef<HTMLDivElement>(null);
|
||
|
||
useEffect(() => {
|
||
const el = ref.current;
|
||
if (!el) return;
|
||
const obs = new IntersectionObserver(
|
||
([entry]) => {
|
||
if (entry.isIntersecting) {
|
||
el.classList.add("tl-visible");
|
||
obs.disconnect();
|
||
}
|
||
},
|
||
{ threshold: 0.15 }
|
||
);
|
||
obs.observe(el);
|
||
return () => obs.disconnect();
|
||
}, []);
|
||
|
||
return (
|
||
<div
|
||
ref={ref}
|
||
className={`tl-card ${isLeft ? "tl-from-left" : "tl-from-right"} w-full md:w-[46%] ${
|
||
isLeft ? "md:mr-auto md:pr-10" : "md:ml-auto md:pl-10"
|
||
}`}
|
||
>
|
||
<div className="mc-card p-6 space-y-3 hover:shadow-lg hover:shadow-indigo-100 transition-shadow">
|
||
{/* Header */}
|
||
<div className="flex flex-wrap items-center gap-2">
|
||
<span className="bg-gradient-to-r from-indigo-500 to-violet-500 text-white text-xs font-bold px-3 py-1 rounded-full uppercase tracking-wide">
|
||
{week.week}
|
||
</span>
|
||
{week.expert && (
|
||
<span className="bg-indigo-50 text-indigo-700 text-xs font-medium px-3 py-1 rounded-full border border-indigo-100">
|
||
{week.expert}
|
||
</span>
|
||
)}
|
||
</div>
|
||
{/* Title */}
|
||
<h4 className="font-extrabold text-slate-900 text-base leading-snug">{week.title}</h4>
|
||
{/* Body */}
|
||
<p className="text-slate-500 text-sm leading-relaxed">{week.body}</p>
|
||
{/* Vorlagen */}
|
||
<p className="text-xs text-indigo-600 italic border-t border-slate-100 pt-3">
|
||
📋 {week.vorlagen}
|
||
</p>
|
||
{/* Result */}
|
||
<p className="text-sm font-semibold text-emerald-600">✓ {week.result}</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default function WeekTimeline() {
|
||
/* Reset global counter each render (SSR-safe: only matters client-side) */
|
||
globalWeekIndex = 0;
|
||
|
||
return (
|
||
<section className="py-20">
|
||
<div className="max-w-5xl mx-auto px-6">
|
||
{/* Section header */}
|
||
<div className="text-center mb-16">
|
||
<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" />
|
||
8 Wochen · Schritt für Schritt
|
||
</span>
|
||
<h2 className="mt-4 text-3xl md:text-4xl font-extrabold text-slate-900">
|
||
Dein Programm — Woche für Woche
|
||
</h2>
|
||
<p className="mt-3 text-slate-500 max-w-xl mx-auto">
|
||
Jede Woche baut auf der nächsten auf. Am Ende hast du kein Zertifikat — sondern ein fertiges System.
|
||
</p>
|
||
</div>
|
||
|
||
{MODULES.map((mod) => (
|
||
<div key={mod.label} className="mb-6">
|
||
{/* Module divider */}
|
||
<ModuleDivider label={mod.label} headline={mod.headline} />
|
||
|
||
{/* Weeks */}
|
||
<div className="relative">
|
||
{/* Vertical timeline line */}
|
||
<div className="hidden md:block absolute left-1/2 top-0 bottom-0 w-0.5 bg-gradient-to-b from-indigo-300 via-violet-300 to-indigo-200 -translate-x-1/2" />
|
||
|
||
<div className="flex flex-col gap-10 py-6">
|
||
{mod.weeks.map((week) => {
|
||
const isLeft = globalWeekIndex % 2 === 0;
|
||
globalWeekIndex++;
|
||
return (
|
||
<div key={week.week} className="relative flex items-start">
|
||
{/* Timeline dot — centered */}
|
||
<div className="hidden md:flex absolute left-1/2 -translate-x-1/2 top-7 w-8 h-8 rounded-full bg-gradient-to-br from-indigo-500 to-violet-500 items-center justify-center shadow-lg shadow-indigo-200 z-10 flex-shrink-0">
|
||
<span className="text-white text-xs font-bold">
|
||
{week.week.replace("Woche ", "")}
|
||
</span>
|
||
</div>
|
||
<WeekCard week={week} isLeft={isLeft} />
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
function ModuleDivider({ label, headline }: { label: string; headline: string }) {
|
||
const ref = useRef<HTMLDivElement>(null);
|
||
|
||
useEffect(() => {
|
||
const el = ref.current;
|
||
if (!el) return;
|
||
const obs = new IntersectionObserver(
|
||
([entry]) => {
|
||
if (entry.isIntersecting) {
|
||
el.classList.add("tl-visible");
|
||
obs.disconnect();
|
||
}
|
||
},
|
||
{ threshold: 0.2 }
|
||
);
|
||
obs.observe(el);
|
||
return () => obs.disconnect();
|
||
}, []);
|
||
|
||
return (
|
||
<div
|
||
ref={ref}
|
||
className="tl-card tl-from-bottom text-center py-8 mb-2"
|
||
>
|
||
<div className="inline-flex items-center gap-3">
|
||
<div className="h-px w-12 bg-gradient-to-r from-transparent to-indigo-300" />
|
||
<span className="bg-gradient-to-r from-indigo-600 to-violet-600 bg-clip-text text-transparent text-xs font-extrabold uppercase tracking-widest">
|
||
{label}
|
||
</span>
|
||
<div className="h-px w-12 bg-gradient-to-l from-transparent to-indigo-300" />
|
||
</div>
|
||
<h3 className="mt-2 text-xl md:text-2xl font-extrabold text-slate-900">{headline}</h3>
|
||
</div>
|
||
);
|
||
}
|