Vacation bridge calculator — find the optimal combination of PTO days to maximize consecutive time off by leveraging public holidays and weekends.
- Astro (SSG) as the base framework
- React for the interactive calculator island
- Tailwind CSS for styling
- TypeScript throughout, strict mode
- No backend, no database — holiday data ships as static JSON in
src/lib/holidays/ - Vitest for unit tests
pnpm install
pnpm dev # start the dev server
pnpm lint # eslint --fix
pnpm test # vitest run
pnpm build # astro check && astro buildNode version is pinned in .nvmrc (22). Package manager is pnpm, pinned via the packageManager field in package.json.
src/pages/index.astro— main page, renders the calculator islandsrc/components/Calculator.tsx— the interactive React island (client:load)src/lib/holidays/— static JSON holiday data, one file per country/year (es-2026.json);registry.tsresolves a region into a merged, rankedHoliday[]src/lib/optimizer.ts— pure TypeScript module with the calendar/bridge-finding logicsrc/lib/optimizer.spec.ts— unit tests for the optimizer (Vitest)
buildCalendar(year, holidays)builds a day-by-day calendar for a given year, tagging each day asholiday,weekend, orworkdayfrom the resolved holiday list.findBridges(calendar, availableDays)scans the calendar for every maximal run of consecutive workdays no longer thanavailableDays, then expands each run outward over the adjacent holidays/weekends to get the full stretch of time off it buys (ptoUsed,totalDaysOff,efficiency = totalDaysOff / ptoUsed). Candidates are ranked by efficiency descending, thentotalDaysOffdescending.combineBridges(candidates, availableDays)greedily picks non-overlapping candidates in that ranked order, up to theavailableDaysbudget — not a perfect knapsack solve, per the MVP spec.
The Calculator island wires this up: a region/year/available-days form drives the calculation, the ranked list shows individual bridges plus the greedy combined plan, and a month-grid calendar highlights holidays, weekends, and the suggested PTO days for the top result.
Holiday data (national + all 17 autonomous communities, 2026) lives in a single src/lib/holidays/es-2026.json: national holidays are listed once, and each entry under regions only carries the delta it adds on top — its own regional days, plus substitute days for a national holiday that region observes (moving a holiday off a Sunday is a per-region decision, not a national one). registry.ts merges national + regions[slug].holidays into the flat, sorted Holiday[] the rest of the app consumes. This mirrors how the date-holidays npm package — used to generate this data rather than typing it by hand — structures its own per-country data file. Cross-checked against several regions (Madrid, Cataluña, País Vasco, Galicia) against known official dates, but still verify against the official BOE calendar before relying on this data in production. Local/municipal holidays are explicitly out of scope.
pre-commitrunslint-staged(Prettier + ESLint--fixon staged.ts/.tsx/.astrofiles).pre-pushrunspnpm build && pnpm test:changed.
Hooks are installed automatically via the prepare script on pnpm install. Set HUSKY=0 to skip hook installation (e.g. in CI).
.github/workflows/ci.yml— lint, test, and build on every pull request via the sharedsisques-labs/workflowsnode-ci.ymlreusable workflow..github/workflows/codeql.yml— CodeQL analysis on push todevelop/staging/main, on pull requests, and weekly..github/workflows/docker.yml— Docker smoke build (multi-arch, no push) plus a blocking Trivy scan on every pull request..github/workflows/pr-labeler.yml— labels pull requests by changed files, per.github/labeler.yml.
The app is a static build served by nginx. Build and run locally:
docker build -t daysoff .
docker run -p 8080:8080 daysoff.github/workflows/pages.yml builds and deploys the site to GitHub Pages on every push to main (only main — develop/staging don't deploy). It publishes to https://sisques-labs.github.io/daysoff/, which is why astro.config.mjs sets site/base to that project-pages subpath — changing the deploy target (e.g. a custom domain) means updating those too.
This workflow needs GitHub Pages enabled once, manually, in the repo: Settings → Pages → Build and deployment → Source: "GitHub Actions". There's no API available in this session to do that step — the workflow will fail until it's set.
.github/workflows/release-train.yml runs on every push to develop, staging, and main. It detects integrated conventional-commit changes, bumps the version, builds and publishes the Docker image (sisqueslabs/daysoff on Docker Hub, ghcr.io/sisques-labs/daysoff on GHCR), and generates CHANGELOG.md/GitHub Releases via cliff.toml. develop and staging publish alpha/beta pre-releases; main publishes stable releases and syncs back into develop.