feat(data): make the site correct for 2027+ seasons - #2
Open
Persblack wants to merge 2 commits into
Open
Conversation
The only script was `astro build`, which does not typecheck — `tsc --noEmit` was already failing on three pre-existing errors nobody could see. Adds typecheck, vitest and a combined `check` script so date and filter logic has a gate that fails instead of a page that looks plausible.
The site assumed one year everywhere, so a second season would not have
failed — it would have quietly lied. Calendar grouped by month index alone
(July 2026 and July 2027 merging into one section labelled "July 2026"), the
analytics histogram summed years into the same bars, the month slider matched
every year with no way to scope one, and five copy strings hardcoded 2026.
Festival dates were parsed with `new Date("2026-07-03")` — UTC midnight read
back through local getters, which shifts the day for every visitor west of
Greenwich and mis-buckets month-boundary festivals.
Rolling window is now the model: the default view is everything from today
forward whatever year it lands in, with year chips to narrow. Adding a season
is a file drop — `src/data/festivals*.json` is globbed, merged and
de-duplicated by id, so 2028 needs no code change.
Data layer
- `lib/festival-normalize.ts`: pure validator. Drops only rows that cannot be
identified or placed in time (no id/name, unparseable start date, duplicate
id) and repairs the rest, reporting every repair at build time. Fails the
build past a 5% drop ratio so a gutted scrape cannot ship as a stub site.
Today's data reports 47 unusable website URLs and 28 missing coordinates.
- `lib/festival-data.ts`: build-time-only loading seam (never imported from an
island, which would bundle the dataset). Year is derived from `start_date`,
never trusted from the file.
- `lib/dates.ts`: local-midnight parsing, year-qualified `YYYY-MM` bucket keys,
cross-year date ranges ("Dec 30, 2027 - Jan 2, 2028").
- `Festival.latitude/longitude/website` are now nullable, because the data
already was; map, maps link, share sheet and recommendations guard instead of
rendering `null,null`.
Views
- `lib/filters.ts`: one filter/sort/group implementation, replacing four
divergent copies of the same predicate chain, plus a prebuilt lowercase
search index instead of lowercasing ~8k lineup strings per keystroke.
- Season chips in FilterBar; calendar sections and jump navigation carry the
real year; analytics draws one series per season.
- All year-specific copy derives from the data, and the footer states dataset
size, span and freshness.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The site assumed a single festival year everywhere. A 2027 dataset would not have failed — it would have quietly lied: the calendar grouped by month index alone, so July 2026 and July 2027 merged into one section labelled July 2026; the analytics histogram summed both years into the same bars; the month slider matched every year with no way to scope one; five copy strings hardcoded 2026. Independently, every festival date went through
new Date("2026-07-03")— UTC midnight read back with local getters, which shifts the day for anyone west of Greenwich and mis-buckets month-boundary festivals.tsc --noEmitwas already red on three errors, because no script ran it.Decision (agreed up front): rolling window — the default view is everything from today forward, whatever year it lands in, with year chips to narrow. Adding a season is a file drop;
src/data/festivals*.jsonis globbed, merged and de-duplicated by id, so 2028 needs no code change.What changed
Data layer (new)
lib/festival-normalize.ts— pure validator. Drops only rows that cannot be identified or placed in time (missing id/name, unparseable start date, duplicate id); repairs everything else and reports each repair at build time. Fails the build above a 5% drop ratio, so a gutted scrape cannot ship as a stub site.lib/festival-data.ts— build-time-only loading seam (never imported from a React island, which would bundle the 1.1 MB dataset).yearis derived fromstart_date, never trusted from the file.lib/dates.ts— local-midnight parsing, year-qualifiedYYYY-MMbucket keys, cross-year ranges (Dec 30, 2027 - Jan 2, 2028), rejects non-dates instead of producingInvalid Date.lib/guards.ts— canonicalisRecord/hasCoordinates.Festival.latitude/longitude/websiteare nullable, because the data already was (28 rows without coordinates, 47 without a usable URL). Map, Google Maps link, share sheet and recommendations now guard instead of renderingnull,null.Views
lib/filters.ts— one filter/sort/group implementation replacing four divergent copies of the same predicate chain; prebuilt lowercase search index instead of lowercasing ~8k lineup strings on every keystroke.FilterBar; calendar sections, jump navigation and planner timeline markers carry the real year; analytics draws one<Area>per season.Gates
npm run typecheck,npm run test,npm run check; 63 unit tests over dates, filters and the validator. Tests do not read the gitignored dataset.Dependencies
Dev-only:
vitest,@types/node. No env vars, no migrations.Review checklist
anyat the Leaflet/recharts boundaries left untouched (out of scope)Evidence
Verified against a synthetic second season (150 shifted rows + 7 deliberate defects) dropped into
src/data/festivals_2027.json:Browser-verified (headless Chromium, 1440x1000) with that two-season dataset:
June 2026 … December 2026 / March 2027 … December 2027sections, jump navJun '26 … Dec '27, header badge2026-2027 Festivals, footer1071 festivals · 2026-2027 · Data updated 14 Feb 2027All years / 2026 / 2027; clicking 2027 narrows 266 → 155 and every section header is 2027; active chiprgb(59,130,246)vs mutedrgb(37,37,37)<Area>series with a 2026/2027 legend11 festivals without coordinates aren't shown, 236 markersJul '26 … Dec '27, Jan '28markers — no duplicate bareJanDec 30, 2027 - Jan 2, 2028Invalid Date/NaNon any of the 7 routesWith the synthetic file removed, single-year output is unchanged: no season chips, bare month names in the jump nav, one chart series,
2026 Festivalsbadge.