A. Morvan — Photographer Portfolio (Demo)
⚠️ 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
.invaliddomain 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
- Node + npm (runtime & package manager)
- React 19 + Vite (build tooling & dev server)
- Tailwind CSS v4 (via
@tailwindcss/vite) - framer-motion (animations, page transitions, animated nav)
- lucide-react (icons)
- @fontsource (self-hosted Inter + Playfair Display — no Google Fonts CDN)
- Vitest (unit tests)
- Prettier (code formatting)
- Oxlint (linting)
- EditorConfig (editor consistency)
- JSDoc (API docs)
- git (version control)
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 startis available as an alias fornpm run dev. If you seeENOENT: no such file or directory, open '.../package.json', you're running npm from the wrong folder — make sure youcd demo-websitefirst.
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 formatonce 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
@themeinsrc/index.css, so there is notailwind.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:
- Push this project to a GitHub repository (on the
mainbranch). - In the repo, go to Settings → Pages.
- Under Build and deployment → Source, select GitHub Actions.
- Push to
main(or run the workflow manually from the Actions tab). The site will be published athttps://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 insrc/data.js. - The "Candid Moments" category uses 7 photos (
CandidMoment-01–07); add a matching entry insrc/data.jsif 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 aprefers-reduced-motionmedia query. - Images: gallery and portrait images use
loading="lazy"+decoding="async", and each tile reserves its space with a CSSaspect-ratioto avoid layout shift. - PWA-ready:
public/manifest.webmanifestplus SVG and PNG icons (apple-touch-icon.png180×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).