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 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) 💡 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 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 } 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 🔒 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) - 🔐 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
useSyncExternalStorewith 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.
![Path Planning and Autonomous Navigation — Ch.25: Future of Autonomous Navigation [KR]](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjs5djnow7ktieu1tmsv0.png)

