Files
eldov-win/src/components/project-filter.tsx
T
Jan Wagner 6cdd8438ae feat(design): comprehensive redesign — top-tier portfolio UX
Größeres Redesign, das die Seite spürbar besser macht. Behält das
Magenta/Grün-Dark-Thing, poliert aber alles drumherum.

Neue Komponenten:
- hero-banner: full-bleed Hero mit AI-generiertem Hintergrund (grok-imagine-image,
  1280×720, Magenta/Grün-Akzente), Terminal-Boot-Animation mit Tipp-Effekt,
  Rotating-Role-Animation, animiertes Grid-Overlay.
- command-palette: �K / Ctrl+K öffnet ein durchsuchbares Modal mit Navigation,
  Projekt-Suche, Aktionen (Gitea/w-make.com/Admin) und Easter Eggs. Tastatur
  ↑↓/Enter/Esc, gruppiert nach Typ, mit Vorschau-Hints.
- custom-cursor: Canvas-basierter Cursor mit Trail für Desktop, 14 Punkte,
  Farbwechsel Magenta↔Grün, Hover-State für interaktive Elemente. Touch-
  Devices behalten den Default-Cursor.
- scroll-progress: Top-Bar mit Magenta→Grün-Gradient.
- tilt-card: 3D-Hover mit Mouse-Tracking (CSS-Variablen --rx/--ry/--mx/--my).
- stack-icon: 13 handgezeichnete inline SVGs (TypeScript, Node, Python, Rust,
  SQLite, Tailwind, React, Docker, Gitea, Next, Nix, Hermes, Shell, Self-Host).
  Kein CDN, kein Hydration, currentColor-aware.
- project-filter: tag-basierter Filter für Projekt-Liste, Status-Tone-Tags
  (live/warm/muted) plus Kategorie-Tags, useTransition für sanfte Updates.

Pages-Update:
- Home (/): neuer Hero-Banner, Featured-Grid mit Tilt-Cards und KI-generierten
  Thumbnails (hermes.jpg, casino-bot.jpg, polymarket-trader.jpg), darunter
  filterbare Projekt-Liste, Bio + Links Sidebar.
- Projects: filterbar nach Kategorie und Status.
- Project-Detail: Hero-Image (für 3 Projekte), Stack-Pills mit SVG-Icons,
  bessere Sektionierung (~/stack, ~/notes).
- About: Stack-Visualisierung mit 13 Einträgen in einem Grid.
- OpenGraph-Image pro Projekt (Next.js native, 1200×630): eldov-Logo,
  Projekt-Slug in Magenta, Status-Badge, Kategorie-Badge.

Build-Hash-Support:
- NEXT_PUBLIC_BUILD_HASH + NEXT_PUBLIC_BUILD_DATE werden via Docker ARG in
  die Footer geschrieben ("build abc1234 · 2026-08-31"), deploy-vps.sh
  ermittelt Commit-Hash automatisch aus git und gibt ihn weiter.

Theme v2:
- Tiefere Palette (--bg #0a0a10, --bg-deep #050508), --fg-dim für Hierarchie.
- Neue Komponenten-Klassen: tilt-card, stack-pill, tag, hero-bg, hero-banner,
  scroll-progress, cursor-blink, type-in, glow-text.
- ASCII-Box-Hover-Effekt mit Akzent-Border.

Public-Bilder unter public/img/{hero,projects}/ — KI-generiert via xAI
grok-imagine-image (Token aus .hermes-state/credentials/xai.json), alle
im Marken-Stil (Magenta/Grün/dunkel). 4 Bilder, ~960 KB total.

Sonstiges:
- Header mit ⌘K-Hint, sticky+blur.
- Footer mit Build-Hash, externen Links, Datenschutz-Badge.
- About-Page mit Stack-Grid.
2026-08-31 20:58:48 +02:00

94 lines
2.9 KiB
TypeScript

"use client";
import { useState, useTransition, useMemo } from "react";
import type { Project } from "@/lib/models";
import { ProjectCard } from "./project-card";
import type { Locale } from "@/i18n/routes";
type StatusTone = "live" | "warm" | "muted";
function statusTone(s: string): StatusTone {
const v = s.toLowerCase();
if (v.startsWith("aktiv") || v.startsWith("live") || v === "running") return "live";
if (v.includes("early") || v.includes("draft") || v.includes("hardening")) return "warm";
return "muted";
}
export function ProjectFilter({
projects,
locale,
emptyLabel,
}: {
projects: Project[];
locale: Locale;
emptyLabel: string;
}) {
// Sammle alle einzigartigen Kategorien + Status-Töne als Filter-Tags.
const tags = useMemo(() => {
const set = new Set<string>();
projects.forEach((p) => {
const [main] = p.category.split("·");
if (main) set.add(main.trim());
set.add(statusTone(p.status));
});
return Array.from(set);
}, [projects]);
const [active, setActive] = useState<string | null>(null);
const [, start] = useTransition();
const filtered = useMemo(() => {
if (!active) return projects;
return projects.filter((p) => {
if (active === "live" || active === "warm" || active === "muted") {
return statusTone(p.status) === active;
}
const [main] = p.category.split("·");
return main?.trim() === active;
});
}, [projects, active]);
return (
<div>
<div className="mb-6 flex flex-wrap items-center gap-2">
<span className="mr-1 font-mono text-[10px] uppercase tracking-wider text-muted">filter</span>
<button
type="button"
onClick={() => start(() => setActive(null))}
className={`tag ${active === null ? "tag--active" : ""}`}
>
all · {projects.length}
</button>
{tags.map((t) => {
const count = projects.filter((p) => {
if (t === "live" || t === "warm" || t === "muted") return statusTone(p.status) === t;
const [main] = p.category.split("·");
return main?.trim() === t;
}).length;
return (
<button
key={t}
type="button"
onClick={() => start(() => setActive(active === t ? null : t))}
className={`tag ${active === t ? "tag--active" : ""}`}
>
{t} · {count}
</button>
);
})}
</div>
{filtered.length === 0 ? (
<div className="rounded border border-hairline bg-panel p-8 text-center font-mono text-sm text-muted">
{emptyLabel}
</div>
) : (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{filtered.map((p) => (
<div key={p.slug} className="transition-opacity duration-200">
<ProjectCard project={p} locale={locale} showStance />
</div>
))}
</div>
)}
</div>
);
}