Skip to content

Commit 76d6527

Browse files
authored
feat: kuratierte externe Kalender (Kult 41) + clickable Freund:innen-Logos (#14)
* feat(events): kuratierte externe Kalender + UID-Identity Neuer Quell-Typ ics-filtered in calendars.json mit categoryAllow/categoryDeny und titleAllow/titleDeny. Erste Anwendung: Kult 41 (Hauptkalender events.ics, Konzerte gefiltert, cap 15). Zweiter Typ ics-single bleibt vorbereitet für künftige Single-Event-ICS-URLs. Parser-Erweiterungen: extrahiert UID, URL, TZID. Non-Europe/Berlin TZIDs werden geloggt (warn-once) statt umgerechnet — bisheriges Verhalten bleibt. firstSeen und RSS-GUID nutzen jetzt UID statt date|summary, mit date|title-Fallback für Migration von alten events-data.json-Einträgen. events.js: bei externer eventUrl wird direkt verlinkt statt /timeGridDay/<datum> anzuhängen — sonst 404 auf kult41.de. Tests: Inline-Funktionskopien rausgezogen, Tests importieren echte Symbole aus sync-events.mjs. 12 neue Tests (applyFilter, UID/URL-Parsing, TZID-Warn, eventUrl-Fallback, cap, calTags). 55/55 pass. * feat(homepage): clickable Freund:innen-Logos via logos.json mapping Bestehender Logo-Slider rendert <img> ohne Link. Neue images/logo-slider/logos.json maps Dateinamen → display name + optionale URL. Wenn URL gesetzt: Logo wird in <a target=_blank rel=noopener> gewrappt mit aria-label "<Name> (externer Link)". Ohne URL: bisheriges <img>-only Verhalten (backward compat). Der zweite slider__set (aria-hidden Duplikat für nahtloses Scrolling) bekommt tabindex=-1, sonst doppelte Tab-Stops. CSS: .logo-slider__link nutzt display:contents damit das Layout (.logo-slider__item img Selektor) greift, plus focus-visible outline auf dem img. Erste verlinkte Logos: FrOSCon (froscon.org), FSFE (fsfe.org), Kult 41 (kult41.de/veranstaltungen/programm). Die übrigen vier Einträge in logos.json haben url:"" als Platzhalter zum Nachtragen. Kult 41 Logo-Datei muss noch nach images/logo-slider/kult41.png — der Eintrag in logos.json ist vorbereitet, npm run build:logos zieht ihn automatisch ein sobald die Datei da ist. * feat(homepage): add Kult 41 logo to Freund:innen slider * feat(events): switch Kult 41 to single curated event Statt Master-Feed mit Konzert-Filter (15 Events) ziehen wir aktuell nur das eine thematisch passende Event: Theater Tumult: K.I. und Abel + Reggae. Der ics-single-Typ war genau dafür vorbereitet — pro Event ein JSON-Block mit der einzelnen ICS-URL des Plugins (wp-events-plugin/Events Manager). Output: 1 VEVENT → 1 card. Link geht direkt zur kult41.de-Event-Seite. Weitere Events können einfach durch zusätzliche ics-single-Einträge ergänzt werden (jeweils mit unique id). * refactor(events): split curated externals into calendars/external/*.json Pro kuratiertem externen Event/Source eine eigene File statt eines wachsenden Arrays in calendars.json. Schöneres git-diff bei Add/Remove, klare 1:1-Sicht "Datei = Quelle". calendars.json bleibt für die stabilen primary feeds (bitcircus + datenburg). calendars/external/*.json ergänzt — jede Datei = ein Source-Objekt im selben Format wie ein calendars.json-Eintrag. sync-events.mjs mergt beides über neuen loadCalendars()-Helper, der defekte/unvollständige Files mit Warnung überspringt statt das ganze Sync zu brechen. Workflow zieht jetzt auch calendars/ aus main, sonst fehlen externals auf dem live-Branch. Erstes externes File: Theater Tumult bei Kult 41. Docs in CLAUDE.md und README beschreiben die neue Convention plus die source types (ics-full/ics-single/ics-filtered). * refactor(events): one-file-per-source under calendars/ with config manifest calendars.json (root) → weg. Stattdessen lebt jede Quelle in einer eigenen JSON-Datei unter calendars/. calendars/config.json ist das Manifest, das auflistet welche Sources verarbeitet werden und in welcher Reihenfolge. Eintrag rausnehmen = Source deaktivieren, ohne die Datei zu löschen. Konsistent für stable primary feeds (bitcircus, datenburg) UND kuratierte externals (calendars/external/*) — alles über das gleiche Pattern. loadCalendars() liest jetzt das Manifest und lädt die referenzierten Files. Fehlende oder defekte Files werden mit Warning übersprungen, das Sync läuft weiter (eine kaputte Source kann nicht alles brechen). Workflow zieht nur noch calendars/ aus main. Docs in CLAUDE.md + README zeigen die neue Struktur und Tabelle der aktiven Sources.
1 parent b68b781 commit 76d6527

16 files changed

Lines changed: 593 additions & 317 deletions

.github/workflows/sync-events.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
- name: Use scripts and config from main
2525
run: |
2626
git fetch origin main
27-
git checkout origin/main -- scripts/ calendars.json
27+
git checkout origin/main -- scripts/ calendars/
2828
2929
- uses: actions/setup-node@v6
3030
with:

CLAUDE.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,23 @@ These are generated by CI. Seed values exist on `main` (for local dev / E2E test
117117

118118
## Adding a calendar source
119119

120-
Add an entry to `calendars.json` — no code changes needed.
120+
Every source lives in its own JSON file under `calendars/`. Manifest `calendars/config.json` lists which sources to process and in what order. Adding a new source = create the JSON file, list its relative path in `config.json`. Removing = remove the line (or delete the file).
121+
122+
```
123+
calendars/
124+
config.json ← manifest, lists active sources
125+
bitcircus.json ← stable primary feed
126+
datenburg.json
127+
external/
128+
kult41-theater-tumult-…json ← curated external entries
129+
```
130+
131+
Source `type`s:
132+
- (default) `ics-full` — pull the whole calendar
133+
- `ics-single` — single curated event ICS URL (e.g. `https://kult41.de/events/foo/ical/`)
134+
- `ics-filtered` — full calendar with `filter.categoryAllow` / `categoryDeny` / `titleAllow` / `titleDeny` lists
135+
136+
Each source can also set `tags` (always-added hashtags), `cap` (per-source slot override), `eventUrl` (fallback link when ICS lacks `URL`). Sources without `id`/`ics` are skipped with a warning.
121137

122138
## Adding a new page
123139

README.md

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ style.css Global styles (terminal theme, dark bg, green accent
3333
main.js Frontend: nav, carousel, map, events preview, footer
3434
3535
events.js Frontend: fetches events-data.json, renders event cards, tag filtering
36-
calendars.json Calendar source definitions (ICS URLs, names, flags)
36+
calendars/ Calendar source definitions (config.json manifest + one file per source)
3737
funding.json Current funding percentage (generated by CI)
3838
3939
scripts/
@@ -93,7 +93,7 @@ Nothing reaches production without passing all tests first.
9393
- **Branch:** Checks out `live`, pulls scripts/config from `main`
9494
- **What it does:**
9595
1. Runs `scripts/sync-events.mjs` (Node 22, zero dependencies)
96-
2. Fetches ICS feeds from all calendars defined in `calendars.json`
96+
2. Fetches ICS feeds from all sources listed in `calendars/config.json`
9797
3. Parses VEVENT entries, expands RRULE recurrences (weekly, monthly with BYDAY/BYSETPOS)
9898
4. Filters out internal/blocker events and past events
9999
5. Generates `events-data.json` with `lastSync` timestamp (max 40 cards, 120-day horizon)
@@ -131,14 +131,51 @@ Releases are **decoupled from deploys** — every merge to `main` deploys automa
131131

132132
## Calendar system
133133

134-
### Data sources (`calendars.json`)
134+
### Data sources
135135

136-
| ID | Name | Nextcloud instance | Primary | In RSS |
137-
|----|------|--------------------|---------|--------|
138-
| `bitcircus` | bitcircus101 | nc.6bm.de | yes | yes |
139-
| `datenburg` | Datenburg e.V. | cloud.datenb.org | no | no |
136+
Every source has its own JSON file. `calendars/config.json` is the manifest that lists which sources are active and in what order:
140137

141-
To add a new calendar, add an entry to `calendars.json` — no code changes needed.
138+
```
139+
calendars/
140+
config.json manifest
141+
bitcircus.json stable primary feed
142+
datenburg.json
143+
external/
144+
kult41-theater-tumult-k-i-abel.json curated externals
145+
```
146+
147+
Active sources right now:
148+
149+
| ID | Name | Type | Primary | In RSS |
150+
|----|------|------|---------|--------|
151+
| `bitcircus` | bitcircus101 | ics-full | yes | yes |
152+
| `datenburg` | Datenburg e.V. | ics-full | no | no |
153+
| `kult41-theater-tumult-k-i-abel` | Kult 41 | ics-single | no | no |
154+
155+
To add a calendar: create the source JSON in `calendars/` (or `calendars/external/`) and add its path to `sources` in `config.json`. To temporarily disable: remove the path from `sources` — the file stays put.
156+
157+
#### Source `type`s
158+
159+
| `type` | Use | Filter |
160+
|--------|-----|--------|
161+
| `ics-full` (default) | Pull entire calendar ||
162+
| `ics-single` | Single curated event ICS URL ||
163+
| `ics-filtered` | Full calendar, narrowed by lists | `categoryAllow`, `categoryDeny`, `titleAllow`, `titleDeny` |
164+
165+
External sources also accept `tags` (always-added hashtags), `cap` (per-source slot override), and `eventUrl` (overrides the per-event link when ICS has no `URL` field).
166+
167+
Example `calendars/external/kult41-theater-tumult-k-i-abel.json`:
168+
169+
```json
170+
{
171+
"id": "kult41-theater-tumult-k-i-abel",
172+
"name": "Kult 41",
173+
"type": "ics-single",
174+
"ics": "https://kult41.de/events/theater-tumult-k-i-und-abel-reggae-2/ical/",
175+
"url": "https://kult41.de/veranstaltungen/programm",
176+
"tags": ["#kult41", "#theater"]
177+
}
178+
```
142179

143180
### How events get tagged
144181

calendars.json

Lines changed: 0 additions & 18 deletions
This file was deleted.

calendars/bitcircus.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "bitcircus",
3+
"name": "bitcircus101",
4+
"ics": "https://nc.6bm.de/remote.php/dav/public-calendars/DCaFSYECrcTJRJjC?export",
5+
"url": "https://nc.6bm.de/apps/calendar/p/DCaFSYECrcTJRJjC",
6+
"primary": true,
7+
"rss": true
8+
}

calendars/config.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"_comment": "Steuert welche Quellen sync-events.mjs verarbeitet und in welcher Reihenfolge. Pfade sind relativ zu diesem Verzeichnis (calendars/). Eintrag rausnehmen = Source deaktivieren ohne die Datei zu löschen.",
3+
"sources": [
4+
"bitcircus.json",
5+
"datenburg.json",
6+
"external/kult41-theater-tumult-k-i-abel.json"
7+
]
8+
}

calendars/datenburg.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "datenburg",
3+
"name": "Datenburg e.V.",
4+
"ics": "https://cloud.datenb.org/remote.php/dav/public-calendars/QLmHENYXAXECozbt/?export",
5+
"url": "https://cloud.datenb.org/apps/calendar/p/QLmHENYXAXECozbt",
6+
"primary": false,
7+
"rss": false
8+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"_note": "Theater-Performance bei Kult 41. Passt thematisch zum Hackspace-Profil (gegenkulturelle Veranstaltung). Single-Event-ICS aus dem wp-events-plugin der kult41.de-Seite.",
3+
"id": "kult41-theater-tumult-k-i-abel",
4+
"name": "Kult 41",
5+
"type": "ics-single",
6+
"ics": "https://kult41.de/events/theater-tumult-k-i-und-abel-reggae-2/ical/",
7+
"url": "https://kult41.de/veranstaltungen/programm",
8+
"primary": false,
9+
"rss": false,
10+
"tags": ["#kult41", "#theater"]
11+
}

events.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -301,15 +301,21 @@
301301
}
302302
html += "</div>";
303303
// Action pills
304-
var calUrl = e.calendarUrl || CALENDAR_URL;
304+
// External event URLs (e.g. Kult 41) link directly; Nextcloud calendar URLs
305+
// get the timeGridDay suffix for day view
306+
var calHref = e.eventUrl
307+
? e.eventUrl
308+
: (e.calendarUrl || CALENDAR_URL) + '/timeGridDay/' + e.date;
309+
var calLabel = e.eventUrl ? '\u2192 link' : '\u2192 kalender';
310+
var calTitle = e.eventUrl ? 'Auf externer Seite \u00f6ffnen' : 'Im Kalender anzeigen';
305311
html += '<div class="event-card__actions">';
306312
html += '<button class="event-action event-action--link" ' +
307313
'data-href="#' + anchor + '" title="Link kopieren">' +
308314
'\u2190 link</button>';
309315
html += '<a class="event-action event-action--cal" href="' +
310-
calUrl + '/timeGridDay/' + e.date +
311-
'" target="_blank" rel="noopener" title="Im Kalender anzeigen">' +
312-
'\u2192 kalender</a>';
316+
calHref +
317+
'" target="_blank" rel="noopener" title="' + calTitle + '">' +
318+
calLabel + '</a>';
313319
if (e.location) {
314320
var osmQuery = encodeURIComponent(e.location);
315321
html += '<a class="event-action event-action--map" href="' +

images/logo-slider/kult41.png

57.6 KB
Loading

0 commit comments

Comments
 (0)