My personal site. Built with Next.js 14 (App Router) and exported as static HTML
to GitHub Pages on the nabinpdev.com custom domain.
This started as a single 3,300-line index.html file (still preserved under
legacy/ for reference), then went through a
Vite + React refactor, then a move to Next.js. Each step is in the commit log
and was driven by something specific I wanted — see
docs/decisions.md for the reasoning.
A multi-section portfolio: hero, about, skills, ML lifecycle, experience,
projects, research, education, photos, contact. Three of the sections involve
real interactive <canvas> work — they're the part I had the most fun with
and also the part that taught me the most about scheduling animation frames.
HeroPhoto— pixel-scatter portrait. Decodes the source JPEG once into a particle grid, then runs a per-particle force field around the cursor. GRID size controls particle density vs CPU (set to 4 on the live site — seedocs/decisions.md).HeroDots— mouse-reactive constellation behind the hero text. Computes pairwise distance for nearby dots only.SkillsBubbleCanvas— soft-body bubble physics with O(n²) pair collisions. 16 bubbles, manageable.
All three IntersectionObserve their own canvas so the animation loop pauses when scrolled off-screen.
| Layer | Pick | Why |
|---|---|---|
| Framework | Next.js 14, App Router | File-based routing per section, easy static export |
| Build mode | output: 'export' |
Pages is free, my content is static — no reason for SSR |
| UI | React 18 | Already had components from the Vite version, no rewrite |
| Styling | Plain CSS in app/globals.css |
One stylesheet, design system as :root custom props |
| Bootstrap | react-bootstrap |
Reserved for grid helpers, not actively driving the look |
| Fonts | next/font/google (Space Grotesk + Inter) |
Self-hosted at build time, no FOIT |
| Hosting | GitHub Pages | Free, fast enough, custom domain support |
| DNS | Cloudflare | Email forwarder lives here too (hi@nabinpdev.com) |
| CI | GitHub Actions | One workflow file, builds and deploys on push to main |
framer-motion was in for a while and got pulled — see
docs/decisions.md.
app/ Next App Router
layout.jsx root layout; mounts <FullPage> once
globals.css design system + section styles (~1500 lines, one file on purpose)
page.jsx / route stub — content rendered by FullPage in layout
about/ /about route stub
skills/, ... one stub per top-level section
components/ React components
FullPage.jsx mounts every section + handles URL <-> scroll sync
Hero*, Skills* the canvas-driven pieces
About/Workflow/ structured content sections
Experience.jsx has internal tab state for engineering vs ops timelines
Navbar.jsx listens for `np:routechange` events from FullPage
data/
content.js all copy, links, KPIs, project metadata
images.js base64 photos (legacy from the single-file days; on the
todo list to move into public/ for proper caching)
hooks/
useReveal.js intersection-observer driven section reveal
useMediaQuery.js thin matchMedia wrapper
public/ static assets, favicons, CNAME
legacy/ the original single-file site, archived
docs/ decisions, architecture, known issues — read these
.github/workflows/ CI
Node ≥ 18.
git clone https://github.com/nabindev3/nabinwebsite.git
cd nabinwebsite
npm install
npm run dev # localhost:3000Static build (what CI produces):
npm run build # writes ./out
npx serve out # serve at localhost:3000 to sanity-checkAlmost everything routes through data/content.js. Change a
KPI number, swap a project link, reorder skills — components re-read on the
next render. The JSX rarely needs touching unless I'm changing layout.
Push to main → .github/workflows/deploy.yml runs.
actions/checkout@v5actions/setup-node@v5(Node 24)npm install(no lockfile cache — seedocs/decisions.md)npm run build→./outtouch out/.nojekyllactions/upload-pages-artifact@v5+actions/deploy-pages@v5
public/CNAME is copied into out/ on every build, so the custom domain
sticks across deploys (had to learn this the hard way — see
docs/known-issues.md).
The portfolio links out to the actual code for the projects it talks about:
- FM Difficulty Probe — one pipeline tested on Chronos-T5 (TSFM) and
Pythia (LLM). Two foundation-model families, one predictive null and one
causal signal.
fm-difficulty-probe - Cascade Engine — multi-tier LLM router using a cheap gatekeeper
- Thompson Sampling for cost-quality routing.
Cascade-Engine
- Thompson Sampling for cost-quality routing.
- Hotel Revenue ML Platform — XGBoost cancellation classifier + N-BEATS /
Prophet for forecasting. Walk-forward TS-Split, target-leakage features
audited out before training.
Smart-Hotel-Analytics-· live demo
hi@nabinpdev.com — forwarded to my personal inbox via Cloudflare Email
Routing. Also: LinkedIn,
GitHub.
Documentation lives in docs/:
architecture.md— how the URL/scroll sync works and why I didn't use a library for itdecisions.md— design-decision logknown-issues.md— what's rough and what's next
License: not formally licensed yet. If you want to reuse a chunk, ask first.