DEV Community

Cover image for I Turned My Portfolio Into a Desktop You Can Actually Use ๐Ÿ–ฅ๏ธ
Shubham Chaudhary
Shubham Chaudhary

Posted on Originally published at shubham-works.hashnode.dev

I Turned My Portfolio Into a Desktop You Can Actually Use ๐Ÿ–ฅ๏ธ

Originally published on https://shubham-works.hashnode.dev.

Drag windows. Open a Terminal. Travel back to 1995. Beat a global high score. It's still my portfolio. You just get to play with it.

๐Ÿ”— Try it live โ†’ shubhamworks.vercel.app


โšก TL;DR

  • A portfolio built as an interactive desktop on computers and a Home Screen on phones
  • Next.js App Router + TypeScript (strict) + plain CSS. No Tailwind, no UI library
  • A real genie minimize effect that I like the most
  • A global top score you can't fake by sending { "score": 999999 }
  • A like button with a shared count that's hard to game
  • Lighthouse accessibility: 100 on desktop and phone

๐Ÿค” Why not just another scrolling page?

Most developer portfolios follow the same script:

Hero  โ†’  About  โ†’  Projects  โ†’  Contact
Enter fullscreen mode Exit fullscreen mode

Mine did too. Then I asked myself one question:

What would make someone actually spend two minutes on it?

The answer: let them use it.

๐Ÿ’ป On a computer ๐Ÿ“ฑ On a phone
A desktop with a Dock A Home Screen with app icons
Windows you can drag, resize, minimize and zoom A tab bar
A menu bar and a few apps Bottom sheets

Underneath, it's still my portfolio: about, experience, projects, skills and contact, all in one window.

I'm a Full Stack Engineer with 4 years of React, Next.js and Node.js. This project was a good excuse to go deep on the parts of frontend I enjoy most: rendering, animation and the small details that make an interface feel right.


๐Ÿงฉ What's inside

๐Ÿ—‚๏ธ The Portfolio Window

One scrolling window with a sidebar (Home, About, Projects, Skills, Contact). It's not a separate window per section. I tried to keep it a portfolio first.

โŒจ๏ธ Terminal

help ยท whoami ยท skills ยท projects ยท resume ยท links... and three hidden commands. help tells you they exist. hint gives you clues, never the answer. ๐Ÿ•ต๏ธ

๐ŸŒŒ Time Machine

My life as a stack of "desktop snapshots" from 1995 to today, in a 3D starfield. Scroll back in time and the stars warp into streaks while you travel. Psst... try going back before 1995.

๐Ÿ“Š Activity Monitor

My skills as running processes, with a live load graph and a "Used in" view that lists the jobs and projects where each skill shows up.

๐Ÿž Debug Runner

A small runner game:

  • ๐Ÿฆ— Hop the bugs
  • ๐Ÿง  Long-jump the memory leaks
  • ๐Ÿšซ Stay under the 404s
  • โ˜• Grab coffee for "hotfix mode"

It has a top score shared by every visitor. ๐Ÿ†

โš™๏ธ System Settings

Light / Dark / Auto, 8 gradient wallpapers I made, and Larger Text.

๐Ÿ“„ A Resume "Installer"

A four-step wizard (with a license agreement joke ๐Ÿ˜„) that ends in the PDF download.

โค๏ธ A Like Button

A heart with a like count shared by every visitor: in the status bar on computers, in the title bar on phones.

  • ๐Ÿ˜Š Like it and a thank-you notification pops up with a random happy emoji
  • ๐Ÿ’” Change your mind? Click again to take it back, and a sad face stands in for the heart for a moment

๐Ÿ› ๏ธ How it's built

The stack

Layer Choice
Framework Next.js (App Router)
Language TypeScript, strict mode
Styling Plain CSS: no Tailwind, no CSS-in-JS, no UI library
Runtime deps Only Next, React and Vercel's analytics packages
Backend storage Upstash Redis via Next.js route handlers
Testing Playwright

1๏ธโƒฃ Server Components for content, one client component for the desktop

A desktop UI is very interactive, so it's tempting to make everything a client component. But then the content only shows up after JavaScript runs. That's bad for search engines and for slow phones.

So I split it up:

Server  โ”€โ”€โ–บ  Every portfolio section is a React Server Component
              (all the text is in the server HTML)
                        โ”‚
                        โ–ผ  passed as props
Client  โ”€โ”€โ–บ  One client root that runs the desktop
              (decides which sections a tab shows, and in what order)
Enter fullscreen mode Exit fullscreen mode

๐Ÿ’ก The server owns the content. The client owns the behavior.


2๏ธโƒฃ A real genie effect, without killing the frame rate

Minimizing a window into the Dock with a "genie" effect was the hardest part of the whole project.

How it works: the window is copied into 10โ€“16 horizontal strips. On every frame, each strip gets a matrix3d transform that maps its rectangle onto its slice of a curved funnel leading into the Dock icon.

My first version used clip-path and kept the sidebar's background blur on the copies. It lagged badly. ๐ŸŒ

Here's what fixed it:

  • ๐ŸŽฏ Transform only. Nothing changes size or repaints per frame, so the GPU just moves layers.
  • ๐Ÿ”ฎ Prepare before the click. When the mouse rests on the window buttons for ~80ms, the copies are already built (invisibly), so the click only has to start the motion.
  • โœ‚๏ธ Do less on click. The rest of the desktop (Dock, menu bar) hears about the minimize when the animation lands, not when it starts.
  • ๐Ÿ› One surprising CSS bug. A selector that ended in a plain tag name (span) made Chrome restyle every span on the page, including ~870 elements in the copies, on every animation. Switching it to a class fixed it.

Zooming a window had a similar story: Chrome was redrawing all the text in the window on every frame. Giving the content its own GPU layers (will-change: transform) during the glide fixed it.

๐Ÿ“‰ The numbers (measured on my laptop)

What Before After
Pause between click and minimize motion ~58ms ~9ms โœ…
Drawing time per zoom (integrated graphics) ~300ms ~90ms โœ…

โ™ฟ All of this is skipped under prefers-reduced-motion.


3๏ธโƒฃ A game loop that runs the same on every screen

My first version of the game moved things a fixed amount per frame. On a 120Hz screen, it ran twice as fast. ๐Ÿ˜…

The fix:

position += speed ร— timeSinceLastFrame
Enter fullscreen mode Exit fullscreen mode

Everything now moves per second, so it plays the same at 60Hz and 120Hz.


4๏ธโƒฃ A global top score that's hard to fake

The site has a tiny backend: a like counter and the game's top score, both in Upstash Redis through Next.js route handlers (plain fetch to Upstash's REST API, no client library).

A shared high score is an open invitation to send this:

{ "score": 999999 }
Enter fullscreen mode Exit fullscreen mode

So instead of trusting the browser:

 1. Run starts   โ”€โ”€โ–บ  Server hands out a TICKET
                      { start time + random id }, signed with HMAC
                      (no database write)

 2. Game over    โ”€โ”€โ–บ  Server checks:
                      โœ” the signature is valid
                      โœ” score โ‰ค what the elapsed time allows
                        (the game earns exactly 10 points/second, plus a little slack)

 3. Submitted    โ”€โ”€โ–บ  Each ticket counts once
Enter fullscreen mode Exit fullscreen mode

๐Ÿ”’ Faking a top score now takes as long as playing for it.

And the player never waits: the game shows the new record the moment you break it and saves it in the background. If the server says no, the old score comes back.


5๏ธโƒฃ A like counter that's hard to abuse

The same tiny backend counts likes. A like button sounds trivial until you ask: what stops someone from liking 1,000 times, or "unliking" everyone else's likes?

 Like     โ”€โ”€โ–บ  Server adds one and hands back a random id
              (the browser keeps it, so it knows it liked)

 Unlike   โ”€โ”€โ–บ  Browser sends that id back
              โœ” the count only drops if the server handed out
                that id and it wasn't taken back yet

 Spam     โ”€โ”€โ–บ  Each network gets up to 5 likes a day
              (offices and mobile networks share one IP, so it isn't 1)
Enter fullscreen mode Exit fullscreen mode
  • ๐Ÿ” The IP is stored only as a keyed hash that expires after a day, never the raw address
  • โšก The heart reacts instantly and the count settles on the server's total; if a request fails, the change is undone

๐Ÿ™… Made-up or repeated requests can't remove anyone else's like.


6๏ธโƒฃ No flash of the wrong theme

  • Saved settings (theme, wallpaper, larger text) are read through useSyncExternalStore with a server snapshot, so there are no hydration mismatches.
  • A tiny inline script applies the saved theme before the first paint, so dark mode users never see a white flash. ๐ŸŒ™

7๏ธโƒฃ Accessible, even though it looks like a desktop

  • โŒจ๏ธ Menus, tabs, dialogs and window buttons all work from the keyboard
  • ๐Ÿ” Focus returns where it came from
  • ๐ŸŽจ Colors pass WCAG AA (the classic system blue fails for small text, so I used a darker one)

๐Ÿ… Lighthouse accessibility: 100 on desktop and phone.


8๏ธโƒฃ Tested in a real browser

Playwright tests run at desktop and phone sizes and cover:

startup screen ยท windows ยท Terminal ยท Time Machine ยท contact form ยท likes ยท game top score ยท phone layout

The tests point the database at a dead address and stub the APIs, so they can never change the real like count.


๐Ÿค– Built with AI

I built this with AI coding assistants, and I'm not going to pretend otherwise. An animation like the genie effect gives it away.

AI helped write much of the code. My part was the product:

  • ๐Ÿงญ Deciding what the site should be
  • ๐Ÿ“ฑ Testing every change on my laptop and phone
  • ๐Ÿ—ฃ๏ธ Reporting what felt off
  • โœ‚๏ธ Cutting whatever didn't earn its place

It's still a lot of judgment calls, and every one of them is mine.


๐Ÿ“š What I learned

1. Measure before you optimize. The slow parts weren't where they looked. The sidebar blur and the shadow seemed like obvious suspects, but they weren't the cost. Chrome's performance panel showed layout on click and text redraws.

2. Server Components and rich interactivity mix well ...when the server owns the content and the client owns the behavior.

3. Never trust the client, but don't make it wait either. Show the result at once, let the server decide, and undo gracefully.

4. Cut things. I built and removed an achievement system, a music player and a startup chime. The site is better without them.


๐Ÿš€ Try it

๐Ÿ‘‰ shubhamworks.vercel.app

  • โŒจ๏ธ Open the Terminal and type help
  • ๐ŸŒŒ Go back to 1995 in the Time Machine
  • ๐Ÿ† Try to beat the top score in Debug Runner
  • โค๏ธ And if you enjoyed it, leave a like: it's the heart at the bottom of the window (top right on phones)

I'd love to hear what you think, and what you'd add or remove. Drop it in the comments ๐Ÿ‘‡


๐Ÿ’ผ P.S. If you're hiring: I'm open to Full Stack / Frontend roles. Reach me through the Contact section on the site or on LinkedIn.

Top comments (0)