feat(portfolio): add operator profile with banner, publication, and honors

This commit is contained in:
kbot
2026-07-06 20:32:12 -05:00
parent f4264886f9
commit efe0b5b394
4 changed files with 495 additions and 0 deletions

View File

@@ -0,0 +1,84 @@
import Link from "next/link";
import type { PostMeta } from "@/lib/posts";
type FieldNotesProps = {
posts: PostMeta[];
};
function formatPostDate(date: string) {
const parsed = new Date(date);
if (Number.isNaN(parsed.getTime())) return date;
return parsed.toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "2-digit",
});
}
export function FieldNotes({ posts }: FieldNotesProps) {
const recentPosts = posts.slice(0, 4);
return (
<section
id="notes"
className="border-y border-blacksite-line bg-blacksite-bg2 px-4 py-24 text-blacksite-text sm:px-6 lg:px-8"
>
<div className="mx-auto max-w-7xl">
<div className="mb-10 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<div>
<p className="section-label mb-3">FIELD NOTES /.04</p>
<h2 className="font-display text-4xl font-semibold tracking-[-0.04em] text-blacksite-text sm:text-6xl">
Blog telemetry from the archive.
</h2>
</div>
<Link
href="/blog/"
className="section-label w-fit rounded-md border border-blacksite-line px-4 py-3 !text-blacksite-text transition-colors hover:border-signal-cyan/70 hover:!text-signal-cyan focus-visible:outline-signal-cyan"
>
All field notes
</Link>
</div>
{recentPosts.length > 0 ? (
<div className="divide-y divide-blacksite-line border-y border-blacksite-line">
{recentPosts.map((post) => (
<Link
key={post.slug}
href={`/blog/${post.slug}/`}
className="group grid gap-3 px-0 py-6 transition-colors hover:bg-blacksite-surface/55 focus-visible:outline-signal-cyan sm:grid-cols-[12rem_minmax(0,1fr)] sm:px-4"
>
<time className="section-label pt-1" dateTime={post.date}>
{formatPostDate(post.date)}
</time>
<span className="min-w-0">
<span className="block font-display text-2xl font-medium tracking-[-0.03em] text-blacksite-text transition-colors group-hover:text-signal-cyan group-focus-visible:text-signal-cyan">
{post.title}
</span>
{post.excerpt && (
<span className="mt-2 block max-w-3xl text-sm leading-6 text-blacksite-muted">
{post.excerpt}
</span>
)}
</span>
</Link>
))}
</div>
) : (
<div className="border border-dashed border-blacksite-line bg-blacksite-surface/45 p-8 sm:p-10">
<p className="section-label mb-5 !text-blacksite-text">
NO FIELD NOTES ON RECORD ARCHIVE OPENS SOON
</p>
<Link
href="/blog/"
className="section-label inline-flex rounded-md border border-blacksite-line px-4 py-3 !text-blacksite-text transition-colors hover:border-signal-cyan/70 hover:!text-signal-cyan focus-visible:outline-signal-cyan"
>
Visit archive
</Link>
</div>
)}
</div>
</section>
);
}

View File

@@ -0,0 +1,68 @@
"use client";
import { m, useReducedMotion } from "motion/react";
import { hero } from "@/lib/portfolio";
import { HeroCanvas } from "./HeroCanvas";
const paletteEventName = "blacksite:palette";
export function Hero() {
const shouldReduceMotion = useReducedMotion();
const openCommandPalette = () => {
window.dispatchEvent(new CustomEvent(paletteEventName));
};
return (
<section
aria-labelledby="hero-heading"
className="relative isolate flex min-h-screen overflow-hidden bg-blacksite-bg px-5 py-28 text-blacksite-text sm:px-8 lg:px-12"
>
<HeroCanvas />
<div className="absolute inset-0 z-0 bg-[radial-gradient(circle_at_20%_20%,rgba(255,90,31,0.12),transparent_30%),linear-gradient(90deg,rgba(3,5,6,0.98),rgba(3,5,6,0.78)_52%,rgba(3,5,6,0.96))]" />
<m.div
initial={shouldReduceMotion ? false : { opacity: 0, y: 18 }}
animate={shouldReduceMotion ? undefined : { opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
className="relative z-10 mx-auto flex w-full max-w-7xl flex-col justify-center"
>
<div className="mb-8 inline-flex w-fit items-center gap-3 border border-blacksite-line bg-blacksite-surface/80 px-4 py-2 font-mono text-[11px] uppercase tracking-[0.16em] text-blacksite-muted shadow-[0_0_40px_rgba(0,0,0,0.35)] backdrop-blur-sm">
<span className="h-2 w-2 rounded-full bg-signal-green shadow-[0_0_14px_rgba(182,255,0,0.75)]" />
[STATUS: ONLINE]
</div>
<h1
id="hero-heading"
className="max-w-6xl font-display text-[clamp(64px,10vw,140px)] leading-[0.92] tracking-[-0.06em] text-balance text-blacksite-text"
>
{hero.heading}
</h1>
<p className="mt-8 max-w-2xl text-lg leading-8 text-blacksite-muted sm:text-xl">
{hero.subline}
</p>
<div className="mt-10 flex flex-col gap-3 sm:flex-row">
<a
href="#work"
className="group inline-flex items-center justify-center border border-blacksite-line border-l-signal-orange bg-blacksite-surface px-6 py-3 font-mono text-xs uppercase tracking-[0.16em] text-blacksite-text transition-colors hover:border-signal-orange hover:bg-blacksite-surface2 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-signal-orange"
>
View Work
<span className="ml-3 text-signal-orange" aria-hidden="true">
/.
</span>
</a>
<button
type="button"
onClick={openCommandPalette}
className="inline-flex items-center justify-center border border-blacksite-line bg-blacksite-bg2/85 px-6 py-3 font-mono text-xs uppercase tracking-[0.16em] text-blacksite-muted transition-colors hover:border-signal-orange hover:text-blacksite-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-signal-orange"
>
Open Command Palette
</button>
</div>
</m.div>
</section>
);
}

View File

@@ -0,0 +1,200 @@
"use client";
import { useEffect, useRef } from "react";
import { useReducedMotion } from "motion/react";
const COLORS = {
bg: "#030506",
line: "rgba(44, 53, 58, 0.46)",
dot: "rgba(244, 247, 248, 0.38)",
cyan: "rgba(0, 240, 255, 0.82)",
};
const GRID_COLUMNS = 14;
const GRID_ROWS = 13;
const PULSE_PERIOD_MS = 5200;
function projectX(width: number, xIndex: number, yIndex: number, time: number) {
const normalizedX = (xIndex / (GRID_COLUMNS - 1) - 0.5) * 2;
const normalizedY = yIndex / (GRID_ROWS - 1);
const depth = normalizedY * normalizedY;
return (
width * 0.5 +
normalizedX * width * (0.13 + depth * 0.5) +
Math.sin(time * 0.00008 + yIndex * 0.45) * 7
);
}
function projectY(height: number, yIndex: number, time: number) {
const normalizedY = yIndex / (GRID_ROWS - 1);
const drift = Math.sin(time * 0.00012 + yIndex * 0.7) * 5;
return height * 0.24 + height * 0.82 * normalizedY * normalizedY + drift;
}
export function HeroCanvas() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const shouldReduceMotion = useReducedMotion();
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) {
return;
}
const context = canvas.getContext("2d", { alpha: true });
if (!context) {
return;
}
let width = 0;
let height = 0;
let dpr = 1;
let frameId = 0;
let isMounted = true;
const resize = () => {
const rect = canvas.getBoundingClientRect();
dpr = Math.min(window.devicePixelRatio || 1, 2);
width = Math.max(1, rect.width);
height = Math.max(1, rect.height);
canvas.width = Math.floor(width * dpr);
canvas.height = Math.floor(height * dpr);
context.setTransform(dpr, 0, 0, dpr, 0, 0);
if (shouldReduceMotion) {
draw(0);
}
};
const drawGridLines = (time: number) => {
context.lineWidth = 1;
context.strokeStyle = COLORS.line;
for (let column = 0; column < GRID_COLUMNS; column += 1) {
context.beginPath();
for (let row = 0; row < GRID_ROWS; row += 1) {
const x = projectX(width, column, row, time);
const y = projectY(height, row, time);
if (row === 0) {
context.moveTo(x, y);
} else {
context.lineTo(x, y);
}
}
context.stroke();
}
for (let row = 0; row < GRID_ROWS; row += 1) {
context.beginPath();
for (let column = 0; column < GRID_COLUMNS; column += 1) {
const x = projectX(width, column, row, time);
const y = projectY(height, row, time);
if (column === 0) {
context.moveTo(x, y);
} else {
context.lineTo(x, y);
}
}
context.stroke();
}
};
const drawDots = (time: number) => {
context.fillStyle = COLORS.dot;
for (let row = 0; row < GRID_ROWS; row += 1) {
const normalizedY = row / (GRID_ROWS - 1);
const radius = 0.9 + normalizedY * 1.15;
for (let column = 0; column < GRID_COLUMNS; column += 1) {
context.beginPath();
context.arc(
projectX(width, column, row, time),
projectY(height, row, time),
radius,
0,
Math.PI * 2,
);
context.fill();
}
}
};
const drawPulse = (time: number) => {
if (shouldReduceMotion) {
return;
}
const progress = (time % PULSE_PERIOD_MS) / PULSE_PERIOD_MS;
const row = 8;
const maxColumn = Math.min(
GRID_COLUMNS - 1,
Math.max(1, Math.floor(progress * (GRID_COLUMNS + 2))),
);
context.save();
context.globalAlpha = progress > 0.84 ? (1 - progress) / 0.16 : 1;
context.lineWidth = 2;
context.strokeStyle = COLORS.cyan;
context.shadowBlur = 14;
context.shadowColor = COLORS.cyan;
context.beginPath();
for (let column = 0; column <= maxColumn; column += 1) {
const x = projectX(width, column, row, time);
const y = projectY(height, row, time);
if (column === 0) {
context.moveTo(x, y);
} else {
context.lineTo(x, y);
}
}
context.stroke();
context.restore();
};
const draw = (time: number) => {
context.clearRect(0, 0, width, height);
context.fillStyle = COLORS.bg;
context.fillRect(0, 0, width, height);
drawGridLines(time);
drawDots(time);
drawPulse(time);
};
const tick = (time: number) => {
if (!isMounted) {
return;
}
draw(time);
frameId = window.requestAnimationFrame(tick);
};
resize();
window.addEventListener("resize", resize, { passive: true });
if (!shouldReduceMotion) {
frameId = window.requestAnimationFrame(tick);
}
return () => {
isMounted = false;
window.removeEventListener("resize", resize);
if (frameId) {
window.cancelAnimationFrame(frameId);
}
};
}, [shouldReduceMotion]);
return (
<canvas
ref={canvasRef}
aria-hidden="true"
className="pointer-events-none absolute inset-0 h-full w-full opacity-[0.32]"
/>
);
}

View File

@@ -0,0 +1,143 @@
import { honors, identity, publication, skillsMatrix } from "@/lib/portfolio";
export function OperatorProfile() {
return (
<section
id="operator"
className="bg-blacksite-bg px-4 py-20 text-blacksite-text sm:px-6 lg:px-8"
>
<div className="mx-auto max-w-7xl">
<div className="section-label mb-4">OPERATOR PROFILE /.06</div>
<div className="overflow-hidden rounded-2xl border border-blacksite-line bg-blacksite-surface shadow-[0_24px_90px_rgba(0,0,0,0.35)]">
<div className="relative h-48 overflow-hidden border-b border-blacksite-line bg-blacksite-bg2 sm:h-56 lg:h-64">
<img
src="/banner.jpg"
alt=""
className="h-full w-full object-cover opacity-45 saturate-50"
/>
<div className="absolute inset-0 bg-black/85" aria-hidden="true" />
<div
className="absolute inset-0 bg-[linear-gradient(90deg,rgba(0,240,255,0.12),transparent_34%,rgba(255,90,31,0.08))]"
aria-hidden="true"
/>
</div>
<div className="grid gap-px bg-blacksite-line lg:grid-cols-[1fr_1.15fr]">
<div className="bg-blacksite-surface p-6 sm:p-8 lg:p-10">
<p className="section-label mb-4">IDENTITY</p>
<h2 className="font-display text-4xl leading-tight tracking-tight text-blacksite-text sm:text-5xl">
{identity.name}
</h2>
<dl className="mt-8 grid gap-5 text-sm text-blacksite-muted sm:text-base">
<div>
<dt className="section-label mb-2">ROLE</dt>
<dd className="text-blacksite-text">{identity.roleLine}</dd>
</div>
<div>
<dt className="section-label mb-2">EDUCATION</dt>
<dd>{identity.education}</dd>
</div>
<div>
<dt className="section-label mb-2">SITE</dt>
<dd>{identity.site}</dd>
</div>
</dl>
</div>
<div className="grid gap-px bg-blacksite-line">
<article className="bg-blacksite-surface2 p-6 sm:p-8 lg:p-10">
<p className="section-label mb-4">PUBLICATION</p>
<h3 className="font-display text-2xl leading-tight text-blacksite-text">
{publication.title}
</h3>
<p className="mt-4 text-sm text-blacksite-muted sm:text-base">
{publication.venue} · {publication.year} · {publication.authorship}
</p>
<a
href={publication.href}
className="section-label mt-6 inline-flex text-signal-cyan underline decoration-signal-cyan/35 underline-offset-4 transition-colors hover:text-blacksite-text focus-visible:outline-signal-cyan"
>
DOI: {publication.doi}
</a>
</article>
<div className="bg-blacksite-surface p-6 sm:p-8 lg:p-10">
<p className="section-label mb-5">HONORS</p>
<ul className="grid gap-4">
{honors.map((honor) => (
<li
key={`${honor.title}-${honor.year}`}
className="grid gap-2 border-l border-signal-orange/50 pl-4 sm:grid-cols-[auto_1fr] sm:items-baseline sm:gap-4"
>
<span className="section-label text-signal-orange">
{honor.year || "RECOGNITION"}
</span>
<span>
<span className="text-blacksite-text">{honor.title}</span>
{honor.detail ? (
<span className="text-blacksite-muted"> {honor.detail}</span>
) : null}
</span>
</li>
))}
</ul>
</div>
</div>
</div>
<div className="border-t border-blacksite-line bg-blacksite-bg2 p-6 sm:p-8 lg:p-10">
<div className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
<div>
<p className="section-label mb-3">SKILLS MATRIX</p>
<h3 className="font-display text-2xl text-blacksite-text">
Domain Capabilities
</h3>
</div>
<p className="section-label text-blacksite-faint">OPERATING SURFACE</p>
</div>
<div className="overflow-hidden rounded-xl border border-blacksite-line">
<table className="w-full border-collapse text-left text-sm sm:text-base">
<thead className="bg-blacksite-surface2">
<tr>
<th scope="col" className="section-label px-4 py-3 sm:px-5">
Domain
</th>
<th scope="col" className="section-label px-4 py-3 sm:px-5">
Capabilities
</th>
</tr>
</thead>
<tbody className="divide-y divide-blacksite-line bg-blacksite-surface">
{skillsMatrix.map((row) => (
<tr key={row.category}>
<th
scope="row"
className="w-56 px-4 py-4 align-top font-mono text-xs uppercase tracking-[0.16em] text-blacksite-text sm:px-5"
>
{row.category}
</th>
<td className="px-4 py-4 text-blacksite-muted sm:px-5">
<div className="flex flex-wrap gap-2">
{row.skills.map((skill) => (
<span
key={skill}
className="rounded-full border border-blacksite-line bg-blacksite-surface2 px-3 py-1 text-xs text-blacksite-text sm:text-sm"
>
{skill}
</span>
))}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</section>
);
}