This site uses one functional cookie to keep feature rollouts consistent for you. Nothing is set until you choose. See the privacy notice.
Dev notes
How I turned 11 old jobs into a single interactive page, and the handful of decisions that made it buildable without turning into a museum of dead apps.
Past work rots. The apps have dead backends, retired auth, paid licenses, and client names you can't show. A list of "things I built" is either a wall of dead links or a wall of screenshots. I wanted something you could actually touch.
The originals leaned on MUI, ECharts, AG Grid, a node-graph library, gridstack, a code editor. Pulling all of that in to mimic them would bloat the bundle for a portfolio page, which is exactly the kind of thing the tree-shaking write-up argues against.
recharts already in the tree. The drag-drop dashboard is CSS grid, the node graph is hand-built SVG, the "code editor" is a read-only pre.next/dynamic, so the page ships only the demo on screen, never all 23 at once.?feature= deep links. prefers-reduced-motion drops the animation entirely.ComingSoonDemo placeholder.Update — August 10, 2026
The dual tickers above were written for this page and stayed that way for a while. Two things forced the issue. First, they could not actually reach every chip — the loop scrolled but there was no way to get to items past the fold, so some of the portfolio was unreachable by anything except waiting. That got fixed by pinning the tickers, scrolling the demo area beneath them, and driving the loop from scrollLeft rather than a transform, which makes the same element both the animation and the scroll container instead of fighting between the two.
Once they scrolled properly they were close enough to the ticker on the landing page that keeping two implementations was indefensible, so they moved onto the shared component. That is the sequence I would repeat: make the bespoke thing correct first, then unify. Unifying around a broken shape just spreads the bug.
The drag bugs were the same bug twice. Dragging an NFT between panes and dragging a post between queue columns both failed the same way, because a dragged item rendered inside its source container is clipped by that container the moment it leaves. The fix in both cases is a drag overlay — render the thing being dragged in a layer above the layout rather than in the tree it came from. Worth knowing once, because it is the answer every time.
And a class of bug I keep finding. Side effects had crept into state updater functions here, exactly as they had in the Learn steppers. An updater must be a pure function of previous state; React is allowed to call it twice, and anything with a consequence does not belong in it. Two features, two independent authors of the same mistake, which tells me it is a shape worth watching for rather than a one-off. The referral click tracking moved to the mutation where it belongs, and the Escape handling on the explainer dialog stopped depending on render timing.
Update — September 9, 2026
I went back through the demos the way someone landing cold would — at a smaller laptop window, clicking everything. That framing surfaced a run of small things that each made a demo read as a mock rather than the product it came from.
A chart that escaped its card was a layout bug, and my first fix for it was worse than the bug. The slug-dashboards line spilled over the config JSON on a short window. The demo root was pinned to h-full, so instead of the stage scrolling, the flex column squeezed the card until the chart overflowed it. I switched the root to min-h-full so the content could grow and the stage scroll — and the line vanished entirely. It looked fixed on my screen and broke on a smaller one, which is the exact trap I keep falling into.
The chart was not hidden. It was never rendered. recharts' ResponsiveContainer only draws once it measures a definite box, and a flex-grown height inside a min-h-full root never resolves to one, so it measured nothing and returned nothing:
{ wrapperH: 160, svgPresent: false, linePath: null }The real fix was to stop being clever about the height: a fixed h-44 on the chart wrapper, not a flex-grown one. It renders at every window size, and the greedy fills that had been stretching short demos into empty cards went with it.
The projects looked like one project. Each was the same near-black stage with an eight-percent tint, so switching jobs barely changed anything. Now the stage takes on the project's own accent as a corner glow and a texture keyed to its type — graph-paper grid for the mono/data products, a dot field for the sans ones — so the surface re-skins the moment you move between them.
The campaign manager became a Season Board. The old version was a stepped create-modal with a store inspector — honest, but it read like a form. I rebuilt it so the same campaigns show two ways: a radial dial for the shape of the year, and a run-of-show gantt you can drag to reschedule. One inspector drives both. The constraint that shaped it was the house rules, not the design: no web fonts, so the poster type is the site's own display face; and the palette sweep forbids raw neon hex on a live surface, so the goal colours are hsl — the same escape hatch the generative-art demos already use.
The last stop is this site. Every other demo is a reconstruction of past work; the new final entry is the real thing — a directory of live links to the features I built here, from the operator dashboard to the calendar to these write-ups. It is the one demo where nothing is mocked.
Adding that one entry set off a chain of guards, which is the system working. A new project and feature tripped the catalog counts, the flagship count, the prose that states the totals, and the palette sweep — which caught that my amber accent sat outside the tone band and that the styled-JSON config view had reached for stock Tailwind colours:
admin-suite.tsx:518 <span className="text-sky-600 dark:text-sky-300"> this-site.tsx:6 #e08a3c // saturation 0.73, band max is 0.68
Each one was a real thing to correct, not a test being fussy: a retuned in-band amber, and inline accent hues in place of the stock classes.
Most of the rest was making the demos honest. The wallet lookup gained a net worth, a balance sparkline and a holdings breakdown so it reads like an explorer; the campaign builder's every field now moves the preview (a channel model drives reach, installs and CPI), not just the name; the NFTs are deterministic generative art instead of flat swatches; the AI content module opens on real output rather than sitting blank; the wallet lookup opens on a sample already resolved instead of one input on an empty stage. And a genuine bug — a referral link created on the develop deploy pointed at production, because the API builds the URL against its own host, so I now rewrite it onto the current origin and, when there is no API to reach, fall back to a local preview link rather than a red network error.
Validation and affordances, everywhere they were missing. The auth and admin forms disable their action until the input is valid; the config tab renders as syntax-coloured JSON; the info icons pulse so it is obvious they open an explainer; and the recharts default tooltip is gone, replaced by one that matches the rest of the site.
Update — September 10, 2026
I went back through every demo again, this time driving each one and fixing whatever felt like a mock rather than the product. Most of the changes were interaction, not paint: the NFT transfer now asks you to confirm, waits a few seconds like a real settle, and shows a completion modal before it disappears from your inventory; a workflow node opens an action menu and an edit modal; the AI module wears its platform and personality settings on its face instead of hiding them in a dialog.
Centering the community feed, I added a wrapper div, re-ran tsc and the tests, and they passed. The dev server did not. Turbopack's parser is stricter about JSX than the type checker, and it caught a div I had balanced in the wrong order:
Build Error — Expected ',', got '{'
./src/app/work-portfolio/demos/community-mode.tsx (368:7)
> 368 | {composer && (The lesson I keep relearning: when a build tool and a type checker disagree about whether code is valid, run the build. It renders 200s or it doesn't.
A screenshot of the arcade demo showed the targets clipped at the top edge. The cause was the same min-h-full reflex that broke the slug chart: the play area collapsed instead of filling, so a target at 14% landed under the frame. A definite height on the cabinet and a wider spawn margin fixed it. The pattern is clear enough now that I should stop reaching for min-h-full without asking what it does to a flex child.
Update — September 28, 2026
This whole feature now lives in its own repo, work-portfolio-mfe, and this site loads it at runtime as a Module Federation remote. The full story is in the micro-frontends write-up; the part that belongs here is what moving it did to the demos.
Every demo was already one file and one registry line, so leaving Next.js meant changing the registry, not the demos: next/dynamic became React.lazy. Of the 23 demo files, 20 didn't change at all: two swapped next/image for a plain img, and the third is the fix below.
It deep-linked with the URL and scanned straight away, before the lazy demo arrived. Passing the slug as a prop instead finally put a demo in front of axe, and it failed on the realtime-metrics chart:
aria-label attribute cannot be used on a div with no valid role attribute. <div class="min-h-40 flex-1" aria-label="Signups per minute chart">
Fixed in the remote with role="img". The copy still in this repo keeps the bug until the flag reaches 100% and that copy is deleted.