Skip to content

Commit 514d3dd

Browse files
committed
docs: rewrite README and update project documentation
- Rewrite README with tool tables, tech stack, architecture highlights, getting started guide, and project structure - Add Logs section to CLAUDE.md (dev log path, prod via Vercel) - Add E2E testing section to CLAUDE.md with pitfalls guide - Update all docs to remove ToolPageShell/Breadcrumbs references - Update test counts and command listings
1 parent 9c7e684 commit 514d3dd

5 files changed

Lines changed: 227 additions & 83 deletions

File tree

CLAUDE.md

Lines changed: 121 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ PhotoTools is an educational photography application — free calculators, simul
1010

1111
- Next.js 16 (App Router, Turbopack dev)
1212
- React 19 + TypeScript 6
13+
- next-intl 4.x (i18n — single-locale English, ready for multi-locale)
1314
- Vitest + jsdom + @testing-library/react + @testing-library/jest-dom
1415
- ESLint with typescript-eslint
1516
- CSS Modules + CSS custom properties (design tokens)
@@ -22,8 +23,10 @@ PhotoTools is an educational photography application — free calculators, simul
2223
- `npm run dev` — start dev server with Turbopack at `http://localhost:3000`
2324
- `npm run build` — production build via `next build`
2425
- `npm run start` — serve production build locally
25-
- `npm test` — run Vitest tests (351 tests across 24 files)
26+
- `npm test` — run Vitest tests (353 tests across 25 files)
2627
- `npm run test:watch` — run tests in watch mode
28+
- `npm run test:e2e` — run Playwright e2e tests (requires a build first)
29+
- `npm run test:e2e:ui` — run Playwright tests with interactive UI
2730
- `npm run lint` — run ESLint
2831

2932
## Architecture
@@ -32,11 +35,11 @@ All source code lives under `src/`, with `@/` aliased to `src/` in tsconfig.json
3235

3336
- **App Router**: all routes under `src/app/`. Homepage (`src/app/page.tsx`) is the tool hub. Each tool lives at `src/app/[slug]/page.tsx` with co-located `_components/` for tool-specific UI. Glossary at `src/app/learn/glossary/page.tsx`. Additional pages: `/about`, `/contact`, `/privacy`, `/terms`. API route: `src/app/api/contact/route.ts`.
3437
- **Tool Co-location**: Each tool's components live in `src/app/[slug]/_components/` alongside its `page.tsx`. The `_` prefix makes it a private folder (not a route segment). Page files import from `./_components/...` using relative paths.
35-
- **Shared Components**: `src/components/` contains only shared/reusable code: `layout/` (Nav, Footer, ThemeProvider, ThemeToggle) and `shared/` (ToolPageShell, LearnPanel, ControlPanel, ToolIcon, InfoTooltip, ShareModal, ToolActions, FileDropZone, PhotoUploadPanel, ScenePicker, DraftBanner, Breadcrumbs, JsonLd, DoFDiagram, DoFCanvas, AnimatedGrid, ModeToggle, AdUnit, AdScripts, MobileAdBanner).
38+
- **Shared Components**: `src/components/` contains only shared/reusable code: `layout/` (Nav, Footer, ThemeProvider, ThemeToggle) and `shared/` (LearnPanel, ChallengeCard, ControlPanel, FocalLengthField, ToolIcon, InfoTooltip, ShareModal, ToolActions, FileDropZone, PhotoUploadPanel, ScenePicker, DraftBanner, JsonLd, DoFDiagram, DoFCanvas, AnimatedGrid, ModeToggle, AdUnit, AdScripts, MobileAdBanner).
3639
- **Tool Registry**: `src/lib/data/tools.ts` defines all tools with slug, name, description, `dev`/`prod` status fields (`'live'`/`'draft'`/`'disabled'`), and category. `getLiveTools()` returns live tools. `getVisibleTools()` returns live + draft. `getToolBySlug()` looks up by slug. `getAllTools()` returns all tools regardless of status.
37-
- **Education System**: `src/lib/data/education/` contains per-tool educational content (beginner/deeper explanations, key factors, pro tips, tooltips, challenges). `LearnPanel` renders as a right sidebar on every tool page.
40+
- **Education System**: `src/lib/data/education/` contains per-tool education skeletons (non-translatable data: IDs, difficulty levels, correct answers, option values). All translatable education text lives in `src/lib/i18n/messages/en/education/*.json`. `LearnPanel` and `ChallengeCard` render by combining skeleton data with translations.
3841
- **Pure Math Modules**: `src/lib/math/` contains pure functions for FOV, DOF, exposure (including shader math for CoC, motion blur, noise), diffraction, star trails, color, histogram, compression, frame, and grid calculations. Each has co-located `.test.ts` files. TDD approach — math is tested independently from UI.
39-
- **Data**: `src/lib/data/` contains tool registry, education content, sensors (with dimensions/colors), focal lengths, scenes, glossary, camera settings (apertures/shutter speeds/ISOs), ND filters, and white balance presets — each with tests.
42+
- **Data**: `src/lib/data/` centralizes all pure data. Shared data files (with tests): tool registry, education skeletons, sensors, focal lengths, scenes, glossary, camera settings (apertures/shutter speeds/ISOs), ND filters, white balance presets. Per-tool data files: `frameStudio.ts`, `exposureScenes.ts`, `fovSimulator.ts`, `colorSchemeGenerator.ts`, `exifViewer.ts`, `starTrailCalculator.ts`, `dofCalculator.ts`, `hyperfocalSimulator.ts`, `perspectiveCompression.ts`.
4043

4144
## Key Directories
4245

@@ -49,17 +52,77 @@ src/
4952
api/contact/ Contact form API route
5053
components/
5154
layout/ Nav (mega-menu), Footer, ThemeProvider, ThemeToggle
52-
shared/ ToolPageShell, LearnPanel, ControlPanel, AdUnit, MobileAdBanner, etc.
55+
shared/ LearnPanel, ChallengeCard, ControlPanel, FocalLengthField, AdUnit, MobileAdBanner, etc.
56+
i18n/
57+
request.ts Message loader — merges all JSON files at request time
58+
messages/en/ i18n translation files (next-intl)
5359
lib/
54-
math/ Pure calculation modules (fov, dof, exposure, compression, frame, grid, etc.)
55-
data/ Tool registry, education content, sensors, focal lengths, scenes, glossary, camera, ndFilters, whiteBalance
56-
data/education/ Per-tool educational content, challenge definitions, types
60+
math/ Pure calculation modules (fov, dof, exposure, compression, frame, grid, color, etc.)
61+
data/ All pure data: shared (tools, sensors, glossary, camera, etc.) + per-tool (fovSimulator, frameStudio, etc.)
62+
data/education/ Per-tool education skeletons, types, barrel export
5763
ads.ts Ad configuration and feature flags
5864
utils/ Query sync, export helpers
5965
types.ts Shared TypeScript types
66+
og.tsx + og-layout.tsx OpenGraph image generation
67+
e2e/ Playwright e2e test specs
6068
public/ Images, icons, manifest, sitemap, robots.txt
6169
```
6270

71+
## Internationalization (i18n)
72+
73+
Uses **next-intl** with a single-locale (English) setup, ready for multi-locale expansion.
74+
75+
### How it works
76+
77+
All translatable strings live in JSON files under `src/lib/i18n/messages/en/`. The message loader (`src/lib/i18n/request.ts`) imports every JSON file and merges them into namespaces. The root layout's `NextIntlClientProvider` makes the merged messages available to all components.
78+
79+
**Namespaces** (top-level key in each JSON file → how components access strings):
80+
81+
| Namespace | JSON location | Access pattern |
82+
|-----------|--------------|----------------|
83+
| `common` | `messages/en/common.json` | `useTranslations('common')``t('nav.tools')` |
84+
| `home` | `messages/en/home.json` | `useTranslations('home')``t('hero.title')` |
85+
| `tools` | `messages/en/tools.json` | `useTranslations('tools')``t('fov-simulator.name')` |
86+
| `glossary` | `messages/en/glossary.json` | `useTranslations('glossary')``t('entries.aperture.term')` |
87+
| `metadata` | `messages/en/metadata.json` | `getTranslations('metadata')` (server) |
88+
| `about`, `contact`, `privacy`, `terms` | `messages/en/<name>.json` | `useTranslations('<name>')` |
89+
| `education.<tool-slug>` | `messages/en/education/<tool-slug>.json` | `useTranslations('education.<tool-slug>')``t('beginner')` |
90+
| `toolUI.<tool-slug>` | `messages/en/tools/<tool-slug>.json` | `useTranslations('toolUI.<tool-slug>')``t('labelName')` |
91+
92+
Education files are merged into a single `education` namespace; tool UI files are merged into a single `toolUI` namespace. Both use `<tool-slug>` as a sub-key.
93+
94+
### Adding new strings
95+
96+
**To an existing tool** — add keys to the tool's JSON file and use them in the component:
97+
1. Add key to `src/lib/i18n/messages/en/tools/<tool-slug>.json` under the `toolUI.<tool-slug>` object
98+
2. In component: `const t = useTranslations('toolUI.<tool-slug>')``t('newKey')`
99+
100+
**To a new tool** — 3 files must be created and registered:
101+
1. Create `src/lib/i18n/messages/en/tools/<tool-slug>.json` with `{ "toolUI": { "<tool-slug>": { ... } } }`
102+
2. Create `src/lib/i18n/messages/en/education/<tool-slug>.json` with `{ "education": { "<tool-slug>": { ... } } }`
103+
3. **Register both files in `src/lib/i18n/request.ts`**: add an import to the `Promise.all` array, add the variable to the `toolUIMessages` and/or `educationMessages` reducer array. **If you skip this step, the strings will not load and you'll get `MISSING_MESSAGE` errors at runtime.**
104+
105+
**To shared UI** (nav, footer, actions, etc.) — add keys to `messages/en/common.json` under the appropriate sub-object.
106+
107+
**Server components / metadata** — use `const t = await getTranslations('namespace')` (async).
108+
109+
**Rich text**`t.rich('key', { link: (chunks) => <a href="...">{chunks}</a> })` for inline markup.
110+
111+
### What NOT to put in i18n
112+
113+
- **Canvas/WebGL data** — sensors, scenes, focal lengths, ND filters, WB presets keep English text directly in `src/lib/data/` files because `useTranslations` isn't available in draw functions. When adding locales, pass resolved names into drawing functions.
114+
- **Education skeletons** (`src/lib/data/education/`) store only non-translatable data (IDs, difficulty, correctOption, optionValues). All text is in the education JSON files.
115+
- **Glossary** (`src/lib/data/glossary.ts`) stores entry IDs and optional relatedTool slugs. Terms and definitions are in `messages/en/glossary.json`.
116+
117+
## Data Management
118+
119+
All pure data (constants, presets, lookup tables, configuration arrays) lives in `src/lib/data/`. UI plumbing (`PARAM_SCHEMA`, `DEFAULT_STATE`, component-specific config) stays co-located in `_components/`.
120+
121+
- **Shared data** — data used by 2+ tools gets its own file (e.g. `camera.ts` exports `APERTURES` used by star-trail and hyperfocal tools).
122+
- **Per-tool data** — tool-specific constants get a file named after the tool (e.g. `fovSimulator.ts`, `frameStudio.ts`).
123+
- **i18n for data** — user-facing prose goes to i18n JSON files (`src/lib/i18n/messages/en/`), not data files. Data files consumed by canvas/WebGL keep English text directly (sensors, scenes, etc.) since `useTranslations` isn't available in draw functions.
124+
- **When adding a new tool**: extract all constants, presets, and lookup tables into `src/lib/data/<toolSlug>.ts`. Only leave UI wiring in `_components/`.
125+
63126
## Tool Visibility
64127

65128
Each tool in `src/lib/data/tools.ts` has separate `dev` and `prod` status fields with three states: `'live'`, `'draft'`, or `'disabled'`.
@@ -81,13 +144,13 @@ Each tool has a **LearnPanel** (right sidebar) with:
81144
- **Challenges** — 3-5 progressive multiple-choice questions with pass/fail feedback, try-again on wrong answers, reset all progress, persisted to localStorage
82145
- **Tooltips** — hover info icons on control labels (via `InfoTooltip` component)
83146

84-
Content is defined as structured data in `src/lib/data/education/content.ts`, `content2.ts`, and `frame-studio.ts`. Types in `types.ts`, barrel export via `index.ts`. To add education content for a new tool, add a `ToolEducation` entry matching the tool's slug.
147+
Education skeletons in `src/lib/data/education/content*.ts` define non-translatable structure (IDs, difficulty, correct answers). All user-facing text is in `src/lib/i18n/messages/en/education/*.json`. To add education content for a new tool, add a `ToolEducationSkeleton` entry and a corresponding JSON file.
85148

86149
## Design
87150

88151
- **No page scroll (desktop only).** On desktop, the application must fit within the viewport (100vh). The page never scrolls — only individual panels (controls, canvas, LearnPanel) scroll internally via `overflow-y: auto`. On mobile, pages are allowed to scroll naturally.
89152
- **FOV Simulator is the reference implementation.** All tools should match its look and feel: dark surface panels, compact controls, same spacing/typography tokens, and consistent use of `var(--accent)` for interactive elements.
90-
- **Three-column layout**: ToolPageShell renders tool content (left/center) + LearnPanel (right sidebar, collapsible). Full-height tools (FOV Simulator, Color Harmony, Exposure Simulator, Sensor Size Comparison) manage their own layout but include LearnPanel directly.
153+
- **Three-column layout**: Tool pages render content (left/center) + LearnPanel (right sidebar, collapsible). Each tool manages its own layout and includes LearnPanel directly.
91154
- **Tool icons**: Each tool has an inline SVG icon (`components/shared/ToolIcon.tsx`) displayed on homepage cards, nav mega-menu items, and tool page headers. Icons are mapped by slug.
92155
- **Nav mega-menu**: Tools dropdown groups tools by category (Visualizers, Calculators, Reference, File Tools) with icon + name + description per item.
93156

@@ -111,11 +174,57 @@ GoogleAdSense integration managed via `src/lib/ads.ts` (configuration and featur
111174
- **Named exports** for all components
112175
- **Shared components first**: Before building tool-specific UI, check `src/components/shared/` for existing components. When two or more tools need similar UI (e.g. file upload, scene selector, share modal), extract a shared component. Familiarize yourself with what each shared component does so you reuse them consistently.
113176
- **DRY**: Avoid duplicating logic, styles, constants, or markup. Extract shared utilities, components, and data modules. When adding a feature, check if similar patterns already exist in the codebase and reuse them.
114-
- **200-line file limit**: Keep all `.ts`/`.tsx` files under 200 lines. If a file grows beyond this, break it into smaller focused modules (e.g. extract hooks, sub-components, helpers, constants, or types into separate files).
177+
- **200-line file limit**: Keep all `.ts`/`.tsx` files under 200 lines (test files exempt). If a file grows beyond this, break it into smaller focused modules (e.g. extract hooks, sub-components, helpers, constants, or types into separate files).
115178
- **Test files** co-located next to source files (`*.test.ts`)
116-
- **24 test files, 351 tests** covering math, data, education, ads, and component integration
179+
- **25 test files, 353 tests** covering math, data, education, ads, and component integration
117180
- **Privacy Sandbox is deprecated** — do not discuss, recommend, or implement any Privacy Sandbox APIs (Topics, Attribution Reporting, Protected Audience, etc.)
118181

182+
## E2E Testing (Playwright)
183+
184+
Playwright integration tests live in `src/e2e/` and run against a production build (`npm run build` + `npm run start`). Config: `playwright.config.ts`.
185+
186+
### Structure
187+
188+
```
189+
src/e2e/
190+
smoke/all-pages.spec.ts Parameterized smoke tests for all pages (200 status, no console errors, content rendering, no desktop scroll)
191+
tools/fov-simulator.spec.ts FOV Simulator interaction tests
192+
tools/color-scheme.spec.ts Color Scheme Generator interaction tests
193+
tools/sensor-size.spec.ts Sensor Size Comparison interaction tests
194+
fixtures/test-image.jpg Minimal JPEG fixture for upload tests
195+
```
196+
197+
### Running
198+
199+
1. Build first: `npm run build`
200+
2. Run tests: `npm run test:e2e` (or `npx playwright test`)
201+
3. If port 3000 is in use: `lsof -ti:3000 | xargs kill -9` then retry
202+
4. Run a single spec: `npx playwright test src/e2e/tools/fov-simulator.spec.ts`
203+
5. Debug mode: `npx playwright test --debug`
204+
6. View last report: `npx playwright show-report`
205+
206+
### Common Pitfalls When Writing/Fixing Tests
207+
208+
- **Duplicate DOM elements**: Every tool page renders controls twice — once in the desktop sidebar (`<aside>`) and once in a mobile controls section. Always scope selectors to avoid matching both. Use `page.locator('aside').first()` or `page.locator('[class*="sidebar"]').first()`.
209+
- **LearnPanel challenge buttons**: The LearnPanel sidebar contains challenge questions with answer buttons (e.g. "24mm", "Complementary") that share text with tool control buttons. Use `sidebar.locator('button:text-is("...")')` scoped to the controls panel, not the full page.
210+
- **CSS Modules hashed classes**: Class names are hashed at build time. Never match exact class names. Use `[class*="partialName"]` attribute selectors (e.g. `[class*="paletteBarSwatch"]`, `[class*="editForm"]`, `[class*="copyGroup"]`).
211+
- **Hidden file inputs**: File upload uses a hidden `<input type="file">` inside `FileDropZone`. Use `page.locator('input[type="file"]').setInputFiles(...)` directly.
212+
- **Ref-based state (no re-render)**: Some components use refs instead of state (e.g. `PhotoPicker`'s `imageLoaded`). When a ref controls visibility, the DOM won't update after async operations. Fix with `page.evaluate()` to force styles, then `click({ force: true })`.
213+
- **Canvas/WebGL tools**: These render to `<canvas>`, not DOM elements. Use screenshot comparison (`canvas.screenshot()` + `Buffer.compare`) to verify visual changes.
214+
- **ESM module context**: Test files use ESM. Use `import { fileURLToPath } from 'url'` and `path.dirname(fileURLToPath(import.meta.url))` instead of `__dirname`.
215+
- **Console error filtering**: Smoke tests assert no console errors, but filter benign ones: favicon 404, cookieyes, adsense, adsbygoogle, `_vercel/speed-insights`. Add new benign patterns to the filter in `src/e2e/smoke/all-pages.spec.ts` as needed.
216+
- **i18n translated text**: UI labels come from `next-intl` message files (`src/lib/i18n/messages/en/`). If a placeholder or label changes, check the translation JSON, not just the component source.
217+
- **Select elements use value, not label**: For `<select>` dropdowns (e.g. sensor picker), use `selectOption('value_id')` not `selectOption({ label: '...' })`.
218+
219+
### CI
220+
221+
Playwright runs in GitHub Actions after `npm run build`. See `.github/workflows/deploy.yml`. CI installs Chromium + Firefox, runs all tests, and uploads `playwright-report/` as an artifact on failure.
222+
223+
## Logs
224+
225+
- **Dev logs**: `.next/dev/logs/next-development.log` — written automatically by Turbopack when `npm run dev` is running. Read this file when asked to "check the dev logs".
226+
- **Prod logs**: Use `vercel logs <url>` or check the Vercel dashboard. Use `vercel logs --follow` for live tailing.
227+
119228
## Deployment
120229

121230
- Vercel auto-deploys from `main` branch

GEMINI.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ Every tool features a `LearnPanel` (right sidebar) defined in `content.ts` and `
2828
- **Interactive Challenges**: 3-5 multiple-choice questions with persistence to `localStorage`.
2929
- **Tooltips**: Use `InfoTooltip` on control labels for quick definitions.
3030

31-
### 4. Layout Shell (`src/components/shared/ToolPageShell.tsx`)
32-
Standard three-column layout (Controls | Main Visualizer | LearnPanel). Provides consistent header actions (Share, Export, Info).
31+
### 4. Layout
32+
Each tool manages its own layout (Controls | Main Visualizer | LearnPanel). Consistent header actions (Share, Export, Info) via `ToolActions` component.
3333

3434
## Engineering Standards
3535

0 commit comments

Comments
 (0)