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.
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user