Luxury dark portfolio: Gravity-style hero, Linear/Vercel/Raycast UI, Lusion-style 3D objects and an Igloo-style continuous 3D world. Concept: code becomes product: keycaps assemble into each project's object as you scroll.
bun install
bun run dev # http://localhost:3000
bun run check # oxlint + oxfmt check + typecheck + build
bun run format # oxfmtCommits follow Conventional Commits (feat: …,
fix(scope): …), enforced by commitlint via a husky commit-msg hook and in CI.
src/ all source code (`@/*` → `src/*`)
app/ routes: /, /for/[audience], /projects/[slug], /blog, /skills, sitemap, robots, OG image
content/ typed content, one file per domain (profile, audiences, projects, skills…)
features/ one folder per feature
audience/ hire · freelance · learn switch, client store, section ordering
chrome/ header, bottom bar, ⌘K palette, smooth scroll
hero/ portrait, pitch, scroll effects
sections/ landing sections (impact, projects, experience, services, skills, writing…)
skills-map/ interactive star map
three/ the WebGL stage, physics hero, keycap morph, project objects, keycap floor
ui/ shared primitives (Dialog, GlowCard, Keycap, SectionHeading…)
lib/ motion tokens, JSON-LD, runtime state, helpers
- Audiences:
/?for=hire|build|followis rewritten (next.config.ts) to the prerendered/for/[audience], so every variant is static HTML. On the client, switching (click or keys 1–3) updates a small store (src/features/audience/audience-store.ts) and the URL;SectionStackreorders the server-rendered sections. Pitch, CTA and order live insrc/content/audiences.ts. - One persistent WebGL canvas (
src/features/three/stage/Stage.tsx) sits fixed behind the page. HTML stays the source of content (SEO, a11y); 3D objects follow empty slots in the HTML (id="obj-…", seesrc/features/three/stage/anchor.ts). - Hero (
src/features/hero/,src/features/three/hero-physics/): name behind the portrait, throwable keycaps with Rapier physics colliding with the silhouette. The first scroll flies through them. - Keycap morph (
src/features/three/morph/): instanced keycaps fly onto the target object's surface, then a scanline turns the wireframe into the solid object (src/features/three/objects/). - Reveals use CSS scroll-driven animations (
[data-reveal]insrc/app/globals.css); everything honoursprefers-reduced-motion.
Design tokens live in src/app/globals.css (@theme), motion tokens in src/lib/motion.ts. Design intent and rules: DESIGN.md.
| File | What | Then |
|---|---|---|
portrait-cutout.png |
AI-enhanced B&W portrait, transparent background, 4:5, ≥ 2000 px tall | set profile.portrait in src/content/profile.ts, adjust src/lib/portrait.ts so colliders hug the outline |
thibault-friedrich-cv.pdf |
CV | — |
projects/<slug>/… |
screen recordings / mockups per case study | replace the placeholder block in src/app/projects/[slug]/page.tsx |
models/*.glb (optional) |
real models to replace the procedural ones | load with drei useGLTF, compress with gltf.report |
Content placeholders are marked TODO in src/content/ (email, booking link, channel URLs, La Loupe
and Design System Hub numbers, talk links, the full article list).
- Articles are a hand-written list; the Medium RSS / Dev.to API import is next.
- Contact opens the mail client (
mailto:); no backend.