A calm, koi-pond-themed personal portfolio for Yun-Chen (Donkey) Lee. Built with Next.js 15 App Router, Tailwind CSS v4, Framer Motion, and a small client-only React Three Fiber koi-pond hero.
The redesign is intentionally SSR-first: only the things that have to be client (the koi pond canvas, scroll-reveals, the season switcher, the PDF viewer) are client components. Everything else — About, Experience, Projects, nav skeleton, footer, metadata — is rendered on the server for great SEO and fast initial paint.
src/
├── app/
│ ├── layout.tsx # Root layout: fonts, season-boot script, providers, analytics
│ ├── page.tsx # SSR home (Hero is the only client island)
│ ├── globals.css # Tailwind v4 + season palette CSS variables
│ └── resume/page.tsx # SSR résumé page; viewer isolated in a client wrapper
│
├── components/
│ ├── animations/ # Reveal, YinYangKoi (small client islands)
│ ├── icons/ # In-house SVG icon set (currentColor)
│ ├── nav/ # SiteNav, SiteFooter, SeasonSwitcher
│ ├── pond/ # KoiPondHero + KoiPondScene (R3F) + HeroFallback (SVG)
│ ├── resume/ # ClientResume + ResumeViewer (react-pdf)
│ ├── sections/ # About, Experience, Projects, ProjectCard (server)
│ └── track/ # Vercel Analytics wiring
│
├── lib/
│ ├── season/ # Season type, palette tokens, SSR-safe boot script
│ └── site/ # Profile / experience / projects content
│
├── providers/season/ # React context for the season palette
├── utils/ # cn() helper, shared TS types
└── middleware.ts # Geo headers for tracked routes
tests/unit/ # Vitest + React Testing Library component tests
e2e/ # Playwright user-flow tests
| Concern | Choice | Why |
|---|---|---|
| Framework | Next.js 15.5 App Router | RSC-first, fast image / font primitives, official React 19 support |
| React | React 19 | Required by @react-three/fiber@9 — older R3F v8 ships react-reconciler@0.27 which trips ReactCurrentOwner undefined on React 18.3+ |
| Styling | Tailwind v4 + CSS variables | Replaced styled-components — better SSR, no FOUC, smaller runtime |
| UI primitives | Custom Tailwind components | Replaced Ant Design — heavy for a 3-section portfolio |
| 3D | @react-three/fiber 9 + three 0.184 |
Compatible with React 19 reconciler |
| Animation | Framer Motion | Scroll reveals + scroll-linked yin-yang transition |
| Tests | Vitest + RTL, Playwright | Unit + e2e smoke coverage for key flows |
| Analytics | @vercel/analytics |
Same as before, wired through middleware geo headers |
The season palette is implemented as four CSS variable sets selected by
<html data-season="…">. A small beforeInteractive script reads the user's
saved season from localStorage and sets the attribute before paint, so we
never get a palette flash on first load.
The koi pond hero is dynamic(() => …, { ssr: false }). It is only
mounted when:
prefers-reduced-motionis off- viewport ≥ 768px
- WebGL is available
- the browser is idle (uses
requestIdleCallbackwhen available)
Otherwise the page falls back to a JS-free SVG version of the same scene.
pnpm install
pnpm dev # http://localhost:3000pnpm build
pnpm startpnpm test # Vitest unit tests (jsdom)
pnpm test:watch # Vitest in watch mode
pnpm test:e2e:install # one-time: install Playwright browsers
pnpm test:e2e # Playwright (auto-starts dev server on :3000)E2E tests cover:
- Hero renders, "Wander in" CTA scrolls to
#about, all 3 project cards show - Season switcher persists the chosen palette across reloads
/resumepage renders with a download button
Unit tests cover:
<SeasonSwitcher />— radios, persistence, html attribute<ProjectCard />— content, featured badge, conditional links<About />— bio, education, contact links, downloadable résumé
pnpm lint # next lint . --ext ts,tsx --max-warnings 0 --fixAll editable content (bio, summary, education, experience entries,
project descriptions) lives in src/lib/site/profile.ts.
Edit there — the home page rebuilds itself.
Deployed on Vercel. The vercel.json and .vercel/ folders are tracked.
Ensure environment variables, if any, are configured in the Vercel dashboard.