A single-page developer portfolio built from my CV. React + Vite + Tailwind CSS v4, no backend.
Two pages. / is the single-page portfolio — Hero · About · Skills · Experience · Projects ·
Education · Contact. /services/ is a standalone client-facing services page — website types ·
what I take on.
npm install
npm run dev # http://localhost:5173| Command | Description |
|---|---|
npm run dev |
Start the dev server with hot reload |
npm run build |
Production build into dist/ |
npm run preview |
Serve the built dist/ locally |
All copy lives in one file: src/data/profile.js. Change your details,
skills, jobs, projects, education and certifications there — the components read from it, so you
never have to touch JSX to update the site.
export const profile = { name, role, email, phone, github, ... }
// one flat list of groups — add, remove or reorder freely
export const skills = [
{ id, name, note, icon, primary?, items: [{ name, daily? }] },
]
export const projects = [ { name, tagline, featured, icon, points: [...], stack: [...] } ]
export const services = [
{ id, icon, title, tagline, flag?, pitch, bestFor,
deliverables: [...], stack: [...], proof },
]
export const servicesCta = { headline, sub, buttonLabel, mailSubject, mailBody }
// the /services/ gallery
export const websiteTypes = [
{ id, name, icon, preview, blurb, bestFor, highlights: [...] },
]
export const websiteTypesIntro = { title, lead }
export const servicesPage = { h1, intro }vite.config.js declares two HTML entries, not a client-side router:
| Entry | Source | Mounts | Output |
|---|---|---|---|
main |
index.html |
src/App.jsx |
dist/index.html |
services |
services/index.html |
src/ServicesApp.jsx |
dist/services/index.html |
So /services/ is a genuinely separate document with its own <title>, meta description,
canonical and OG tags, it needs no routing dependency, and neither page ships the other's
JavaScript — Rollup splits a shared vendor chunk plus one small per-page chunk. Both entries load
the same src/index.css and reuse Background, Nav and Footer.
<Nav standalone current="services" /> is what makes the shared nav work off the home page: the
section entries become /#about instead of #about, the scroll-spy does not run (there are no
sections to spy on), and the active item is the current page rather than an observed section.
navLinks is the single source of truth for the nav. It drives the nav, the scroll-spy and
the numbered eyebrow on every section, via sectionNo(id) — so there are no hand-written 01 —
strings left to go stale. To move a section, reorder navLinks and match it in App.jsx; every
ordinal renumbers itself.
An entry with page: true is a separate HTML page rather than a home-page section, so it carries
a real href and is excluded from both the scroll-spy and the numbering. pageSections is the
filtered list the ordinals actually count, and sectionNo returns null for anything absent from
it, so the eyebrow drops its prefix instead of rendering 00 —.
websiteTypes feeds the gallery at the top of /services/. preview names a composition in
SiteMockup.jsx — compositions live in the component because a layout is markup, not content, and
encoding one as coordinate arrays in the data file would make the data unreadable without buying
anything. Adding a type means adding a composition there and mapping its icon in
WebsiteTypes.jsx.
Each service is a cell in the Services lattice. pitch addresses the client in second
person and leads with the outcome rather than the technology; deliverables render as a
ruled ledger; proof must name a real project from the projects array above, and
links through to that section. Exactly one service carries featured: true, which earns
it a "Most requested" marker — a label, not a size change, so the lattice stays even.
The Skills section renders as a set of capability cards. name is the plain-English group
title and note is the one line under it saying what the group is for. The single group
marked primary: true renders as the wide featured band at the top; everything else falls
into the grid below, in array order.
Marking an item daily: true gives it a filled dot, heavier weight and a tinted plate — the
section lead tells the reader what that means, so there is no separate legend to keep in sync.
Icons are lucide-react names, referenced as strings in the data and mapped
in the component (ICONS object in Skills.jsx / Projects.jsx / Services.jsx). If you add a new
icon name to the data, add it to that map too — an unmapped name silently falls back to the
component's default icon rather than failing the build.
Drop the new file at public/Upendra_Kumar_Mahto_CV.pdf (or change profile.resume to match a
different filename). The nav and contact download buttons both point at it.
- Theming — light/dark is a
.darkclass on<html>, set before first paint by an inline script inindex.htmlso there is no flash. It follows the OS setting until the user picks a theme, which is then stored inlocalStorage. - Colours — semantic tokens (
--bg,--fg,--card,--line,--grad-*) are defined per theme insrc/index.css. Accent tokens (--color-brand-400,--color-teal-400) are overridden per theme too, so every Tailwind accent utility darkens automatically in light mode. - Motion — scroll reveals use a shared
IntersectionObserver(components/Reveal.jsx), and everything is disabled underprefers-reduced-motion. That block zeroes transition and animation delays as well as durations: a stagger is expressed as a delay, so without that the content would sit invisible for the full delay and then snap in. - Services lattice — Skills and Projects already own "grid of bordered cards with a
gutter", so Services is deliberately a different object: one plate subdivided by 1px
seams. A single light tracks the pointer underneath the cell faces — soft across each
face, near-full strength through the seams, where there is never any text to lose
contrast against. It is a fixed-size blob moved with
translate3d(compositor only, no repaint),--mx/--myare written on the childless glow node to keep style invalidation to one element, and the one layout read happens inside the rAF, at most once a frame. Per-service accents step along acolor-mix(in oklab, brand → teal)ramp; because both endpoints are theme-swapped tokens, every step clears WCAG AA in light and dark (measured 5.25–12.29:1). Worst case under the light — cursor parked on a hovered cell's body copy — measures 5.69:1 dark and 5.89:1 light. - Blueprint table (
/services/) — six miniature wireframes, one per kind of site. Three things keep it from restating the light table below it: each preview is an opaque sheet where the light table is translucent glass; the caption sits outside the frame as a<figcaption>on bare ground rather than inside a bordered face; and the wireframe is grey with the accent reserved for the things you would actually click — buttons, prices, the active nav item, chart data. That last rule is what stops an abstract sketch reading as a grey loading skeleton, which is otherwise exactly what it becomes in dark mode. Each preview is sized withaspect-ratio+container-type: inline-size+max(px, cqw), so it scales as a single unit with no internal breakpoints. The entrance uses from-only keyframes withanimation-fill-mode: backwards: the implicittois the element's own computed style, so the assembled state is the resting state structurally — a card cannot strand half-built, and the reduced-motion rule above lands everything finished with no separate fallback.
The build is fully static — npm run build, then serve dist/.
- Netlify / Vercel — build command
npm run build, publish directorydist. - GitHub Pages — set
base: '/<repo-name>/'invite.config.jsfirst, then publishdist/.
React 18 · Vite 6 · Tailwind CSS v4 · lucide-react