when-am-i / commit
docs: generated README badges + MIT license
.gitlab-ci.yml | 3 ++ LICENSE | 21 +++++++++++++ README.md | 29 ++++++++++++++++- package.json | 2 ++ scripts/gen-badges.mts | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 138 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 838ec31..5767c1f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,6 +14,9 @@ test: - npm run lint - npm run typecheck - npm test + # Fail if the README badge block is out of sync with the repo (run + # `npm run gen:badges` and commit the result to fix). + - npm run gen:badges -- --check create-pages: stage: deploy diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..88e06c2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Pavel Safronov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index e92fb0d..ad9dad3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,19 @@ # 🚀 [Live site](https://pavelsafronov.gitlab.io/what-year-is-it) +<!-- badges:start --> +<!-- generated by `npm run gen:badges` — do not edit by hand --> +[](https://gitlab.com/PavelSafronov/what-year-is-it/-/pipelines) +[](https://pavelsafronov.gitlab.io/what-year-is-it) + + + + + + + + +<!-- badges:end --> + ## What Year Is It? Imagine you're a time traveler who ends up in a random place and time in history. What does a local say when you ask, "What year is it?" This project is a fun and educational React app that lets you explore different calendar systems around the world by selecting a location on an interactive map and choosing any year in history. It then shows you the local calendar year and system for that place and time. @@ -49,6 +63,17 @@ npm test # Vitest unit tests npm run test:watch # Vitest in watch mode ``` +### Badges + +The README badge block (between the `badges:start`/`badges:end` markers above) is +generated, not hand-edited. It pulls the app version from `package.json` and counts +the calendar data files, so the badges never drift. Run it before opening a PR: + +```bash +npm run gen:badges # rewrite the badge block in README.md +npm run gen:badges -- --check # fail if the block is stale (no write) — for CI +``` + ### Screenshots Screenshots are generated with Playwright, which forces deterministic app state @@ -144,11 +169,13 @@ See `src/data/calendars/SCHEMA.json` for the schema and field documentation. - Run `npm run screenshot:calendar -- src/data/calendars/<your-file>.json` and attach the images to your PR, so the region, eras, and conversions can be reviewed visually. +- Run `npm run gen:badges` so the README's calendar-data count reflects your + addition, and commit the updated README. - The build process will validate and compile all data for use in the app. ## License -MIT +[MIT](LICENSE) © Pavel Safronov ## Attributions diff --git a/package.json b/package.json index a7c8918..72bfccb 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "private": true, "type": "module", "description": "A React SPA to explore what year it was in different places and times.", + "license": "MIT", "scripts": { "dev": "vite --open", "build": "vite build", @@ -14,6 +15,7 @@ "test:watch": "vitest", "screenshot": "tsx scripts/generate-screenshots.mts", "screenshot:calendar": "tsx scripts/screenshot-calendar.mts", + "gen:badges": "tsx scripts/gen-badges.mts", "gen:favicon": "sips -z 64 64 calendar-source.png --out src/assets/favicon.png", "copy:leaflet-icons": "mkdir -p public && cp node_modules/leaflet/dist/images/marker-icon.png public/ && cp node_modules/leaflet/dist/images/marker-icon-2x.png public/ && cp node_modules/leaflet/dist/images/marker-shadow.png public/", "postinstall": "npm run copy:leaflet-icons" diff --git a/scripts/gen-badges.mts b/scripts/gen-badges.mts new file mode 100644 index 0000000..49dfe87 --- /dev/null +++ b/scripts/gen-badges.mts @@ -0,0 +1,84 @@ +// Generate the README badge block from real project data, so the badges never +// drift out of sync with the repo. Run before opening a PR: +// +// npm run gen:badges # rewrite the badge block in README.md +// npm run gen:badges -- --check # exit non-zero if it's stale (CI / pre-PR gate) +// +// The block lives between the <!-- badges:start --> and <!-- badges:end --> +// markers in README.md. Computed values (app version, number of calendar data +// files) come from disk; the static badges (pipeline, demo, tech stack) are +// emitted here too so the whole row has a single source of truth. + +import { readFile, writeFile, readdir } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const README = path.join(REPO_ROOT, 'README.md'); + +const GITLAB_PROJECT = 'PavelSafronov/what-year-is-it'; +const PAGES_URL = 'https://pavelsafronov.gitlab.io/what-year-is-it'; +const START = '<!-- badges:start -->'; +const END = '<!-- badges:end -->'; +const PREAMBLE = '<!-- generated by `npm run gen:badges` — do not edit by hand -->'; + +/** A shields.io static badge URL: label-message-color, each part encoded. */ +const shield = (label: string, message: string, color: string) => + `https://img.shields.io/badge/${encodeURIComponent(label)}-${encodeURIComponent(message)}-${color}`; + +/** Number of calendar data files (everything under calendars/ except the schema). */ +async function countCalendars(): Promise<number> { + const files = await readdir(path.join(REPO_ROOT, 'src/data/calendars')); + return files.filter((f) => f.endsWith('.json') && !f.startsWith('SCHEMA')).length; +} + +async function buildBlock(): Promise<string> { + const pkg = JSON.parse(await readFile(path.join(REPO_ROOT, 'package.json'), 'utf-8')) as { version: string }; + const calendars = await countCalendars(); + + return [ + PREAMBLE, + `[](https://gitlab.com/${GITLAB_PROJECT}/-/pipelines)`, + `[})](${PAGES_URL})`, + `})`, + `, 'orange')})`, + `})`, + '', + '', + '', + '', + '', + ].join('\n'); +} + +async function main() { + const check = process.argv.includes('--check'); + const readme = await readFile(README, 'utf-8'); + + const startIdx = readme.indexOf(START); + const endIdx = readme.indexOf(END); + if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) { + throw new Error(`README.md is missing the ${START} … ${END} markers`); + } + + const next = + readme.slice(0, startIdx + START.length) + + '\n' + await buildBlock() + '\n' + + readme.slice(endIdx); + + if (next === readme) { + console.log('✅ README badges already up to date'); + return; + } + if (check) { + console.error('❌ README badges are stale — run `npm run gen:badges` and commit the result'); + process.exit(1); + } + await writeFile(README, next); + console.log('✨ Updated README badges'); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +});