Source for danhenderson.dev, a client-side portfolio site built with React, TypeScript, and MUI. The app stays fully static-hostable: content is stored in local TypeScript modules, routes are handled in the browser, and the CV enhances itself with public GitHub data when it is available.
This repository powers four main areas:
/for the home page and optional welcome audio/cvfor the interactive CV, downloadable resume link, and GitHub-backed highlights/climbingfor climbing ticks and to-do routes/photographyfor gallery browsing and album detail pages
The site is a React Router single-page app. Keep unknown-route rewrites, PUBLIC_URL compatibility, and shipped static assets intact when making changes.
| Route | Page | Purpose |
|---|---|---|
/ |
Home |
Intro page with optional welcome audio prompt |
/cv |
CV |
Resume-style experience, education, certificates, tools, code samples, and GitHub sections |
/climbing |
Climbing |
Tick list and route wish list in MUI X DataGrid tables |
/photography |
Photography |
Photography collection index |
/photography/:slug |
PhotographyCategory |
Album view for a selected collection |
* |
NotFound |
Fallback page |
- React 18
- TypeScript
- React Router v6
- MUI + Emotion
- MUI X DataGrid
@fontsource/source-sans-3@fontsource/space-groteskreact-github-calendar- Create React App (
react-scripts) - Node 20.x in CI
src/index.tsxbootstraps the app and wraps it withThemeProviderandWelcomeAudioProvider.src/App.tsxownsBrowserRouter, the sharedHeader/Footer, and route registration.- Route pages compose shared layout primitives such as
BackgroundPaper,PageFrame, andSectionCard. /cvis composed from reusable section components such asCVAboutSection,CVExperienceSection,CVEducationSection,CVVolunteeringSection,CVGitHubSection,CVCertificatesSection,CVStackToolsSection, andCVCodingSection, with layout orchestration insrc/pages/cvPageLayout.ts.
Animation behavior is intentionally centralized for the CV route instead of being hardcoded in individual feature components.
src/components/AnimatedContentCard.tsx- shared wrapper for card-style entry animation
- uses
IntersectionObserverto trigger near viewport entry - applies a literal
delayMsbefore a shortZoomtransition - skips observer setup, timers, and transition effects when
prefers-reduced-motionis enabled
src/components/AnimatedContentList.tsx- shared staggered list wrapper for repeatable CV content such as experience, education, volunteering, certificates, coding examples, and GitHub items
- supports both stacked and wrapped layouts while keeping delay math out of feature components
- supports reusable item surface modes (
card,panel,plain) so section components avoid repeated card/panel styling logic
src/components/AnimatedZoomList.tsx- shared staggered
Zoomlist for accordion chip reveals - used by the CV tools accordion so chip timing is not defined inline
- shared staggered
src/hooks/useHomeWelcomeSequence.ts- coordinates the home-page intro so the hero shell and title stay hidden until the welcome dialog and follow-up hints have been dismissed
src/styles/componentStyles.ts- source of truth for shared motion tokens and delay helpers used by CV animation wrappers and sections
- current motion tokens are:
itemOffsetMs = 120itemStaggerMs = 120sectionStaggerMs = 120githubSubsectionStaggerMs = 120accordionChipStaggerMs = 120
- exposes helpers such as
getSectionDelayMs(...),getItemDelayMs(...), andgetAnimatedZoomItemSx(...)so feature components avoid hardcoding delay math
Current /cv sequencing rules:
- Section cards stagger from shared section timing.
- Repeatable inner item groups wait for the same additional shared item offset before their own stagger starts.
- GitHub activity, contributions, and project chips follow the same shared item-offset rule as experience and volunteering.
- Reduced-motion users should receive the same content without stagger, delayed reveal, or decorative pulse behavior.
When changing CV motion:
- prefer updating
src/styles/componentStyles.tstokens/helpers first - reuse
AnimatedContentCard,AnimatedContentList, andAnimatedZoomList - avoid reintroducing inline transition-delay styles or per-component timing constants unless there is a route-specific reason
src/data/cv.ts- Profile/contact info
- Experience entries
- Education data
- Certificates
- Stack/tools list
- GitHub fallback activity, projects, and contributions
src/data/climbs.tsticks: sent routes with datestodos: project/wishlist routes- Current dataset size: 566 ticks and 352 todos
src/data/photography.ts- Album category cards + per-album images
- Current dataset size: 4 categories, 43 photos
src/hooks/useGithubProfile.ts fetches from GitHub:
- User events:
GET /users/:username/events/public - User repos:
GET /users/:username/repos - Public PR contribution search:
GET /search/issues - Repo metadata enrichment:
GET /repos/:owner/:repo
If API calls fail or are rate-limited, CV sections gracefully fall back to static content from src/data/cv.ts.
- Node.js 20.x
- npm
npm installnpm startThe development server defaults to port 3001.
Use a different port when needed:
PORT=3000 npm start| Script | Command | Purpose |
|---|---|---|
npm start |
PORT=${PORT:-3001} react-scripts start |
Start the local dev server |
npm run build |
react-scripts build |
Create the production build in build/ |
npm test |
react-scripts test |
Start the Jest runner |
npm run eject |
react-scripts eject |
Eject CRA configuration |
npm run test:e2e |
playwright test |
Run Playwright end-to-end tests (headless) |
npm run test:e2e:headed |
playwright test --headed |
Run E2E tests in a visible browser |
npm run test:e2e:ui |
playwright test --ui |
Open the Playwright interactive UI runner |
npm run serve:e2e |
serve -s build -l 3100 |
Serve the production build locally on port 3100 |
GitHub Actions workflows live in .github/workflows/:
tests.ymlrunsCI=true npm test -- --watch=false --passWithNoTests --coveragebuild.ymlrunsnpm run build
Playwright provides browser-level E2E coverage that complements the Jest unit/integration suite. Tests live in e2e/ and run against a production build served on port 3100.
Install the Chromium browser binary that Playwright requires (one-time setup):
npx playwright install chromiumBuild the app first, then run the test suite:
npm run build
npm run test:e2ePlaywright automatically starts a local server (serve -s build -l 3100) before running the tests and shuts it down afterwards. Outside CI, it reuses an already-running server on that port so you can keep npm run serve:e2e running in a separate terminal for faster iteration.
npm run test:e2e:headed # Watch tests run in a visible browser window
npm run test:e2e:ui # Open the Playwright interactive UI runnere2e/
├── helpers/
│ └── github.ts # Reusable GitHub API mock handlers (success + failure)
├── home.spec.ts # Home hero render and welcome audio prompt dismissal
├── cv.github.spec.ts # CV render, mocked GitHub API success, and graceful fallback
├── climbing.spec.ts # Climbing route tables render
├── photography.spec.ts # Photography category cards and direct slug navigation
└── not-found.spec.ts # 404 page for unknown routes
- Browser: Chromium only (initial rollout).
- Reduced motion: Tests call
page.emulateMedia({ reducedMotion: 'reduce' })before navigation so animated content renders immediately without waiting forIntersectionObserver-drivenZoomtransitions. - CI behavior: Single worker, retries twice, uses the
githubreporter, and traces/screenshots/video are captured on first retry or failure. - Artifacts: Test output goes to
e2e-results/and the HTML report toplaywright-report/; both are git-ignored.
- The app still depends on Create React App (
react-scripts), which is no longer actively maintained. - Current build/test output includes a
babel-preset-react-appwarning about@babel/plugin-proposal-private-property-in-objectbeing transitive-only. - React Router v6 future-flag warnings appear in tests (
v7_startTransition,v7_relativeSplatPath); behavior is currently correct but migration planning is pending.
src/data/cv.ts- primary CV content
- resume download metadata
- fallback GitHub activity, projects, and contributions used when runtime requests fail
src/data/climbs.ts- climbing ticks and to-do routes consumed by
useClimbingData - keep route formatting compatible with the existing sorting and normalization logic
- climbing ticks and to-do routes consumed by
src/data/photography.ts- photography collections, album images, and route slugs consumed by
usePhotographyData - preserve slug stability so existing album URLs continue to resolve
- photography collections, album images, and route slugs consumed by
src/ThemeProvider.tsx- application palette, typography, and component theme overrides
- persisted theme key:
danhenderson-theme
src/WelcomeAudioProvider.tsx- SoundCloud embed URL and welcome-audio behavior
- persisted audio consent key:
danhenderson-welcome-audio-consent
src/utils/assets.ts- centralized
PUBLIC_URL-aware asset path resolution viaresolvePublicAssetPath(...) - used by shared layout wrappers such as
BackgroundPaperto keep local and deployed asset paths consistent
- centralized
resume/daniel-henderson-resume.tex- LaTeX source for the downloadable PDF in
public/assets/daniel-henderson-resume.pdf
- LaTeX source for the downloadable PDF in
.
├── .github/workflows/ # Build and test automation
├── e2e/ # Playwright end-to-end tests
├── public/assets/ # Shipped images, certificates, media, and resume PDF
├── resume/ # LaTeX resume source
├── src/components/ # Shared UI and CV-specific components
├── src/data/ # Source-of-truth content modules
├── src/hooks/ # Data adapters for GitHub, climbing, and photography
├── src/pages/ # Route-level pages
├── src/styles/ # Shared animation, layout, and component style tokens
├── src/types/ # Shared TypeScript models
├── src/utils/ # Asset/date helpers and similar utilities
└── README.md
- Update CV copy, certificates, code examples, and GitHub fallback content in
src/data/cv.ts. - Replace the downloadable resume PDF at
public/assets/daniel-henderson-resume.pdfand keep related metadata insrc/data/cv.tsaligned with it. - Update app theme tokens and MUI component overrides in
src/ThemeProvider.tsx. - Keep reusable page and CV styling centralized in
src/styles/appStyles.tsandsrc/styles/componentStyles.tsrather than reintroducing component-localsxfragments. - Update welcome-audio behavior or track configuration in
src/WelcomeAudioProvider.tsx. - Use
resolvePublicAssetPath(...)fromsrc/utils/assets.tswhen adding new local asset paths to keepPUBLIC_URLbehavior stable. - When changing climbing or photography data, preserve
useClimbingDatasorting assumptions and photography slug stability.
- Production output is generated in
build/. - The host must rewrite unknown routes to
index.htmlso direct links like/cvand/photography/:slugwork. - Set
PUBLIC_URLwhen deploying under a subpath so generated asset URLs resolve correctly. - Route local asset URLs through
resolvePublicAssetPath(...)(already used byBackgroundPaper) to avoid subpath regressions. - Ship
public/assets/with the deployment. - The CV fetches public GitHub data at runtime; if requests fail or are rate-limited, the UI falls back to static content from
src/data/cv.ts.
README claims in this repo should stay aligned with package.json, src/App.tsx, src/data/, src/ThemeProvider.tsx, src/WelcomeAudioProvider.tsx, src/utils/assets.ts, and src/styles/componentStyles.ts.
Use these checks after meaningful changes:
npm run build
CI=true npm test -- --watch=false --passWithNoTests
npm run test:e2e