feat(rezepte)!: liturgical-aware seasonality via date ranges
CI / update (push) Successful in 3m31s

Replace season: number[] (months 1-12) on Recipe with seasonRanges, a
list of date ranges where each endpoint is either a fixed MM-DD or a
movable liturgical anchor (Easter, Ash Wednesday, Palm Sunday,
Pentecost, Advent I) plus a day offset. The old month list couldn't
express liturgical seasons whose boundaries shift each year (Advent,
Lent, Easter Octave, Christmas Octave) nor sub-month windows.

The shared evaluator resolves anchors against [Y-1, Y, Y+1] so spans
that wrap the calendar year boundary (e.g. christmas + 0 to
christmas + 7) match correctly on both sides. SeasonSelect was
rewritten as a controlled bind:ranges editor with a
fixed/liturgical kind toggle, anchor + offset inputs, per-row
resolved-this-year preview, and preset chips.

Run the one-time migration before deploying:
  pnpm exec vite-node scripts/migrate-season-to-ranges.ts

It coalesces contiguous month runs into single fixed ranges and
merges Dec/Jan wrap into one wrapping range; the new code does not
read the legacy season field, so order matters.
This commit is contained in:
2026-05-02 17:53:27 +02:00
parent 68b078c146
commit 096d6e2868
38 changed files with 692 additions and 295 deletions
+13 -13
View File
@@ -1,7 +1,8 @@
import type { BriefRecipeType, RecipeModelType } from '$types/types';
import { isRecipeInSeason, recipeOverlapsMonth } from '$lib/js/seasonRange';
const DB_NAME = 'bocken-recipes';
const DB_VERSION = 2; // Bumped to force recreation of stores
const DB_VERSION = 3; // v3: dropped multi-entry season index after migration to seasonRanges
const STORE_BRIEF = 'recipes_brief';
const STORE_FULL = 'recipes_full';
@@ -51,10 +52,12 @@ function openDB(): Promise<IDBDatabase> {
db.deleteObjectStore(STORE_META);
}
// Brief recipes store - keyed by short_name for quick lookups
// Brief recipes store - keyed by short_name for quick lookups.
// Season membership is now driven by date ranges with movable
// liturgical anchors that can't be expressed as a static index, so
// season filtering loads all rows and runs the shared evaluator.
const briefStore = db.createObjectStore(STORE_BRIEF, { keyPath: 'short_name' });
briefStore.createIndex('category', 'category', { unique: false });
briefStore.createIndex('season', 'season', { unique: false, multiEntry: true });
// Full recipes store - keyed by short_name
db.createObjectStore(STORE_FULL, { keyPath: 'short_name' });
@@ -104,17 +107,14 @@ export async function getBriefRecipesByCategory(category: string): Promise<Brief
});
}
export async function getBriefRecipesBySeason(month: number): Promise<BriefRecipeType[]> {
const db = await openDB();
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_BRIEF, 'readonly');
const store = tx.objectStore(STORE_BRIEF);
const index = store.index('season');
const request = index.getAll(month);
export async function getBriefRecipesInSeasonOn(date: Date = new Date()): Promise<BriefRecipeType[]> {
const all = await getAllBriefRecipes();
return all.filter(r => isRecipeInSeason(r as any, date));
}
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
export async function getBriefRecipesOverlappingMonth(month: number, year: number = new Date().getFullYear()): Promise<BriefRecipeType[]> {
const all = await getAllBriefRecipes();
return all.filter(r => recipeOverlapsMonth(r as any, month, year));
}
export async function getBriefRecipesByTag(tag: string): Promise<BriefRecipeType[]> {
+3 -1
View File
@@ -241,11 +241,13 @@ async function precacheRecipeData(recipes: BriefRecipeType[]): Promise<void> {
dataUrls.push(`/recipes/icon/${encodeURIComponent(icon)}/__data.json`);
}
// Add season subroute data (all 12 months)
// Add season subroute data (all 12 months + today's liturgical-aware view)
for (let month = 1; month <= 12; month++) {
dataUrls.push(`/rezepte/season/${month}/__data.json`);
dataUrls.push(`/recipes/season/${month}/__data.json`);
}
dataUrls.push(`/rezepte/season/__data.json`);
dataUrls.push(`/recipes/season/__data.json`);
// Send message to service worker to cache these URLs
if (dataUrls.length > 0) {