Personal portfolio site with a 2D camera navigation system — the entire home page is a pannable, zoomable canvas of cards instead of a traditional scrolling page.
- Next.js 15 (App Router, Turbopack)
- React 19
- TypeScript
- Tailwind CSS v4 (PostCSS)
- GSAP — animations (modals, galleries, hovers, text reveals)
- Biome — linting and formatting
- 2D camera navigation — pan and zoom across a virtual 4368x3318px canvas
- Multi-input controls — scroll, keyboard, drag, and touch
- Infinite-scroll illusion — 4 translated grid copies create seamless wrapping
- Data-driven cards — all content defined as typed entries (
shot,contact,map,cv,profile,gallery,technologies) - Responsive scaling — CSS breakpoints from mobile to 5K+ displays
- Static generation — project detail pages pre-rendered from shot entries
Prerequisites: Node.js 18+ and Yarn
yarn install
yarn dev # Dev server with Turbopack at http://localhost:3000Production build:
yarn build
yarn startsrc/
app/ # Next.js App Router pages and layouts
(home)/ # Home page — camera-based portfolio grid
[slug]/ # Individual project detail pages
cv/ # CV page
api/ # API routes (projects JSON endpoint)
components/ # Shared UI components
config/ # Site configuration (layout dimensions, contacts, etc.)
db/ # Content database
entries/ # Individual entry files (one per card)
types.ts # Entry type definitions
index.ts # Combined entries array and helpers
hooks/ # Custom React hooks
providers/ # React context providers
CameraProvider/ # Camera system (state, controls, viewport, grid)
utils/ # Utility functions (cn, etc.)
CameraProvider (src/providers/CameraProvider/) is the core abstraction. It manages camera position, viewport CSS transforms, and 4 grid transforms for the infinite-scroll illusion.
Input is handled by dedicated control modules: ScrollControls, KeyboardControls, DragControls, and ToucheControls. The Viewport component applies the CSS transform, and Grid renders 4 translated copies of the card layout.
Content is defined in src/db/entries/ as individual TypeScript files following types from src/db/types.ts. The home page maps entries to card components via a switch on entry.variant. Cards are positioned on the CSS grid using each entry's area field.
| Route | Description |
|---|---|
/ |
Interactive camera-based portfolio grid |
/[slug] |
Project detail pages (statically generated) |
/cv |
CV page |
/api/projects |
JSON endpoint returning all entries |
| Command | Description |
|---|---|
yarn dev |
Dev server with Turbopack |
yarn build |
Production build |
yarn start |
Start production server |
yarn format |
Format with Biome |
yarn lint |
Lint with Biome |
yarn check |
Full Biome check (lint + format + assist) |
yarn ts:check |
TypeScript type checking |
yarn gen:imports |
Regenerate entry imports from db/entries/ |
yarn db:validate |
Validate database entry slugs |
To add a new portfolio entry:
- Create an entry file in
src/db/entries/following theEntryShottype - Run
yarn gen:importsto regenerate the imports - Run
yarn db:validateto verify slugs
- Biome — single quotes, no semicolons, 2-space indent, 120-char line width
- Tailwind CSS v4 with custom
@utilitydefinitions inapp.css - Path alias:
@/*maps tosrc/* - Object keys auto-sorted (
useSortedKeys) - Class names merged with
cn()utility (clsx + tailwind-merge)
