An interactive portfolio built to feel like a minimalist desktop operating system: a scattered desktop with real folders and files, draggable/resizable windows, a floating macOS-style dock, and light/dark themes — instead of a traditional scrolling page.
Live content lives entirely in src/data/portfolio.ts; the rest of the app just renders it.
| Layer | Choice | Why |
|---|---|---|
| Framework | Next.js 16 (App Router) + TypeScript | SEO/metadata support and fast static output, with full client interactivity where needed |
| Styling | Tailwind CSS v4 | Utility-first, fast to iterate, first-class dark mode via a custom dark variant |
| Animation | Framer Motion | Window open/close/minimize/maximize springs, dock magnification, staggered badge reveals |
| State | Zustand | Window manager (open/close/focus/z-index) and theme, without prop-drilling |
| Icons | lucide-react + react-icons (Simple Icons / Font Awesome) | Generic UI icons + official brand logos for GitHub/LinkedIn/tech stack |
| Resume viewer | Native <iframe> |
Uses the browser's built-in PDF viewer — no react-pdf/pdf.js worker configuration to fight in the App Router |
| Deployment | Vercel | Zero-config for Next.js |
npm install
npm run devOpen http://localhost:3000.
npm run build # production build
npm run start # serve the production build
npm run lint # ESLintsrc/
app/
layout.tsx → root layout, metadata, theme-init script, font setup
page.tsx → renders Desktop + WindowManager + Dock
components/
desktop/ → Wallpaper, MenuBar, Desktop shell, DesktopIcon
dock/ → Dock, DockIcon (magnification effect)
window/ → Window (drag/resize/minimize/maximize), WindowManager
apps/ → one component per window's content (About, Projects, Skills, ...)
+ registry.tsx mapping appId → component
data/
portfolio.ts → ALL content — personal info, experience, projects, skills,
education, achievements, desktop icon layout
types/
portfolio.ts → TypeScript interfaces for the data above
store/
windows.ts → open windows, positions, sizes, z-index, minimize/maximize
theme.ts → light/dark theme, persisted to localStorage
lib/
cn.ts → clsx + tailwind-merge helper
useIsMobile.ts → viewport breakpoint hook (drives mobile grid/fullscreen windows)
techIcons.tsx → tech-stack name → brand icon map
public/
images/portrait.webp → full original photo (used for the Open Graph / social preview image)
images/avatar.webp → pre-cropped headshot (About Me)
images/portrait-cutout.webp → background-removed cutout (wallpaper subject)
resume.pdf → resume, served directly and via the Resume window
Almost everything on the site is data-driven. To update it:
-
Personal info, bio, experience, projects, skills, education, achievements — edit
src/data/portfolio.ts. Each section is a typed array/object; add or remove entries and the UI updates automatically. -
Desktop icon layout —
desktopItemsin the same file controls which icons appear and their{ x, y }position (percentages of the viewport) on desktop-sized screens. Mobile ignores these and uses a 4-column grid instead. -
Notes app —
notesContent, same file. Content supports a light markdown-like syntax:# heading,## subheading,- bullet,[ ] checklist item, blank lines for spacing (rendered byTextDocument.tsx). -
Photo: replacing it takes three steps since three derived assets depend on it —
- Replace
public/images/portrait.webpwith the new photo (same filename). - Re-generate the About Me headshot crop:
Adjust the
node -e "require('sharp')('public/images/portrait.webp').extract({left:1000,top:1000,width:2000,height:2000}).resize(400,400).webp({quality:85}).toFile('public/images/avatar.webp')"extractregion to frame the new photo's face — the current numbers are tuned for the current photo. - Re-generate the wallpaper cutout (background removal). This needs the
@imgly/background-removal-nodepackage, which isn't a permanent dependency — install it temporarily:Note: on Windows, run this in its ownnpm install --no-save @imgly/background-removal-node node -e "require('@imgly/background-removal-node').removeBackground('public/images/portrait.webp').then(async (blob) => require('fs').writeFileSync('public/images/portrait-cutout.webp', Buffer.from(await blob.arrayBuffer())))" npm uninstall @imgly/background-removal-nodenode -einvocation separate from anysharpcalls in the same process — the two packages' native modules conflict if loaded together in one process (a Windows DLL-loading quirk, not a real incompatibility). Run background removal first, then a separatesharpcommand if you need to resize/re-encode the result.
The wallpaper background itself (the gradient behind the cutout) is pure CSS — see
--wallpaper-gradientinsrc/app/globals.css— so it never needs regenerating, only the cutout does. - Replace
-
Resume — replace
public/resume.pdf(same filename, no code changes needed).
| Variable | Purpose |
|---|---|
NEXT_PUBLIC_SITE_URL |
Used for metadataBase (resolves absolute Open Graph/Twitter image URLs). Defaults to http://localhost:3000 in development — set this to your real deployed URL (e.g. on Vercel, as a project environment variable) once deployed. |
- Project screenshots use category-icon placeholder headers rather than real product screenshots (none were provided) — swap in real images in
ProjectsApp.tsxif you have them. - Only projects with a confirmed GitHub URL show a "Code" link — add a
githubfield to a project inportfolio.tsto show one for it.