Home

A. Morvan — Photographer Portfolio (Demo)

CI Build & Deploy Docs Static site

⚠️ This is a demonstration website. "A. Morvan" is a fictional photographer created purely to showcase a front-end design. All photographs are royalty-free images from Unsplash, and the people and places shown are not affiliated with this project. The contact email uses an intentionally invalid .invalid domain and is not monitored — please don't send anything to it.

A dark-mode, editorial photographer portfolio built with React and Vite. It ships with three views (Projects / The Eye & the Intent / Contact), FR/EN internationalization with browser-language detection, scroll-reveal animations, a masonry photo grid, and a fullscreen lightbox.

Tech stack

Prerequisites

  • Node.js 18+ (Node 20 recommended)
  • npm (bundled with Node)

Getting started

From inside the demo-website folder (not its parent):

cd demo-website

# 1. Install dependencies
npm install

# 2. Start the local dev server (http://localhost:5173)
npm run dev

npm start is available as an alias for npm run dev. If you see ENOENT: no such file or directory, open '.../package.json', you're running npm from the wrong folder — make sure you cd demo-website first.

Available scripts

Command Description
npm run dev Start the Vite dev server with hot reload
npm start Alias for npm run dev
npm run build Build the production site into dist/
npm run preview Serve the built dist/ locally to preview it
npm run lint Run Oxlint over the source
npm test Run the unit tests in watch mode (Vitest)
npm run test:run Run the unit tests once (CI mode)
npm run coverage Run the tests and print a coverage report
npm run docs Generate the HTML API docs into docs/ (JSDoc)
npm run format Format the whole codebase with Prettier
npm run format:check Check formatting without writing (used by CI)

Testing

Unit tests use Vitest and live next to the code they cover (src/**/*.test.js). The current suite (src/data.test.js) validates the content layer: FR/EN structural parity, category/photo counts, asset URL resolution, alt text, and the naming/typography rules the site relies on.

npm test          # watch mode
npm run test:run  # single run (used by CI)
npm run coverage  # single run + coverage summary

To test React components later, add jsdom and @testing-library/react, then set environment: 'jsdom' in vitest.config.js.

Formatting

Code style is enforced with Prettier (.prettierrc.json: single quotes, no semicolons) and EditorConfig (.editorconfig). The CI runs npm run format:check and fails on unformatted files.

First-time setup: run npm run format once and commit the result so the whole codebase matches Prettier — otherwise the CI format check will flag pre-existing files.

Documentation (JSDoc)

The JavaScript is annotated with JSDoc. Generate a browsable HTML reference with:

npm run docs

Output goes to docs/ (git-ignored) — open docs/index.html in a browser. Configuration lives in jsdoc.json; it documents the .js sources (e.g. src/data.js) and uses this README as the docs landing page.

The deploy workflow also builds the docs into the published site, so once deployed they're live at https://craft-and-code.github.io/demo-website/docs/ (the "View the docs" link on the Contact page points there).

SEO

index.html includes primary meta tags (description, author, robots, theme-color), Open Graph and Twitter Card tags, and a schema.org Person JSON-LD block. public/robots.txt and public/sitemap.xml are served at the site root. All URLs (canonical, OG/Twitter image, sitemap) point to the deployed site https://craft-and-code.github.io/demo-website/ — update them if you move the site to a custom domain.

Project structure

demo-website/
├─ public/
│  ├─ images/              # All portfolio photos (LiveStage-*, Urban-*, CandidMoment-*, AMorvan.jpg)
│  ├─ favicon.svg          # Logo used as the tab icon
│  ├─ apple-touch-icon.png, icon-192.png, icon-512.png  # PWA / iOS icons
│  ├─ manifest.webmanifest # PWA manifest
│  ├─ robots.txt           # SEO: crawler directives
│  └─ sitemap.xml          # SEO: sitemap
├─ src/
│  ├─ components/          # Header, ProjectsPage, PhotoGrid, Lightbox, OeilPage, ContactPage, Footer
│  ├─ data.js              # FR/EN translations + photo data (JSDoc-annotated)
│  ├─ data.test.js         # Unit tests for the content layer
│  ├─ App.jsx              # Root: page/language state
│  ├─ main.jsx             # React entry point (self-hosted @fontsource imports)
│  └─ index.css            # Tailwind import + theme tokens + cursors + a11y
├─ .github/workflows/
│  ├─ ci.yml               # Lint + test + build on push/PR
│  └─ deploy.yml           # GitHub Pages deployment
├─ vite.config.js          # Vite + React + Tailwind (relative base for Pages)
├─ vitest.config.js        # Vitest unit-test config
├─ jsdoc.json              # JSDoc generation config
└─ LICENSE                 # MIT (code only; photos are Unsplash-licensed)

Tailwind v4 reads its design tokens from @theme in src/index.css, so there is no tailwind.config.js — the accent color and fonts live in the CSS.

Deploying to GitHub Pages

Yes — this is a static site, so GitHub Pages works. A ready-to-use GitHub Actions workflow is included at .github/workflows/deploy.yml. It builds the site and publishes dist/ automatically.

One-time setup:

  1. Push this project to a GitHub repository (on the main branch).
  2. In the repo, go to Settings → Pages.
  3. Under Build and deployment → Source, select GitHub Actions.
  4. Push to main (or run the workflow manually from the Actions tab). The site will be published at https://craft-and-code.github.io/demo-website/.

The Vite config uses a relative base (base: './') and all image paths are prefixed with import.meta.env.BASE_URL, so the site works whether it's served from a domain root or from a /demo-website/ subpath — no extra configuration needed.

Assets & credits

All imagery comes from Unsplash (free license) and was downloaded into public/images/, renamed per project. Photos display order is driven by src/data.js. Sources:

File Category Source
AMorvan.jpg Portrait Unsplash
LiveStage-01.jpg Live & Stage Unsplash
LiveStage-02.jpg Live & Stage Unsplash
LiveStage-03.jpg Live & Stage Unsplash
LiveStage-04.jpg Live & Stage Unsplash
LiveStage-05.jpg Live & Stage Unsplash
LiveStage-06.jpg Live & Stage Unsplash
LiveStage-07.jpg Live & Stage Unsplash
LiveStage-08.jpg Live & Stage Unsplash
Urban-01.jpg Urban Geometry Unsplash
Urban-02.jpg Urban Geometry Unsplash
Urban-03.jpg Urban Geometry Unsplash
Urban-04.jpg Urban Geometry Unsplash
Urban-05.jpg Urban Geometry Unsplash
Urban-06.jpg Urban Geometry Unsplash
Urban-07.jpg Urban Geometry Unsplash
Urban-08.jpg Urban Geometry Unsplash
CandidMoment-01.jpg Candid Moments Unsplash
CandidMoment-02.jpg Candid Moments Unsplash
CandidMoment-03.jpg Candid Moments Unsplash
CandidMoment-04.jpg Candid Moments Unsplash
CandidMoment-05.jpg Candid Moments Unsplash
CandidMoment-06.jpg Candid Moments Unsplash
CandidMoment-07.jpg Candid Moments Unsplash
CandidMoment-08.jpg (not used — only 7 candid photos) Candid Moments Unsplash

Notes

  • Languages: the UI starts in the browser's language (navigator.language); toggle FR/EN with the pill in the header.
  • Images: swap any file in public/images/ (keep the same filename) to change a photo; captions live in src/data.js.
  • The "Candid Moments" category uses 7 photos (CandidMoment-0107); add a matching entry in src/data.js if you drop in an 8th (source above).

Accessibility & performance

  • Reduced motion: animations respect the OS "reduce motion" preference — framer-motion via <MotionConfig reducedMotion="user"> and CSS via a prefers-reduced-motion media query.
  • Images: gallery and portrait images use loading="lazy" + decoding="async", and each tile reserves its space with a CSS aspect-ratio to avoid layout shift.
  • PWA-ready: public/manifest.webmanifest plus SVG and PNG icons (apple-touch-icon.png 180×180, icon-192.png, icon-512.png) allow adding the site to a home screen with a proper icon on iOS and Android.

License / disclaimer

Code is released under the MIT License — see LICENSE.

The photographs are not covered by that license: they are royalty-free images from Unsplash (see Assets & credits) and remain subject to the Unsplash License. "A. Morvan" is a fictional persona; people and places shown are not affiliated with this project. © 2026 (demo).