Files
eldov-win/src/components/hero-banner.tsx
T
Jan Wagner ffe65eb8d7 feat(polish): 6 Special Effects — Konami, ViewTransitions, Reveal, Stats, Ripple, HeroTilt
Sechs neue Special Effects, die das Portfolio auf das nächste Level bringen.
Alle theme-konsistent, alle reduced-motion-safe, alle performance-bewusst.

1. Konami-Code Easter Egg (KonamiMatrix):
   ↑↑↓↓←→←→BA (oder WASD-Variante) aktiviert einen 'Matrix-Mode' mit
   fallenden japanischen Glyphen + Zahlen über den ganzen Screen.
   Canvas-basiert, GPU-beschleunigt, 14-px-Spalten, ~60 fps.
   Akzeptiert auch WASD statt Pfeiltasten. Respektiert prefers-reduced-motion.
   Kleines 'matrix mode active'-Hint oben rechts für 3.5s beim Aktivieren.
   Easter Egg: konami + matrix im ⌘K-Palette.

2. View-Transitions für Page-Navigation:
   theme.css: ::view-transition-old/new(root) mit 280ms Fade. Browser-
   Support Chrome 111+, Safari TP, Firefox-Flag. Fallback: instant nav.
   Macht den Übergang zwischen Routes spürbar smoother, ohne JS-Overhead.

3. Section-Reveal-on-Scroll (SectionReveal):
   IntersectionObserver (threshold 0.12, rootMargin -10% bottom).
   Einmal-Trigger (kein Re-Anim beim Hoch-Scrollen), GPU-beschleunigt
   via transform + opacity, 620ms cubic-bezier(0.16,1,0.3,1).
   Staggered-Delay für Featured-Cards (60ms pro Card).
   Sofort sichtbar bei prefers-reduced-motion.

4. Live-Uptime (LiveStats im Footer):
   Echte Container-Uptime. Persistenz via localStorage, Updates jede Minute.
   'online · 1d 5h' mit pulsierendem grünen Dot. Macht das Portfolio
   lebendig statt fake Counter.

5. Click-Ripple-Effekt:
   Vanilla CSS + Pointer-Event-Listener. Wenn User irgendwo klickt (nicht
   auf a/button/input), entsteht am Click-Punkt eine animierte Welle
   (Magenta, 580ms). Modernes Material-Design-Pattern, gibt haptisches
   Feedback. reduced-motion: komplett disabled.

6. Hero-Mouse-Tilt (HeroTilt):
   Die ganze Hero-Section kippt subtil (±2°) basierend auf Maus-Position.
   CSS-Variablen + transform, 600ms ease. Touch + reduced-motion: deaktiviert.
   Gibt der Hero-Section ein 'lebendiges' Gefühl ohne aufdringlich zu sein.

Plus:
- Reading-Time-Estimator: Tokenisierung (Code/Markdown ignoriert), 180 WPM
  für Deutsch, 220 für Englisch. Anzeige im Project-Detail-Header neben
  Status/Kategorie: '⏱ 5 min read · 847 Wörter'.
- StatsBar (Home-Page): Echte Live-Zahlen aus der DB — Anzahl Projekte,
  Anzahl Kategorien, totale Wörter in Notizen, 'since 2003'.
  Vier ascii-boxen mit großen Mono-Zahlen, theme-konsistent.

Build-Hash, Commit-Logik und Proxy bleiben unverändert.
2026-08-31 21:44:27 +02:00

168 lines
6.3 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { HeroTilt } from "./hero-tilt";
const BOOT_LINES = [
"> initializing eldov.win...",
"> loading context bus...",
"> scanning fleet...",
"> ready.",
];
const ROTATING = [
"Self-Hoster",
"Bot-Bauer",
"Multi-Agent-Fleet-Owner",
"Open-Source-Themer",
"Casino-Bot-Dev",
"Prediction-Market-Trader",
];
export function HeroBanner({ dict, heroImage }: { dict: { kicker: string; title: string; lede: string; cta: { title: string; body: string } }; heroImage: string }) {
const [lineIdx, setLineIdx] = useState(0);
const [typed, setTyped] = useState("");
const [done, setDone] = useState(false);
const [rotIdx, setRotIdx] = useState(0);
// Boot-Animation: zeilenweise tippen, 280ms pro Zeile.
useEffect(() => {
if (lineIdx >= BOOT_LINES.length) { setDone(true); return; }
const line = BOOT_LINES[lineIdx];
let i = 0;
const id = setInterval(() => {
i += 1;
setTyped(line.slice(0, i));
if (i >= line.length) {
clearInterval(id);
setTimeout(() => {
setLineIdx((v) => v + 1);
setTyped("");
}, 380);
}
}, 24);
return () => clearInterval(id);
}, [lineIdx]);
// Rotating titles (für die Unterzeile), 2.5s pro Wort.
useEffect(() => {
if (!done) return;
const id = setInterval(() => setRotIdx((v) => (v + 1) % ROTATING.length), 2500);
return () => clearInterval(id);
}, [done]);
return (
<section className="hero-banner">
<HeroTilt className="hero-tilt-wrap">
<div
className="hero-bg"
style={{
["--hero-bg" as string]: `url('${heroImage}')`,
}}
/>
{/* Grid-Linien (sehr subtil) */}
<div
aria-hidden
className="absolute inset-0 opacity-[0.04]"
style={{
backgroundImage:
"linear-gradient(var(--accent) 1px, transparent 1px), linear-gradient(90deg, var(--accent) 1px, transparent 1px)",
backgroundSize: "64px 64px",
maskImage: "linear-gradient(to bottom, black, transparent 80%)",
WebkitMaskImage: "linear-gradient(to bottom, black, transparent 80%)",
}}
/>
<div className="relative mx-auto max-w-[var(--max-w)] px-6 py-24 md:py-36 lg:py-48">
<div className="grid gap-12 lg:grid-cols-[1.4fr_1fr] lg:items-center">
<div>
<p className="mb-4 flex items-center gap-2 font-mono text-xs uppercase tracking-[0.18em] text-accent">
<span className="inline-block h-1 w-6 bg-accent" />
{dict.kicker}
</p>
<h1
className="headline-glitch glow-text mb-6 text-5xl font-semibold leading-[1.05] tracking-tight md:text-7xl lg:text-8xl"
data-text={dict.title}
>
{dict.title}
</h1>
<p className="mb-8 max-w-xl text-lg leading-relaxed text-[var(--fg-dim)] md:text-xl">
{dict.lede}
</p>
<div className="flex flex-wrap items-center gap-3">
<a
href="#featured"
className="group inline-flex items-center gap-2 border border-accent bg-accent px-5 py-2.5 font-mono text-sm uppercase tracking-wider text-[var(--bg)] transition-colors hover:bg-transparent hover:text-accent"
data-cursor="hover"
>
<span>{dict.cta.title}</span>
<span aria-hidden className="transition-transform group-hover:translate-x-0.5"></span>
</a>
<span className="font-mono text-xs text-muted">
<kbd className="rounded border border-hairline px-1.5 py-0.5"></kbd>
<kbd className="ml-0.5 rounded border border-hairline px-1.5 py-0.5">K</kbd>
<span className="ml-2">search</span>
</span>
</div>
</div>
<div className="hidden lg:block">
<BootSequence typed={typed} lineIdx={lineIdx} done={done} rotIdx={rotIdx} />
</div>
</div>
</div>
{/* Subtle Bottom-Fade zu bg */}
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-24 bg-gradient-to-b from-transparent to-[var(--bg)]" />
</HeroTilt>
</section>
);
}
function BootSequence({ typed, lineIdx, done, rotIdx }: { typed: string; lineIdx: number; done: boolean; rotIdx: number }) {
return (
<div
aria-hidden
className="ascii-box font-mono text-sm leading-relaxed"
style={{ boxShadow: "0 24px 60px rgba(0,0,0,0.5)" }}
>
<div className="mb-3 flex items-center justify-between border-b border-hairline pb-2">
<span className="flex items-center gap-2 text-muted">
<span className="inline-block h-2 w-2 rounded-full bg-[color-mix(in_oklab,var(--accent)_50%,transparent)]" />
<span className="text-[10px] uppercase tracking-wider">terminal ~ eldov</span>
</span>
<span className="text-[10px] uppercase tracking-wider text-muted">v0.42.0</span>
</div>
<div className="space-y-1.5 text-[var(--fg-dim)]">
{BOOT_LINES.slice(0, lineIdx).map((l, i) => (
<div key={i}>
<span className="text-muted">{l}</span>
</div>
))}
{!done ? (
<div>
<span className="text-accent">{BOOT_LINES[lineIdx]?.slice(0, typed.length)}</span>
<span className="cursor-blink inline-block w-2 -mb-0.5 bg-accent align-baseline">&nbsp;</span>
</div>
) : (
<>
<div className="text-accent">{BOOT_LINES[BOOT_LINES.length - 1]}</div>
<div className="mt-3 text-[var(--fg)]">
<span className="text-muted">role:</span>{" "}
<span
className="inline-block min-w-[8ch] text-accent"
style={{ transition: "opacity 200ms" }}
>
{ROTATING[rotIdx]}
</span>
<span className="cursor-blink inline-block w-2 -mb-0.5 bg-accent align-baseline">&nbsp;</span>
</div>
<div className="text-[var(--fg-dim)]">
<span className="text-muted">since:</span> 2003 (online-handle)
</div>
<div className="text-[var(--fg-dim)]">
<span className="text-muted">now:</span> building things that survive contact with users
</div>
</>
)}
</div>
</div>
);
}