From efe0b5b3942dadaeb75c0f174433e90e7a697603 Mon Sep 17 00:00:00 2001 From: kbot Date: Mon, 6 Jul 2026 20:32:12 -0500 Subject: [PATCH] feat(portfolio): add operator profile with banner, publication, and honors --- components/portfolio/FieldNotes.tsx | 84 ++++++++++ components/portfolio/Hero.tsx | 68 ++++++++ components/portfolio/HeroCanvas.tsx | 200 +++++++++++++++++++++++ components/portfolio/OperatorProfile.tsx | 143 ++++++++++++++++ 4 files changed, 495 insertions(+) create mode 100644 components/portfolio/FieldNotes.tsx create mode 100644 components/portfolio/Hero.tsx create mode 100644 components/portfolio/HeroCanvas.tsx create mode 100644 components/portfolio/OperatorProfile.tsx diff --git a/components/portfolio/FieldNotes.tsx b/components/portfolio/FieldNotes.tsx new file mode 100644 index 0000000..adc34bf --- /dev/null +++ b/components/portfolio/FieldNotes.tsx @@ -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 ( +
+
+
+
+

FIELD NOTES /.04

+

+ Blog telemetry from the archive. +

+
+ + All field notes → + +
+ + {recentPosts.length > 0 ? ( +
+ {recentPosts.map((post) => ( + + + + + {post.title} + + {post.excerpt && ( + + {post.excerpt} + + )} + + + ))} +
+ ) : ( +
+

+ NO FIELD NOTES ON RECORD — ARCHIVE OPENS SOON +

+ + Visit archive → + +
+ )} +
+
+ ); +} diff --git a/components/portfolio/Hero.tsx b/components/portfolio/Hero.tsx new file mode 100644 index 0000000..a37d15f --- /dev/null +++ b/components/portfolio/Hero.tsx @@ -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 ( +
+ +
+ + +
+ + [STATUS: ONLINE] +
+ +

+ {hero.heading} +

+ +

+ {hero.subline} +

+ +
+ + View Work + + + + +
+
+
+ ); +} diff --git a/components/portfolio/HeroCanvas.tsx b/components/portfolio/HeroCanvas.tsx new file mode 100644 index 0000000..f06b718 --- /dev/null +++ b/components/portfolio/HeroCanvas.tsx @@ -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(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 ( +