/* global React */ const { useState, useEffect, useRef } = React; /* ============================================================ SKILLS — grouped by category, drag to rearrange ============================================================ */ const GROUPS = [ { id: "eng", title: "Engineering", sub: "Languages, frameworks, runtime models", icon: "stack", skills: [ { id: "dotnet", cat: "Backend · Core", name: ".NET / C#", detail: "ASP.NET Core 2 → 9, C# 6–13. REST, gRPC, JSON-RPC, WCF. Functional C# with LangExt / OneOf.", yrs: "7 yrs", lvl: 0.95 }, { id: "orleans", cat: "Architecture", name: "Microsoft Orleans", detail: "Distributed actor-model systems in production at Metso — grain design, clustering, persistence.", yrs: "2 yrs", lvl: 0.78 }, { id: "react", cat: "Frontend", name: "React 18 + TypeScript", detail: "Hooks, Suspense, Tailwind / MUI / Bootstrap. Storybook-driven design systems with strict TS.", yrs: "5 yrs", lvl: 0.88 } ] }, { id: "ops", title: "Platform & Operations", sub: "Cloud, infra-as-code, observability", icon: "cloud", skills: [ { id: "azure", cat: "Cloud · Infra", name: "Azure + Terraform", detail: "Migrating VM-era workloads to Azure-native. IaC with Terraform across Azure & AWS, K8s with Helm.", yrs: "4 yrs", lvl: 0.86 }, { id: "obs", cat: "Observability", name: "DataDog · OpenTelemetry", detail: "Tracing, metrics, logging from day one. Grafana dashboards, SLO-driven ops, alert hygiene.", yrs: "3 yrs", lvl: 0.82 } ] }, { id: "data", title: "Data", sub: "Stores, modelling, query design", icon: "data", skills: [ { id: "data", cat: "Databases", name: "SQL · Postgres · Mongo", detail: "MS SQL, PostgreSQL, MongoDB, Redis. Time-series in InfluxDB / Azure Data Explorer.", yrs: "7 yrs", lvl: 0.85 } ] }, { id: "people", title: "Product & Leadership", sub: "Backlog, stakeholders, outcomes", icon: "target", skills: [ { id: "po", cat: "Product · Delivery", name: "Agile / Scrum (PO)", detail: "Backlog ownership, refinement, INVEST stories, release planning. JIRA + Confluence-led.", yrs: "3 yrs", lvl: 0.86 }, { id: "stake", cat: "Leadership", name: "Stakeholder management", detail: "Bridge engineering ↔ business. Written-first comms, demos, expectation-shaping, RACI.", yrs: "4 yrs", lvl: 0.84 } ] } ]; function GroupIcon({ kind }) { const common = { width: 22, height: 22, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" }; if (kind === "stack") { return ( ); } if (kind === "cloud") { return ( ); } if (kind === "data") { return ( ); } if (kind === "target") { return ( ); } return null; } function SkillCard({ skill, dragging, dragOver, innerRef, ...handlers }) { return (
{skill.cat}

{skill.name}

{skill.detail}

Drag to rearrange ↻ {skill.yrs}
); } function SkillGroup({ group }) { const [skills, setSkills] = useState(group.skills); const [draggingId, setDraggingId] = useState(null); const [overId, setOverId] = useState(null); const cardRefs = useRef({}); useEffect(() => { const fallback = setTimeout(() => { Object.values(cardRefs.current).forEach((el) => el && el.classList.add("in")); }, 600); let io; try { io = new IntersectionObserver( (entries) => { entries.forEach((e) => { if (e.isIntersecting) { e.target.classList.add("in"); clearTimeout(fallback); } }); }, { threshold: 0.25 } ); Object.values(cardRefs.current).forEach((el) => el && io.observe(el)); } catch (_) { Object.values(cardRefs.current).forEach((el) => el && el.classList.add("in")); } return () => { clearTimeout(fallback); io && io.disconnect(); }; }, [skills]); const onDragStart = (id) => (e) => { setDraggingId(id); e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", `${group.id}::${id}`); }; const onDragOver = (id) => (e) => { const raw = e.dataTransfer.types.includes("text/plain") ? null : null; // only accept drops from same group — we can't read getData on dragOver, so check draggingId if (!draggingId) return; e.preventDefault(); e.dataTransfer.dropEffect = "move"; if (id !== overId) setOverId(id); }; const onDragLeave = () => setOverId(null); const onDrop = (targetId) => (e) => { e.preventDefault(); const raw = e.dataTransfer.getData("text/plain") || `${group.id}::${draggingId}`; const [grp, sourceId] = raw.split("::"); if (grp !== group.id || !sourceId || sourceId === targetId) { setDraggingId(null); setOverId(null); return; } setSkills((curr) => { const next = [...curr]; const fromIdx = next.findIndex((s) => s.id === sourceId); const toIdx = next.findIndex((s) => s.id === targetId); const [moved] = next.splice(fromIdx, 1); next.splice(toIdx, 0, moved); return next; }); setDraggingId(null); setOverId(null); }; const onDragEnd = () => { setDraggingId(null); setOverId(null); }; return (

{group.title}

{group.sub}
{String(skills.length).padStart(2, "0")} skills
{skills.map((s) => ( (cardRefs.current[s.id] = el)} skill={s} dragging={draggingId === s.id} dragOver={overId === s.id && draggingId !== s.id} onDragStart={onDragStart(s.id)} onDragOver={onDragOver(s.id)} onDragLeave={onDragLeave} onDrop={onDrop(s.id)} onDragEnd={onDragEnd} /> ))}
); } function SkillsSection() { return (
01 / Stack

Built across the stack, still curious.

Grouped by what they actually do. Drag any card within a group to reorder by what matters most to you.

{GROUPS.map((g) => )}
); } Object.assign(window, { SkillsSection });