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.
94 lines
2.9 KiB
TypeScript
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>
|
|
);
|
|
}
|