This commit is contained in:
2026-01-29 23:41:54 +01:00
parent 98d3ee2198
commit f506f2d7a3
5 changed files with 9 additions and 594 deletions

View File

@@ -1,11 +1,10 @@
import type { Metadata } from 'next';
import { Inter, Newsreader } from 'next/font/google';
import './globals.css';
import { Analytics } from '../src/components/Analytics';
import { Footer } from '../src/components/Footer';
import { Header } from '../src/components/Header';
import { CTA } from '../src/components/CTA';
import { InteractiveElements } from '../src/components/InteractiveElements';
import { Analytics } from '../src/components/Analytics';
import './globals.css';
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' });
const newsreader = Newsreader({
@@ -40,7 +39,6 @@ export default function RootLayout({
<main>
{children}
</main>
<CTA />
<Footer />
<InteractiveElements />
<Analytics />

View File

@@ -1,14 +1,11 @@
import * as React from 'react';
import { Zap, Shield, ArrowRight, Check, X, Layout, Database, Workflow } from 'lucide-react';
import { ArrowRight, Check, Database, Layout, Shield, Workflow, X, Zap } from 'lucide-react';
import { CirclePattern, ComparisonRow, ConnectorBranch, ConnectorEnd, ConnectorSplit, ConnectorStart, FlowLines, GridLines, HeroLines, ServicesFlow } from '../src/components/Landing';
import { Reveal } from '../src/components/Reveal';
import { Section } from '../src/components/Section';
import { HeroItem, FeatureCard, ComparisonRow, ServiceCard, HeroLines, GridLines, FlowLines, CirclePattern, ServicesFlow, ComparisonLines, ConnectorStart, ConnectorBranch, ConnectorSplit, ConnectorEnd, FlowingPath } from '../src/components/Landing';
export default function LandingPage() {
return (
<div className="flex flex-col bg-white overflow-hidden relative">
{/* Flowing Path Animation - wraps around content */}
<FlowingPath />
{/* Subtle Grid Pattern Overlay */}
<div className="fixed inset-0 pointer-events-none -z-20 opacity-[0.015]" style={{
@@ -146,11 +143,6 @@ export default function LandingPage() {
{/* Section 03: The Difference */}
<Section number="03" title="Der Unterschied" variant="white" borderTop connector={<ConnectorStart className="h-full" />}>
<div className="space-y-12 relative">
{/* Animated Vertical Line */}
<div className="absolute left-1/2 top-24 bottom-0 -translate-x-1/2 hidden md:block w-24 h-full pointer-events-none z-0">
<ComparisonLines className="w-full h-full" />
</div>
<Reveal>
<p className="text-xl md:text-2xl font-serif italic text-slate-600 max-w-2xl relative z-10">
Der klassische Agentur-Weg ist oft langsam und teuer. Mein Ansatz ist radikal anders: Ich baue zuerst, dann reden wir über Details.

View File

@@ -1,40 +0,0 @@
import * as React from 'react';
import Link from 'next/link';
import { ArrowRight } from 'lucide-react';
import { Reveal } from './Reveal';
export const CTA: React.FC = () => {
return (
<section className="container relative py-20 md:py-32">
<Reveal width="100%">
<div className="relative p-12 md:p-20 border border-slate-200 rounded-[3rem] bg-white">
<div className="absolute top-0 left-12 w-px h-24 bg-slate-200 -translate-y-12"></div>
<div className="absolute -right-12 -top-12 text-[15rem] font-bold text-slate-50 select-none -z-10 opacity-50">?</div>
<div className="space-y-16 relative z-10">
<h2 className="text-6xl md:text-8xl font-bold tracking-tighter leading-[0.9] text-slate-900">
Interesse? <br />
<span className="text-slate-100">Fragen kostet nichts.</span>
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-12 items-end">
<p className="text-2xl md:text-3xl text-slate-400 font-serif italic leading-tight">
Schreiben Sie mir einfach, was Sie brauchen. Ich sage Ihnen ehrlich, ob ich es mache.
</p>
<div className="flex flex-col items-start gap-8">
<Link
href="/contact"
className="group inline-flex items-center gap-4 bg-white border border-slate-200 rounded-full px-10 py-5 text-2xl font-bold text-slate-900 hover:border-slate-400 hover:-translate-y-1 transition-all duration-300"
>
Projekt anfragen
<ArrowRight className="w-6 h-6 group-hover:translate-x-2 transition-transform" />
</Link>
</div>
</div>
</div>
</div>
</Reveal>
</section>
);
};

View File

@@ -1,535 +0,0 @@
'use client';
import * as React from 'react';
import { useEffect, useRef, useState, useCallback } from 'react';
import { motion, useScroll, useTransform, useSpring, useAnimationFrame } from 'framer-motion';
interface FlowingPathProps {
className?: string;
}
interface PathNode {
x: number;
y: number;
type: 'curve' | 'line';
}
export const FlowingPath: React.FC<FlowingPathProps> = ({ className = '' }) => {
const containerRef = useRef<HTMLDivElement>(null);
const [pathData, setPathData] = useState<string>('');
const [pathLength, setPathLength] = useState<number>(0);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const pathRef = useRef<SVGPathElement>(null);
const { scrollYProgress } = useScroll();
const [vh, setVh] = useState(0);
useEffect(() => {
setVh(window.innerHeight);
const handleResize = () => setVh(window.innerHeight);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// Calculate the progress needed to keep the dot at the center of the viewport
const centeredProgress = useTransform(
scrollYProgress,
(latest) => {
if (dimensions.height === 0 || vh === 0) return latest;
// Total scrollable distance
const scrollRange = dimensions.height - vh;
const currentScrollY = latest * scrollRange;
// We want the dot to be at y = currentScrollY + vh/2
// But at the very top (latest=0), we want it at y=0 (or above)
// At the very bottom (latest=1), we want it at y=dimensions.height
let targetY;
if (latest < 0.05) {
// Transition from top of page to center of viewport
targetY = (latest / 0.05) * (vh / 2);
} else if (latest > 0.95) {
// Transition from center of viewport to bottom of page
const p = (latest - 0.95) / 0.05;
targetY = (currentScrollY + vh / 2) * (1 - p) + dimensions.height * p;
} else {
targetY = currentScrollY + vh / 2;
}
return Math.max(0, Math.min(1, targetY / dimensions.height));
}
);
// Smooth spring animation for the path drawing
const smoothProgress = useSpring(centeredProgress, {
stiffness: 100, // Much higher stiffness for immediate response
damping: 40, // Higher damping to prevent any overshoot/backward motion
restDelta: 0.0001,
mass: 0.5, // Lower mass for faster acceleration
});
// Transform scroll progress to path drawing
const pathDrawn = useTransform(smoothProgress, [0, 1], [0, 1]);
// Dot transforms
const dot1Distance = useTransform(smoothProgress, [0, 1], ['0%', '100%']);
const dot2Distance = useTransform(smoothProgress, [0, 1], ['0%', '98%']);
// Generate a flowing path that wraps around content sections
const generatePath = useCallback((width: number, height: number): string => {
const margin = width < 768 ? 20 : 80;
const contentWidth = Math.min(width * 0.9, 1200);
const leftEdge = (width - contentWidth) / 2 - margin;
const rightEdge = (width + contentWidth) / 2 + margin;
const centerX = width / 2;
const sectionHeight = height / 8;
const nodes: PathNode[] = [
{ x: centerX - 100, y: 0, type: 'line' },
{ x: centerX - 100, y: sectionHeight * 0.2, type: 'curve' },
// Hero section - wide sweep
{ x: rightEdge, y: sectionHeight * 0.8, type: 'curve' },
{ x: rightEdge - 50, y: sectionHeight * 1.5, type: 'curve' },
// Section 2 - tight curve to left
{ x: leftEdge + 50, y: sectionHeight * 2.2, type: 'curve' },
{ x: leftEdge, y: sectionHeight * 3.0, type: 'curve' },
// Section 3 - middle flow
{ x: centerX + 150, y: sectionHeight * 3.8, type: 'curve' },
{ x: centerX - 150, y: sectionHeight * 4.6, type: 'curve' },
// Section 4 - right side
{ x: rightEdge - 20, y: sectionHeight * 5.4, type: 'curve' },
{ x: rightEdge - 100, y: sectionHeight * 6.2, type: 'curve' },
// Section 5 - back to left
{ x: leftEdge + 100, y: sectionHeight * 7.0, type: 'curve' },
// Final - center
{ x: centerX, y: sectionHeight * 7.6, type: 'curve' },
{ x: centerX, y: height, type: 'line' },
];
let path = `M ${nodes[0].x} ${nodes[0].y}`;
for (let i = 1; i < nodes.length; i++) {
const prev = nodes[i - 1];
const curr = nodes[i];
if (curr.type === 'curve') {
const dy = curr.y - prev.y;
const cp1y = prev.y + dy * 0.4;
const cp2y = prev.y + dy * 0.6;
path += ` C ${prev.x} ${cp1y}, ${curr.x} ${cp2y}, ${curr.x} ${curr.y}`;
} else {
path += ` L ${curr.x} ${curr.y}`;
}
}
return path;
}, []);
// Generate secondary decorative paths that follow the main path with some divergence
const generateSecondaryPath = useCallback((width: number, height: number, seed: number): string => {
const margin = width < 768 ? 20 : 80;
const contentWidth = Math.min(width * 0.9, 1200);
const leftEdge = (width - contentWidth) / 2 - margin;
const rightEdge = (width + contentWidth) / 2 + margin;
const centerX = width / 2;
const sectionHeight = height / 8;
const random = (i: number) => {
const x = Math.sin(seed + i) * 10000;
return x - Math.floor(x);
};
// Base nodes similar to generatePath but with randomized offsets
const nodes = [
{ x: centerX - 100 + (random(0) - 0.5) * 40, y: 0 },
{ x: centerX - 100 + (random(1) - 0.5) * 40, y: sectionHeight * 0.2 },
{ x: rightEdge + (random(2) - 0.5) * 60, y: sectionHeight * 0.8 },
{ x: rightEdge - 50 + (random(3) - 0.5) * 60, y: sectionHeight * 1.5 },
{ x: leftEdge + 50 + (random(4) - 0.5) * 60, y: sectionHeight * 2.2 },
{ x: leftEdge + (random(5) - 0.5) * 60, y: sectionHeight * 3.0 },
{ x: centerX + 150 + (random(6) - 0.5) * 80, y: sectionHeight * 3.8 },
{ x: centerX - 150 + (random(7) - 0.5) * 80, y: sectionHeight * 4.6 },
{ x: rightEdge - 20 + (random(8) - 0.5) * 60, y: sectionHeight * 5.4 },
{ x: rightEdge - 100 + (random(9) - 0.5) * 60, y: sectionHeight * 6.2 },
{ x: leftEdge + 100 + (random(10) - 0.5) * 60, y: sectionHeight * 7.0 },
{ x: centerX + (random(11) - 0.5) * 40, y: sectionHeight * 7.6 },
{ x: centerX + (random(12) - 0.5) * 40, y: height }
];
let path = `M ${nodes[0].x} ${nodes[0].y}`;
for (let i = 1; i < nodes.length; i++) {
const prev = nodes[i - 1];
const curr = nodes[i];
const dy = curr.y - prev.y;
const cp1y = prev.y + dy * 0.4;
const cp2y = prev.y + dy * 0.6;
path += ` C ${prev.x} ${cp1y}, ${curr.x} ${cp2y}, ${curr.x} ${curr.y}`;
}
return path;
}, []);
// Update dimensions and path on resize
useEffect(() => {
const updateDimensions = () => {
if (typeof window !== 'undefined') {
const width = window.innerWidth;
const height = document.documentElement.scrollHeight;
setDimensions({ width, height });
setPathData(generatePath(width, height));
}
};
updateDimensions();
// Debounced resize handler
let timeoutId: NodeJS.Timeout;
const handleResize = () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(updateDimensions, 100);
};
window.addEventListener('resize', handleResize);
// Also update when content might have changed
const observer = new ResizeObserver(handleResize);
if (document.body) {
observer.observe(document.body);
}
return () => {
window.removeEventListener('resize', handleResize);
observer.disconnect();
clearTimeout(timeoutId);
};
}, [generatePath]);
// Get path length after path is rendered
useEffect(() => {
if (pathRef.current) {
setPathLength(pathRef.current.getTotalLength());
}
}, [pathData]);
if (dimensions.width === 0 || dimensions.height === 0) {
return null;
}
return (
<div
ref={containerRef}
className={`absolute inset-0 pointer-events-none overflow-visible ${className}`}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: dimensions.height,
zIndex: 1,
}}
aria-hidden="true"
>
<svg
width={dimensions.width}
height={dimensions.height}
viewBox={`0 0 ${dimensions.width} ${dimensions.height}`}
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={{ position: 'absolute', top: 0, left: 0 }}
>
<defs>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="4" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
<filter id="dotGlow" x="-200%" y="-200%" width="500%" height="500%">
<feGaussianBlur stdDeviation="5" result="blur" />
<feColorMatrix type="matrix" values="0 0 0 0 0.2 0 0 0 0 0.2 0 0 0 0 0.2 0 0 0 1 0" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<linearGradient id="lineGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="rgba(0, 0, 0, 0.05)" />
<stop offset="20%" stopColor="rgba(0, 0, 0, 0.2)" />
<stop offset="50%" stopColor="rgba(0, 0, 0, 0.4)" />
<stop offset="80%" stopColor="rgba(0, 0, 0, 0.2)" />
<stop offset="100%" stopColor="rgba(0, 0, 0, 0.05)" />
</linearGradient>
</defs>
{/* Background decorative paths - static, very subtle */}
<path
d={generateSecondaryPath(dimensions.width, dimensions.height, 10)}
stroke="rgba(0, 0, 0, 0.1)"
strokeWidth="1"
fill="none"
strokeLinecap="round"
strokeDasharray="5,10"
/>
<path
d={generateSecondaryPath(dimensions.width, dimensions.height, 20)}
stroke="rgba(0, 0, 0, 0.08)"
strokeWidth="0.5"
fill="none"
strokeLinecap="round"
/>
{/* Main flowing path - draws as you scroll */}
<motion.path
ref={pathRef}
d={pathData}
stroke="url(#lineGradient)"
strokeWidth="2"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
filter="url(#glow)"
style={{
pathLength: pathDrawn,
}}
/>
{/* Accent path - fully filled, static */}
<path
d={pathData}
stroke="rgba(0, 0, 0, 0.05)"
strokeWidth="4"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
{/* Secondary accent path - fully filled, static */}
<path
d={pathData}
stroke="rgba(0, 0, 0, 0.03)"
strokeWidth="8"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
transform="translate(5, 5)"
/>
{/* Extra static dashed lines with unique random paths */}
<path
d={generateSecondaryPath(dimensions.width, dimensions.height, 123)}
stroke="rgba(0, 0, 0, 0.15)"
strokeWidth="1.5"
fill="none"
strokeDasharray="10,20"
/>
<path
d={generateSecondaryPath(dimensions.width, dimensions.height, 456)}
stroke="rgba(0, 0, 0, 0.12)"
strokeWidth="1"
fill="none"
strokeDasharray="5,15"
/>
<path
d={generateSecondaryPath(dimensions.width, dimensions.height, 789)}
stroke="rgba(0, 0, 0, 0.1)"
strokeWidth="0.8"
fill="none"
strokeDasharray="2,10"
/>
{/* Pulse System */}
<PulseSystem
pathRef={pathRef}
progress={smoothProgress}
pathLength={pathLength}
checkpoints={[0.12, 0.28, 0.45, 0.62, 0.82]}
/>
{/* Animated dot that travels along the path */}
<motion.circle
r="6"
fill="white"
stroke="black"
strokeWidth="1.5"
filter="url(#dotGlow)"
style={{
offsetPath: `path("${pathData}")`,
offsetDistance: dot1Distance,
}}
animate={{
scale: [1, 1.2, 1],
opacity: [0.8, 1, 0.8],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut"
}}
/>
{/* Secondary traveling dot */}
<motion.circle
r="3"
fill="rgba(0, 0, 0, 0.4)"
style={{
offsetPath: `path("${pathData}")`,
offsetDistance: dot2Distance,
}}
animate={{
scale: [1, 1.3, 1],
translateX: [0, 5, -5, 0],
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "easeInOut"
}}
/>
{/* Node markers at key positions */}
{[0.12, 0.28, 0.45, 0.62, 0.82].map((pos, i) => (
<NodeMarker
key={i}
pos={pos}
pathData={pathData}
progress={smoothProgress}
/>
))}
</svg>
</div>
);
};
interface NodeMarkerProps {
pos: number;
pathData: string;
progress: any;
}
const NodeMarker: React.FC<NodeMarkerProps> = ({ pos, pathData, progress }) => {
const isActive = useTransform(
progress,
[pos - 0.05, pos, pos + 0.05],
[0, 1, 1]
);
const scale = useTransform(
progress,
[pos - 0.05, pos, pos + 0.05],
[0.8, 1.4, 1]
);
const glowOpacity = useTransform(
progress,
[pos - 0.02, pos, pos + 0.02],
[0, 0.6, 0]
);
return (
<g>
<motion.circle
r="12"
fill="rgba(148, 163, 184, 0.2)"
style={{
offsetPath: `path("${pathData}")`,
offsetDistance: `${pos * 100}%`,
opacity: glowOpacity,
scale: 2,
}}
/>
<motion.circle
r="6"
fill="white"
stroke="rgba(148, 163, 184, 0.8)"
strokeWidth="2"
style={{
offsetPath: `path("${pathData}")`,
offsetDistance: `${pos * 100}%`,
opacity: isActive,
scale,
}}
/>
<motion.circle
r="2"
fill="rgba(148, 163, 184, 1)"
style={{
offsetPath: `path("${pathData}")`,
offsetDistance: `${pos * 100}%`,
opacity: isActive,
}}
/>
</g>
);
};
interface Pulse {
id: number;
x: number;
y: number;
life: number;
}
const PulseSystem: React.FC<{
pathRef: React.RefObject<SVGPathElement>;
progress: any;
pathLength: number;
checkpoints: number[];
}> = ({ pathRef, progress, pathLength, checkpoints }) => {
const [pulses, setPulses] = useState<Pulse[]>([]);
const lastProgress = useRef(0);
const pulseId = useRef(0);
useAnimationFrame((time, delta) => {
if (!pathRef.current || pathLength === 0) return;
const currentProgress = progress.get();
const point = pathRef.current.getPointAtLength(currentProgress * pathLength);
// Check for checkpoint pulses
checkpoints.forEach(cp => {
const crossed = (lastProgress.current < cp && currentProgress >= cp) ||
(lastProgress.current > cp && currentProgress <= cp);
if (crossed) {
const checkpointPoint = pathRef.current!.getPointAtLength(cp * pathLength);
setPulses(prev => [...prev, {
id: pulseId.current++,
x: checkpointPoint.x,
y: checkpointPoint.y,
life: 1.0,
}].slice(-8));
}
});
lastProgress.current = currentProgress;
// Update pulses
setPulses(prev => prev.map(p => ({ ...p, life: p.life - 0.015 })).filter(p => p.life > 0));
});
return (
<g>
{pulses.map(p => (
<circle
key={p.id}
cx={p.x}
cy={p.y}
r={(1 - p.life) * 150}
fill="none"
stroke="rgba(0, 0, 0, 0.15)"
strokeWidth={p.life * 3}
style={{ opacity: p.life }}
/>
))}
</g>
);
};
export default FlowingPath;

View File

@@ -1,7 +1,7 @@
export * from './HeroItem';
export * from './FeatureCard';
export * from './ComparisonRow';
export * from './ServiceCard';
export * from './AbstractLines';
export * from './ComparisonRow';
export * from './FeatureCard';
export * from './HeroItem';
export * from './ParticleNetwork';
export * from './FlowingPath';
export * from './ServiceCard';