All posts

Rebuilding the Portfolio for Mobile — A Week of Decisions

Engineering Design Meta Mobile
Mubashir Rehman

Software Engineer at TransData · co-author, peer-reviewed ECG/ML research

I shipped this portfolio a week ago. Then I spent the rest of the week rebuilding half of it.

Not because it was broken — it worked fine on desktop. But loading it on my phone felt like viewing a document, not using an app. The navbar was too small to tap comfortably. The habit heatmap was 5px squares. The contact form had four input fields asking for information a mobile visitor would never bother typing.

So I rebuilt it. Here is what I learned.

The architecture decision

The first real decision was how to handle mobile layouts. The options were:

  • CSS-only breakpoints — fast, but JSX gets messy when two layouts live in the same file
  • A useMobile() hook with conditional rendering — middle ground
  • Page-level split — src/pages/mobile/ alongside src/pages/ — full separation

I chose the page-level split. The reasoning was simple: if you want to change the mobile About page drastically six months from now, you should not have to read the desktop About page to do it. Separation of concerns is not just for backend systems.

The cost is real — any new page gets written twice. But the data stays shared (src/data/*.json), so content updates happen once. The presentation diverges, the source of truth does not.

Material You without a library

I wanted M3 — Material Design 3 — for the mobile experience. Cards with 28px border radius, tonal surface containers, the pill-shaped navigation indicator, emphasized easing curves. The native feel that Google apps have.

The tempting path was pulling in @mui/material or @material/web. I chose neither. MUI conflicts with Tailwind. @material/web adds complexity for web components in a React tree.

Instead I extended the CSS variable system with M3 surface container tokens:

--m3-surface-lowest:        270 24% 99%;
--m3-surface-low:           270 20% 96%;
--m3-surface:               270 20% 97%;
--m3-surface-high:          270 16% 92%;
--m3-surface-highest:       270 14% 88%;
--m3-primary-container:     265 80% 90%;
--m3-on-primary-container:  265 85% 20%;

Seven tokens per theme — light, dark, sakura. All three themes get M3 surface elevation for free. No library, no bundle cost, full control.

The pages that needed the most rethinking

Habits — the GitHub-style heatmap is 52 columns × 7 rows. On a 375px screen each cell is 5px. Unusable. I replaced it with streak cards and a 30-day dot grid filtered by month. The radar chart stays — it is SVG and scales fine. Year and month filter pills at the top let you navigate without scrolling through 52 weeks of tiny squares.

Contact — the desktop has a four-field form above the fold. On mobile, nobody fills out a form standing on the street. The primary actions became full-width cards that open WhatsApp or Email in one tap. The form collapsed into a secondary expansion panel — visible for visitors who want it, out of the way for everyone else.

Projects — I added a bottom sheet. Tap a card, a sheet slides up from the bottom with the full description, all metrics, all tags, and the external links. The card stays visible behind the scrim. Dismiss by swiping down or tapping the overlay. No new page navigation, no back button to manage, context preserved.

What I cut

  • The footer — entirely removed on mobile. The bottom nav is the footer.
  • The navbar — removed on all mobile pages. Content starts at the top of the screen.
  • Metrics on the About page — four numbers were hardcoded in JSX, not from the JSON, and one was incorrect. The experience section makes the same point with more context.
  • The hamburger menu — replaced by the bottom nav. Seven pages in a dropdown felt like a compromise. Four tabs in a persistent bar is a decision.

The AI assistant

I added a chatbot that answers questions about my work — skills, projects, experience. It runs on Groq's free API with llama-3.3-70b. The entire resume and project list is injected as a system prompt. No embeddings, no vector database — context stuffing into a 70B model with a large context window.

It is not a product. It is a useful shortcut for a recruiter who wants to ask "has he worked with Django?" without reading the full About page. The answer is grounded in actual data. The model is instructed to deflect off-topic questions and admit when something is not in the context.

What is still rough

The stale chunk crash — after a deploy, visitors with cached HTML hit "Failed to fetch dynamically imported module". I have two guards for it now, but the root cause is SSG itself. I am considering removing it entirely. The SEO benefit for a personal portfolio does not justify the operational complexity.

Four of six habits have zero entries. The tracker looks sparse. Systems beat motivation — but only if you actually use the system.

The journal had two entries. This is the third.


Ship it. Measure it. Iterate. Still on version 1 — just a different version 1 than last week.