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
An honest, evidence-backed pass over the codebase — where the engineering is weak, where the system design doesn’t hold up, where architecture was overfit to a single feature, and where a feature could be a better experience. Every finding says why it was worth looking at, the trade-offs, and the gain from fixing it, with numbers where they exist.
Four lenses, all reproducible: an axe accessibility scan (WCAG 2.1 AA + best-practice) across every public and authenticated route; a census of lines-of-code and duplication by directory (wc -l + grep for repeated class strings and copied scaffolds); a production build for bundle weight; and a test-file census to find untested surface. The numbers below come from those, not vibes.
Headline shape: ~96,300 lines of src, 80 page routes, 46 API routes, 94 test files. That’s a big surface for a solo playground, and the distribution is the story.
Every number on this page is a census taken in July 2026 and is left at what it read then. The site has kept growing — page routes and test files are both well past these figures now — so re-running the counts would produce a different page, which is rather the point of a review being dated. Refreshing the numbers in place would quietly turn a snapshot into a claim about today that goes stale again the following week.
?version=v1|v2|v3 resolved through a VERSIONS registry in page.tsx, with retired versions behind next/dynamic so their deps stay off the default path. It’s the cleanest idea in the repo — three full landing eras coexist with zero conditional soup.portfolio_api, and the auth + header plumbing is centralised in lib/backendFetch.ts (getBackendAuth, buildHeaders) rather than copied per route. Tokens never reach the client.queryKeys factory — caching, dedupe, and background refetch are uniform, not reinvented per page.styles/tokens.css) bridged into Tailwind, and shared UI primitives used in 48 files. Theming is real, not per-component.Why look: It's the largest single thing in the repo — worth knowing what it costs.
The 35 thoughts/*Content.tsx and 14 learn/*Content.tsx files total ~35,500 lines — about 37% of all of src. Each write-up is a bespoke component: the same PageHeader + “Dev notes” eyebrow + main scaffold repeated 35 times, with individual files running 1,000–2,000 lines (the landing-page write-up alone is 1,991).
Con: Cross-cutting changes are 35× work. The a11y sweep proved it: moving the h1 into a landmark, or making the before/after code block keyboard-scrollable, is one edit in a shared layout and N edits here. Prose lives in JSX, so it’s awkward to write and diff.
Pro: Total freedom to drop an interactive demo anywhere in the narrative, which is the whole point of these pages.
Gain: An MDX pipeline (markdown prose, components for the interactive bits) plus a shared ThoughtLayout would keep the freedom while deleting the scaffold. Rough order: the repeated layout + eyebrow + code-bubble boilerplate is a few hundred lines duplicated across pages; the real win is that the content becomes editable by anyone and cross-cutting fixes go to one file.
Why look: The a11y work had to fix the same select five times — a duplication smell that bites.
Four fantasy pages carry a hand-rolled <section> filter bar with the identical styled <select> (h-9 rounded-lg border… appearance-none…) — 7 copies of the same select markup, plus a per-file selectChevron/selectStyle pair.
Con: Every one drifted slightly, and each needed its own aria-label + landmark fix in the accessibility pass. A bug or a restyle is N edits.
Gain: One <FilterBar> + <LabelledSelect> collapses 5 files of boilerplate to a handful of props and makes the next a11y/UX fix a one-file change.
Why look: It's pure, deterministic, and load-bearing for the default landing — the easiest possible thing to test, untested.
v3/graph/simulation.ts (278 lines) and graphData.ts/buildLayeredLayout are pure functions — deterministic given a seed — yet there is not a single unit test for them. Meanwhile the giant learn-content pages do have a parameterised suite.
Con: The riskiest, most reused code (collision, fit-to-viewport math, layout assignment) can regress silently.
Gain: A dozen cheap assertions (settling converges, no two nodes overlap after N ticks, the layered layout assigns every node a column) would lock the behaviour for near-zero cost, because the functions take plain data in and out.
Why look: The React Doctor pass had to fix the same stepper bug across ten learn pages — a symptom, not a one-off.
The learn steppers share a shape but not an implementation, so the play/advance off-by-one had to be fixed ten times. Same pattern with the fantasy selects (E2) and the thoughts scaffold (E1).
Gain: Extracting the stepper into one useStepPlayer hook means the next fix (or feature, like keyboard controls) is written once. Duplication isn’t just lines — it’s where bugs multiply.
Why look: A consistent data layer only helps if everything uses it.
26 client components call fetch directly. Some are legitimate (inside a Query queryFn, or an auth-proxied navigation), but a few bypass the cache/retry/dedupe that the other 76 call sites get for free.
Gain: Auditing these and routing the genuine data reads through Query gives them caching and error states with no new abstraction.
Why look: The defining structural fact of the project — worth naming honestly.
NBA fantasy, a Pokémon TCG browser, a Postgres calendar, a fleet operator dashboard, an algorithms-learning suite, a GraphQL pokédex, a work portfolio, and 3D labs all live in one Next app and share almost no domain logic — only the shell (auth, tokens, header, data layer).
Pro: For a portfolio that is exactly the goal: breadth on one deployable, one design system, one auth story.
Con: As a product it has no center of gravity; the shared surface (bundle, tokens, primitives) has to serve wildly different needs, and no single feature is deep enough to justify the others. That’s fine here — but it’s the reason “good overall system design” is the wrong yardstick. The right one is “does the shell stay thin and consistent across unrelated features,” and mostly it does.
Why look: A thin proxy layer is only trustworthy if every route fails the same way.
35 of 46 API routes have explicit try/catch and a graceful fallback; ~11 don’t, so a backend hiccup surfaces differently depending on which route you hit (a clean 502 vs an unhandled throw).
Gain: A single withBackend() wrapper (catch → typed 502, consistent logging) applied to every route makes the proxy layer uniformly resilient and deletes the copied try/catch blocks.
Why look: 46 API routes is a lot of files for a proxy.
Many routes are thin passthroughs that differ only in path and method. The auth/header plumbing is already shared (good), but the route bodies still repeat the fetch-map-return dance.
Pro: Explicit routes are easy to read and to special-case.
Con: The repetition is real; a small typed proxy helper would remove it without hiding the routes.
Why look: The user asked directly whether we overfit architecture to a feature — this is the clearest case.
v3 is 1,503 lines of bespoke code: a force-directed simulation (repulsion, springs, gravity, collision, label-aware spacing), a fit-to-viewport renderer, a second flat layout engine, a mobile fallback, and GSAP — all for the landing page.
Pro: No heavy graph dependency, full control over feel, a genuinely distinctive result, and the physics is self-contained and (could be) testable.
Con: It’s a lot of surface for a page most visitors skim once. A library (react-flow, d3-force) or a simpler animated static layout would land ~80% of the effect for a fraction of the code and maintenance. The flat view + mobile path roughly double the footprint for a fallback.
Gain: Not necessarily “rip it out” — it’s a showpiece and it’s isolated. But it’s the honest answer to the overfit question: yes, the landing carries product-grade engineering, and the guardrail is to keep it walled off (which it is) and tested (which it isn’t, see E3).
Why look: E1 measured the cost; here's the design decision under it.
Treating every write-up and lesson as a hand-built React component — rather than content fed through a pipeline — is an architectural stance. It optimises for “drop any interactive demo mid-sentence” at the cost of every other content operation (authoring, diffing, translating, shared layout, bulk fixes).
Gain: MDX (or a headless CMS for the pure prose) keeps the interactive escape hatch while making the other 95% of each page cheap. This is the highest-leverage refactor in the repo by line count.
Why look: Seven dedicated hooks for one showcase feature is worth a second look.
Operator has 7 bespoke hooks (useOperatorStore/Stores/Alerts/Inventory/Mutations/Activity) and its own component folder — genuinely well-architected, arguably the best-engineered feature. That is also the point: it carries production-app depth for a demo.
Pro: It’s a strong portfolio proof-point and the pattern is clean.
Con: The investment is disproportionate to a feature nobody depends on; if effort is the scarce resource, that depth is “spent” where E1/E3 would have paid back more.
Each feature works; this is the single most valuable improvement for each, not a teardown.
v3 landing (graph)
Strength: Distinctive and now accessible.
Biggest gain: Discoverability — a first-time visitor may not grok that nodes are the nav. A one-time hint or a subtle auto-nudge of the graph on load would teach the interaction. Mobile force view is dense; it could adopt the flat list the way flat mode already does.
Work portfolio
Strength: Rich, real interactive reconstructions.
Biggest gain: The explainer dialog doesn't reliably close on Escape (a keyboard-scope timing issue caught by e2e). It's a real keyboard-usability bug — worth fixing at the scope source, not per dialog.
Fantasy (NBA)
Strength: Deep data, nice win bars and predictions.
Biggest gain: The filter bars look slightly different per page (E2) and the team colours only just became legible in light mode. Unifying the filter component would make the sub-pages feel like one feature.
Learn
Strength: 14 genuinely interactive lessons — a highlight.
Biggest gain: The steppers had a shared bug (E4) and no keyboard control. A shared stepper with arrow-key support would make every lesson better at once.
TCG browser
Strength: Infinite scroll + URL-synced filters, well done.
Biggest gain: Filter state and scroll depth restore, but deep-linking a specific card back into the exact grid position isn't seamless. Minor.
Calendar
Strength: Full CRUD, four views, timezone-aware.
Biggest gain: Loading/empty states are where its contrast issues hid (only visible with no backend) — worth a deliberate empty-state design so it degrades gracefully offline.
Operator
Strength: The deepest feature; live-ish fleet data.
Biggest gain: Heading order needed a fix (h1→h3 skip); the information density is high with little onboarding. A one-line 'what am I looking at' would help a cold visitor.
Thoughts
Strength: The soul of the project.
Biggest gain: Inconsistent reading UX — some have an iMessage 'chat' toggle, most don't. Pick one reading model and apply it via the shared layout (E1).
withBackend() wrapper for API routes (S2/S3). Uniform failure + less repetition across 46 routes.The accessibility work that preceded this review is the one place with before/after numbers:
| Metric | Before | After |
|---|---|---|
| Public routes scanned in CI | 2 (/ and /tcg) | 14 + landmarks + auth (3) |
| Axe bar | WCAG 2.1 AA | AA + best-practice |
| Colour-contrast violations | ~120 across 6 routes | 0 |
| Missing-landmark routes | ~10 | 0 |
| Whole scanned surface | unknown / unenforced | clean, enforced |
The rest of this review is a map, not a receipt — the gains in E1–A3 are estimated from line counts and duplication factors, and become real numbers only once the refactors land. That’s the honest state: the accessibility bar is measured and done; the structural debt is identified, quantified where possible, and prioritised.