Live: ngopimas.github.io/enminutes
A baguette costs around €1.20 today. But is that expensive? Compared to what?
Raw prices are deceiving. In 1960, a baguette cost the equivalent of €0.07 - which sounds absurdly cheap, until you realize the net minimum wage was €0.22/h. That baguette took 18 minutes of work to earn. Today, with a net SMIC of €9.52/h, it takes 7 minutes.
En Minutes converts every price into minutes of work at a given salary - a unit that cuts through inflation, currency changes, and seven decades of economic shifts. It tracks 35+ everyday goods in France from the 1950s to today, against SMIC, median salary, or mean salary, and presents the results as interactive items and charts.
- 35+ consumer products tracked from 1950 to present
- Three salary references: SMIC (minimum wage), median salary, mean salary
- Composite purchasing power index (base 100 in 1960) with overlays:
- CPI inflation rate
- Labour productivity (OECD GDP per hour worked)
- French presidential timeline
- Historical context markers (Grenelle, 35h, euro, 2008 crisis…)
- Per-product detail modal with:
- Two-era comparison + year range slider
- Confidence bands for IPC-estimated products (±5%)
- Pre-1970 shading for higher-uncertainty estimates
- Inflection annotations (oil shock, Free Mobile, IRL spike…)
- Data quality badge (actual / IPC estimate / manual)
- Dynamic "Did you know?" fun fact that updates with the selected years
- Share button (copies fun fact + deep link encoding ref and year range)
- Download chart as PNG
- Product explorer: sparklines, search, category tabs, trend filter (↗ ↘ →)
- Shareable deep links:
/#/product/baguette?ref=median&from=1980&to=2024 - Embed mode:
?embed=1renders a stripped-down single-product card for iframes - Dynamic Insights: top 3 most improved and most degraded products, adapts to salary reference
- Methodology & Sources section with links to all data sources
- Bilingual FR/EN, light/dark mode
- Fully static - no backend required
| Layer | Libraries |
|---|---|
| Build | Vite 7 + TypeScript |
| UI | React 18, shadcn/ui, Tailwind CSS v3, Radix UI |
| Charts | Chart.js, chartjs-plugin-annotation, react-chartjs-2 |
| Routing | wouter (hash router) |
| Animation | Framer Motion |
| Testing | Vitest |
npm install
npm run dev # dev server on http://localhost:5000
npm run build # static output → dist/public/
npm run check # TypeScript type-check
npm run test # Vitest unit testsclient/
src/
components/
Header.tsx # Language / salary ref / theme toggles
Hero.tsx # Animated intro with featured product rotation
PurchasingPowerIndex.tsx # Main composite index chart
ProductExplorer.tsx # Sparkline grid with filters
ProductModal.tsx # Per-product detail chart + comparison
Insights.tsx # Dynamic top 3 improved / degraded
BasketComposition.tsx # Basket weights table
Sources.tsx # Methodology & sources cards
ui/ # shadcn/ui primitives + ErrorBoundary
lib/
data.ts # Re-export barrel (imports everything below)
salary-rates.ts # SMIC, mean, median hourly rates + DATA_START/END_YEAR
calculations.ts # interpolate(), computeMinutes()
macroeconomics.ts # inflationRates, productivityIndex, historicalEvents
products.ts # rawProducts, basketWeights, getDynamicFunFact()…
translations.ts # FR/EN strings
i18n.tsx # Language context
theme.tsx # Dark/light mode context
salaryRef.tsx # Salary reference context
chartColors.ts # Theme-aware palette helpers
constants.ts # EURO_TO_FRANC, MOBILE_BREAKPOINT
pages/
Home.tsx # Root layout, embed mode detection
scripts/
update-data.mjs # INSEE data fetcher + file updater
.github/
workflows/
update-data.yml # Annual GitHub Action (runs 1 Feb)
Salary and macroeconomic series are fetched automatically from INSEE. Product prices use three methods:
| Method | Description |
|---|---|
| A - Direct prices | INSEE "Prix moyens annuels de vente au détail" - actual retail prices in EUR |
| B - IPC estimate | IPC consumer price index (base 100 = 2015) anchored to a known 2015 price |
| C - IRL estimate | INSEE IRL rent revision index anchored to a known 2015 market rent |
| Manual | Maintained from public sources (tariff tables, press releases, surveys) |
The update script (
scripts/update-data.mjs) only adds new year entries - it never modifies historical values.
| Data | Method | Source / idbank |
|---|---|---|
| SMIC net hourly | Auto | INSEE 000879878 - January monthly net ÷ 151.67h |
| Mean salary net hourly | Auto | INSEE DADS 010752366 - annual net EQTP ÷ 1820h |
| Median salary net hourly | Auto | INSEE DADS 010752342 - annual net EQTP ÷ 1820h (from 1996) |
| CPI inflation | Auto | INSEE IPC 001759970 - annual average, YoY % change |
| Tomates, oranges, pommes | Auto - Method A | INSEE Prix moyens 000641464, 000641386, 000641388 |
| Baguette, essence, lait, bœuf, œufs, beurre, poulet, pommes de terre, sucre, pâtes, huile, camembert, vin, yaourt | Auto - Method B | INSEE IPC indices (see INDEX_PRICE_MAP in update-data.mjs) |
| Loyer national moyen | Auto - Method C | INSEE IRL 001515333 - anchor €12.0/m² in 2015 |
| Cigarettes | Manual | DGDDI / Tabac Info Service |
| Cinéma | Manual | CNC (Centre national du cinéma) |
| Médecin généraliste | Manual | Assurance Maladie / CNAM |
| Consultation spécialiste | Manual | DREES / SNDS (ophtalmologiste secteur 2) |
| Métro Paris | Manual | RATP / Île-de-France Mobilités |
| Timbre | Manual | La Poste (tarifs en vigueur) |
| Journal | Manual | Prix éditeur |
| Café | Manual | Enquête prix services INSEE |
| Électricité | Manual | EDF / CRE (tarifs réglementés) |
| Gaz | Manual | CRE / DGEC |
| Loyer Paris | Manual | OLAP Paris / CLAMEUR |
| Internet (box) | Manual | ARCEP / opérateurs |
| Forfait mobile | Manual | ARCEP / opérateurs |
| Streaming | Manual | Netflix France |
| Smartphone | Manual | GSMArena / Lesnumériques |
| Voiture milieu de gamme | Manual | Peugeot France (205 → 208) |
When the annual GitHub Action opens a PR, all Manual rows above must be verified. The update script prints a checklist at the end of its run:
MANUAL PRODUCTS - reviewer checklist
- [ ] cigarettes (last: 2024) - DGDDI / Tabac Info Service
- [ ] cinema (last: 2024) - CNC (Centre national du cinéma)
...
A GitHub Action runs every February 1st (or on demand) to fetch fresh INSEE data and open a PR for review.
Actions → Update Purchasing Power Data → Run workflow
# Dry-run - prints what would change without writing any files
node scripts/update-data.mjs
# Write mode - updates the three data files
node scripts/update-data.mjs --writeIn write mode, the script updates:
client/src/lib/salary-rates.ts- SMIC, mean, and median ratesclient/src/lib/macroeconomics.ts- annual CPI inflation ratesclient/src/lib/products.ts- product prices (Methods A, B, C)
- Find the INSEE idbank:
- Direct prices: Prix moyens annuels
- IPC indices: IPC series browser
- Add an entry to
DIRECT_PRICE_MAPorINDEX_PRICE_MAPinscripts/update-data.mjs - Add the product to
rawProductsinclient/src/lib/products.tswith historical anchor prices - Run
node scripts/update-data.mjs --writeto populate recent years
npm run test14 unit tests covering:
interpolate()edge casescomputeMinutes()for each salary reference- Basket / composite purchasing power index computation
- Data integrity (no missing years, no negative prices, valid salary rates)
CC BY 4.0 - attribution required: credit Romain Coupey and link to this repository.