The personal portfolio of Mahnoor Zaffar, Full-Stack Web Developer & AI Engineer. A fast, accessible, and fully responsive single-page application.
Live site: www.mahnoorzaffar.dev
v2 — rebuilt from source. The previous deployment shipped only a minified, third-party build artifact with no maintainable source. This release is a ground-up reconstruction: a typed, modular, and owned codebase.
- Overview
- Tech Stack
- Architecture & Principles
- Getting Started
- Available Scripts
- Project Structure
- Editing Content
- Performance & Accessibility
- Deployment
- Roadmap
This portfolio presents a hero introduction, an about section, the services offered, a categorized skill set, and contact details. It is engineered as a production-grade frontend with an emphasis on maintainability, performance, and inclusive design rather than visual gimmicks.
Highlights
- Strict TypeScript across the entire codebase.
- Centralized design tokens — colors, spacing, breakpoints, and a fluid type scale defined in exactly one place.
- Content fully decoupled from presentation: all copy lives in a single typed module, so updates never require touching component code.
- Animations implemented as progressive enhancement and disabled automatically for users who prefer reduced motion.
| Concern | Choice |
|---|---|
| Build tool | Vite 6 |
| UI library | React 18 |
| Language | TypeScript (strict mode) |
| Styling | Tailwind CSS 3 with a token-driven tailwind.config.ts |
| Design system | Ollama-inspired language documented in DESIGN.md |
| Animation | anime.js v4 (animate, stagger, timelines) |
| Theming | Class-based light/dark with no-FOUC inline script |
| Quality gates | ESLint (flat config, zero-warning policy) + Prettier |
The UI follows an Ollama-inspired, documentation-first design language captured
in DESIGN.md: a flat paper canvas, ink + neutral grays, pill
geometry for interactive elements, hairline-bordered cards (no shadows), and a
single inverted dark surface used once per page as the contact call-to-action.
Headings use Nunito, body uses Inter, and code/terminal UI uses JetBrains Mono.
Light and dark themes are defined as CSS variables in src/styles/index.css and
switched by toggling a .dark class on <html>. The pill CTA palette inverts
automatically. An inline script in index.html applies the stored or
system-preferred theme before first paint to avoid a flash, and useTheme
keeps React in sync and persists the user's choice to localStorage.
The codebase follows a feature-based architecture. Each page section is a
self-contained feature module, composed by App.tsx. Cross-cutting concerns
(layout, reusable UI, animation, and content) are isolated into dedicated
directories.
Guiding principles:
- Single Responsibility — each module does one thing; sections never reach into one another.
- Single Source of Truth — design tokens live in
tailwind.config.ts; content lives insrc/content/site.ts; motion utilities live insrc/lib/anime.ts. - Progressive enhancement — content is rendered and visible without
JavaScript or animation; motion is layered on top and respects
prefers-reduced-motion. - Mobile-first & fluid — layouts scale with
clamp()and responsive utilities instead of fixed pixel values and per-breakpoint overrides.
Prerequisites: Node.js 18+ and npm.
# Install dependencies
npm install
# Start the development server (http://localhost:5173)
npm run dev| Command | Description |
|---|---|
npm run dev |
Start the Vite development server with HMR. |
npm run build |
Type-check, then produce an optimized build in dist/. |
npm run preview |
Serve the production build locally. |
npm run lint |
Run ESLint with a zero-warning policy. |
npm run format |
Format the source with Prettier. |
npm test |
Run the Vitest suite once. |
npm run test:watch |
Run Vitest in watch mode. |
npm run optimize:images |
Regenerate the portrait AVIF/WebP/JPG variants. |
npm run make:og |
Regenerate the social share card (public/og.png). |
npm run make:thumbs |
Regenerate project card thumbnails in public/images/projects/. |
src/
├── main.tsx # Application entry point
├── App.tsx # Section composition + skip-to-content link
├── styles/
│ └── index.css # Tailwind layers, base styles, light/dark tokens
├── content/
│ └── site.ts # All copy and data (single source of truth)
├── lib/
│ ├── anime.ts # anime.js re-exports + reduced-motion guard
│ ├── emailjs.ts # EmailJS config (env vars + committed defaults)
│ └── posthog.ts # PostHog product analytics (explicit events)
├── hooks/
│ ├── useTheme.ts # Theme state synced to <html> + localStorage
│ └── useTrackInView.ts # Fire a callback once on scroll-into-view
├── components/
│ ├── Reveal.tsx # Scroll-reveal wrapper (anime.js + IO)
│ ├── ThemeToggle.tsx # Animated light/dark switch
│ ├── TerminalCard.tsx # Reusable terminal mockup (traffic lights)
│ ├── StatCounter.tsx # Count-up stat animation
│ ├── BackToTop.tsx # Scroll-to-top control
│ ├── ui/
│ │ └── Section.tsx # Section shell + documentation-style heading
│ └── layout/
│ ├── Header.tsx # Sticky nav (terminal brand, theme, mobile menu)
│ └── Footer.tsx
└── features/ # One folder per page section
├── hero/ # Animated "whoami" terminal + stats
├── about/
├── experience/ # Experience & focus timeline
├── services/
├── skills/ # Interactive terminal-style skill explorer
└── contact/ # Inverted dark CTA surface
public/
├── favicon-32.png # Brand favicon (+ favicon-180 / icon-192 / icon-512)
├── site.webmanifest
├── og.png # 1200x630 social share card (run `npm run make:og`)
├── robots.txt # Crawl directives + sitemap pointer
├── sitemap.xml # Single-URL sitemap
├── 404.html # Styled, theme-aware not-found page
├── CNAME # Custom domain for GitHub Pages
└── images/ # Portrait + project thumbnails (projects/)
DESIGN.md # Design-language reference (Ollama-inspired)
_legacy/ # Previous build artifact, retained for reference
All text, services, skills, and social links are defined in
src/content/site.ts as typed, exported objects.
Updating a bio, adding a skill, or changing a link is a single-file edit that
never requires modifying a component.
// src/content/site.ts
export const profile = {
fullName: "Mahnoor Zaffar",
role: "Full-Stack Web Developer & AI Engineer",
email: "1999mahnoor+developer@gmail.com",
// ...
};- Lean bundle — the production build is roughly 97 KB gzipped, with CSS purged to the classes actually used.
- Layout stability — the portrait declares explicit dimensions and an
aspect-ratio, and is served via<picture>with AVIF/WebP sources to minimize Cumulative Layout Shift. - Fonts — self-hosted via Fontsource (variable, weight-axis only) and
bundled by Vite; no third-party CDN, with a
system-uifallback. The browser downloads only the needed subsets viaunicode-range. - Images — the portrait ships as AVIF/WebP/JPG (~36–65 KB, down from
1.6 MB) via
<picture>, regenerated withnpm run optimize:images. - Accessibility — semantic landmarks, a skip-to-content link, keyboard-
visible focus states,
arialabelling on interactive controls, and fullprefers-reduced-motionsupport. Verified at Lighthouse Accessibility 100.
- Structured data — a JSON-LD
Personschema inindex.htmlhelps search engines and recruiter tools parse the profile. - Discoverability —
public/robots.txtandpublic/sitemap.xml. - Social card — a 1200×630
public/og.pngis referenced via absolute-URL Open Graph / Twitter tags so links unfurl correctly on LinkedIn, X, Slack, etc. Regenerate withnpm run make:og.
Product analytics via PostHog, tracking explicit events
only (no autocapture, no session replay). Configure it with VITE_POSTHOG_KEY
(your phc_... project key) and optionally VITE_POSTHOG_HOST (defaults to the
US cloud). See src/lib/posthog.ts for the event schema
and src/vite-env.d.ts for the env types.
Tracked events:
portfolio_view,project_viewgithub_click,demo_clickcontact_click,resume_download,linkedin_click
In CI, the key and host are injected at build time from the
VITE_POSTHOG_KEY / VITE_POSTHOG_HOST repository secrets.
To surface a "Résumé ↓" button in the hero, drop a PDF in public/ (e.g.
public/resume.pdf) and set profile.resumeUrl in
src/content/site.ts to its path. Empty = hidden.
Vitest + React Testing Library (jsdom). Run npm test.
Current coverage: site.ts content-shape validation, the contact form's
validation behaviour, and the section-heading component.
The contact form sends messages serverlessly via EmailJS.
It needs three public (non-secret) values, configured in
src/lib/emailjs.ts from env vars:
| Variable | Where to find it (EmailJS dashboard) |
|---|---|
VITE_EMAILJS_SERVICE_ID |
Email Services → your service |
VITE_EMAILJS_TEMPLATE_ID |
Email Templates → your template |
VITE_EMAILJS_PUBLIC_KEY |
Account → General → Public Key |
The email template should reference these variables: {{from_name}},
{{reply_to}}, {{message}}.
Because this is a static client app, these keys ship in the bundle either way,
so committed defaults live in src/lib/emailjs.ts and the
form works out of the box in production. The env vars above are optional
overrides for local testing or key rotation — copy .env.example
to .env.local and set them (a non-empty value wins over the default).
Abuse protection comes from the domain allow-list in EmailJS → Account →
Security (restricted to mahnoorzaffar.dev), not from key secrecy. If the
config is ever cleared the form falls back to a pre-filled mailto: link.
The site is served as a static build and deployed to the custom domain in
CNAME (www.mahnoorzaffar.dev).
npm run build # outputs to dist/Deploy the contents of dist/ to any static host (e.g. GitHub Pages, Netlify,
Vercel). The previous artifact build is preserved under _legacy/ and tagged
deploy-snapshot-2026-06 for rollback.
Shipped:
- Projects section — selected FinTech / HealthTech / AI work with live
demos, editable in
src/content/site.ts. - Serverless contact form via EmailJS (with a
mailto:fallback). - SEO — JSON-LD,
robots.txt,sitemap.xml, and an OG share card. - Résumé download button (
public/resume.pdf). - Vitest test suite (runs in CI before every deploy).
- PostHog product analytics (explicit events only).
- Project card thumbnails for all six featured projects.
Optional follow-ups:
- Front the domain with a CDN (e.g. Cloudflare) for long-lived asset caching — the one remaining Lighthouse "cache lifetimes" note (GitHub Pages can't set custom headers).
© Mahnoor Zaffar. All rights reserved.