Let's Build a VSCode-Style Interactive Portfolio from scratch in Next.js & TypeScript [Part 1]

Karan Kashyap
July 16, 2026
![Let's Build a VSCode-Style Interactive Portfolio from scratch in Next.js & TypeScript [Part 1]](/_next/image/?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2F3e1sexdu%2Fproduction%2F229694f09ce3eb856898efb7a580df4b9c1a4cf9-1600x739.png%3Frect%3D246%2C0%2C1109%2C739%26w%3D1200%26h%3D800%26q%3D85%26fit%3Dcrop%26auto%3Dformat&w=3840&q=75)
Most developer portfolios are a hero section, a grid of cards, and a
contact form. They work, but they don't say anything about the person
who built them. Mine is a working clone of VSCode — my bio is README.md,
my skills are a skills/ directory, my projects are source files you can
click open in a fake file tree. It's the one portfolio format where the UI
itself is a credibility signal: if the editor renders, syntax-highlights,
and the tabs behave correctly, you've already demonstrated the thing you're trying to prove.
Over this series I'll walk through how the whole thing is built, starting today with the least glamorous but most consequential decision: the shape of the data everything else renders from.
Every part in this series ships working code — clone the repo and follow along, or build it up file by file as we go.
Why the data model comes first
Get this wrong and every component after it fights the structure — you end
up with if (file.name.endsWith('.py')) checks scattered across the
editor, the sidebar, and the icon renderer, each one a slightly different
guess at "what kind of file is this." Get it right, and adding a new
project to your portfolio later is a five-line object literal, not a
component change.
The trade-off is that a good data model asks you to decide things up front you'd rather defer — what fields does a "file" need? What distinguishes a file from a directory? — but that's a cheap decision to make on day one and an expensive one to unwind on day thirty.
The FileNode type
Everything in the sidebar and editor — your README, your skills, every project — is one shape:
1// lib/data.ts23export type Language =4 | 'markdown'5 | 'json'6 | 'javascript'7 | 'typescript'8 | 'jsx'9 | 'tsx'10 | 'python'11 | 'go'12 | 'yaml'13 | 'bash'14 | 'sql'15 | 'graphql'16 | 'text';1718export type FileNode = {19 id: string; // must be unique across the whole tree20 name: string; // shown in the sidebar + editor tab21 type: 'file' | 'directory';22 content?: string; // required for files, rendered as-is (use \n for newlines)23 language?: Language; // drives syntax highlighting + the sidebar icon24 children?: FileNode[]; // required for directories25 hidden?: boolean; // kept in the tree, filtered out of the rendered sidebar26};
Two decisions here are worth calling out.
language instead of a separate icon field. An earlier version of
this had icon: string on every node and a getLanguageFromIcon()
function in the editor translating icon names to Prism grammar names —
two sources of truth for the same fact, and they drift the moment you add
a file type and forget one of them. One field, language, drives both the
sidebar icon (Part 2) and the Prism language (Part 3). If a file doesn't
highlight right, there's exactly one place to look.
hidden instead of deleting the node. I wanted an experience/
directory I could hide from the sidebar without losing the content — the
data should reflect what I've done, and what's currently shown is a
separate, cheaper-to-change concern. hidden: true keeps the node in
fileTree for anything that wants to read it later, while the component
that renders the sidebar (VSCodeWorkspace, in Part 4) filters it out with
a one-line .filter(). Toggle it back on by deleting one line, not by
re-typing content you already wrote.
Writing your first files
fileTree is just an array of FileNode. Here's a minimal starting point
— a README, a whoami script, and a skills directory:
1// lib/data.ts (continued)23export const fileTree: FileNode[] = [4 {5 id: 'readme.md',6 name: 'README.md',7 type: 'file',8 language: 'markdown',9 content: `# [Jane Doe](https://example.com)1011Software engineer with 6+ years shipping production web apps.`,12 },13 {14 id: 'about.sh',15 name: 'whoami.sh',16 type: 'file',17 language: 'bash',18 content: `#!/usr/bin/env bash19NAME="Jane Doe"20ROLE="Full-Stack Engineer"21echo "-> $NAME - $ROLE"`,22 },23 {24 id: 'skills',25 name: 'skills',26 type: 'directory',27 children: [28 {29 id: 'skills-frontend.ts',30 name: 'frontend.ts',31 type: 'file',32 language: 'typescript',33 content: `const frameworks = ['Next.js', 'React'];34export { frameworks };`,35 },36 ],37 },38];
Notice content is a plain template literal, not markdown-rendered HTML or
MDX — it's rendered as literal source text through a syntax highlighter in
Part 3. That's deliberate: it means any file type Prism understands (and a
few it doesn't, until we teach it one in a later part) works with zero
extra plumbing. Want a .sql file listing side projects? Add the node,
set language: 'sql', done.
Project setup
Scaffold a Next.js App Router project with TypeScript and Tailwind v4:
1npx create-next-app@latest vscode-portfolio --typescript --tailwind --app --src-dir=false2cd vscode-portfolio3npm install lucide-react prism-react-renderer
lucide-react gives us icons for the sidebar without needing a folder of
SVG assets (Part 2); prism-react-renderer is the syntax highlighter
driving the editor pane (Part 3).
Confirm the scaffold runs before moving on:
1npm run dev
If localhost:3000 shows the default Next.js starter page, the wiring is
correct and every problem from here on is ours, not the tooling's.
Next up: the sidebar — an activity bar and a file-tree accordion that
turns fileTree into something clickable.
![Let's Build a VSCode-Style Interactive Portfolio from scratch in Next.js & TypeScript [Part 3]](/_next/image/?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2F3e1sexdu%2Fproduction%2F229694f09ce3eb856898efb7a580df4b9c1a4cf9-1600x739.png%3Frect%3D61%2C0%2C1478%2C739%26w%3D800%26h%3D400%26q%3D85%26fit%3Dcrop%26auto%3Dformat&w=3840&q=75)