Files
portfolio/src/components/FeaturedNodes.tsx
T
selimaj-devandclaude 85dbdb3db4 Rebuild portfolio in Next.js and Tailwind
Static-export Next.js 16 app with the tactical-map hero, Dajt terrain
background, dossier pages and the projects node index.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-03 20:40:10 +02:00

94 lines
3.8 KiB
TypeScript

"use client";
import Link from "next/link";
import { useEffect, useRef, useState } from "react";
import { featured, projectById, projects } from "@/content/projects";
import { Segment } from "./Segment";
const ROOT = featured[0];
export function FeaturedNodes() {
const [active, setActive] = useState<string | null>(null);
const nodes = featured.map((f) => ({ ...f, project: projectById(f.id)! }));
const stageRef = useRef<HTMLDivElement>(null);
const [size, setSize] = useState<{ w: number; h: number } | null>(null);
// Lines are drawn in px so they stay 1px crisp at any stage size.
useEffect(() => {
const el = stageRef.current;
if (!el) return;
const ro = new ResizeObserver(([e]) => setSize({ w: e.contentRect.width, h: e.contentRect.height }));
ro.observe(el);
return () => ro.disconnect();
}, []);
return (
<div
ref={stageRef}
className="bracket grid-bg relative mt-8 flex flex-col gap-1 border border-white/6 bg-surface p-6 [--bc:rgb(242_244_245/0.35)] md:block md:h-[460px] md:p-0"
onMouseLeave={() => setActive(null)}
>
{size && (
<div aria-hidden="true" className="hidden md:block">
{nodes.slice(1).map((n) => (
<Segment
key={`${n.id}-${active === n.id}`}
x1={(ROOT.x / 100) * size.w}
y1={(ROOT.y / 100) * size.h}
x2={(n.x / 100) * size.w}
y2={(n.y / 100) * size.h}
on={active === n.id}
dashed
strong
/>
))}
</div>
)}
{nodes.map((n) => {
const on = active === n.id;
const root = n.project.root;
return (
<Link
key={n.id}
href={`/projects/#${n.id}`}
onMouseEnter={() => setActive(n.id)}
onFocus={() => setActive(n.id)}
onBlur={() => setActive(null)}
className="group relative block min-h-12 outline-none md:absolute md:top-(--y) md:left-(--x) md:min-h-0"
style={{ "--x": `${n.x}%`, "--y": `${n.y}%` } as React.CSSProperties}
>
<span
aria-hidden="true"
className={`absolute box-border border ${
root
? "top-2.5 left-0 size-7 rounded-full md:-top-3.5 md:-left-3.5"
: "top-4 left-2 size-3 md:-top-1.5 md:-left-1.5"
} ${on ? "border-accent shadow-[0_0_14px_var(--color-accent)]" : "border-ink/60"} ${on && !root ? "bg-accent" : "bg-night"}`}
>
{root && <span className={`absolute top-[9px] left-[9px] size-2 rounded-full ${on ? "bg-accent" : "bg-ink"}`} />}
</span>
<span
className={`bracket ml-10 block min-h-11 px-3 py-2.5 group-focus-visible:outline group-focus-visible:outline-accent md:absolute md:-top-[22px] md:ml-0 ${
root ? "md:right-6 md:w-max md:text-right" : "md:left-6 md:w-[280px]"
} ${
on ? "[--bc:var(--color-accent)]" : "[--bc:transparent]"
}`}
>
<span className={`flex items-baseline gap-3 ${root ? "md:flex-row-reverse" : ""}`}>
<span className="font-display text-lg font-semibold tracking-[0.08em] uppercase">{n.project.name}</span>
<span className="text-[9px] tracking-[0.22em] text-mute">{root ? "ROOT" : n.project.status}</span>
</span>
<span className={`mt-1.5 block text-[11px] leading-[1.55] text-ink-2 ${on && !root ? "" : "md:sr-only"}`}>{n.project.desc}</span>
</span>
</Link>
);
})}
<div aria-hidden="true" className="absolute right-4 bottom-3 hidden text-[9px] tracking-[0.2em] text-mute/70 md:block">
ROOT ORUS · {featured.length} OF {projects.length} NODES
</div>
</div>
);
}