diff --git a/.editorconfig b/.editorconfig
index 4e8e071..6e5b9f9 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -9,11 +9,6 @@ charset = utf-8
end_of_line = lf
insert_final_newline = true
-[*.py]
-indent_style = space
-indent_size = 4
-
-[*.yaml]
+[*.{js,yaml}]
indent_style = space
indent_size = 2
-
diff --git a/.gitattributes b/.gitattributes
deleted file mode 100644
index ae018fa..0000000
--- a/.gitattributes
+++ /dev/null
@@ -1 +0,0 @@
-*.ics text eol=crlf
diff --git a/.gitignore b/.gitignore
index 18286ce..008e122 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,9 @@
-static/all.ics
+# Created by https://www.toptal.com/developers/gitignore/api/windows,linux,macos,hugo,pycharm+all,vim,direnv
+# Edit at https://www.toptal.com/developers/gitignore?templates=windows,linux,macos,hugo,pycharm+all,vim,direnv
+
+### direnv ###
+.direnv
.envrc
-*.swp
-# Created by https://www.toptal.com/developers/gitignore/api/windows,linux,macos,hugo
-# Edit at https://www.toptal.com/developers/gitignore?templates=windows,linux,macos,hugo
### Hugo ###
# Generated files by hugo
@@ -41,8 +42,7 @@ hugo.linux
.LSOverride
# Icon must end with two \r
-Icon
-
+Icon
# Thumbnails
._*
@@ -67,6 +67,114 @@ Temporary Items
# iCloud generated files
*.icloud
+### PyCharm+all ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# AWS User-specific
+.idea/**/aws.xml
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# SonarLint plugin
+.idea/sonarlint/
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### PyCharm+all Patch ###
+# Ignore everything but code style settings and run configurations
+# that are supposed to be shared within teams.
+
+.idea/*
+
+!.idea/codeStyles
+!.idea/runConfigurations
+
+### Vim ###
+# Swap
+[._]*.s[a-v][a-z]
+!*.svg # comment out if you don't need vector files
+[._]*.sw[a-p]
+[._]s[a-rt-v][a-z]
+[._]ss[a-gi-z]
+[._]sw[a-p]
+
+# Session
+Session.vim
+Sessionx.vim
+
+# Temporary
+.netrwhist
+# Auto-generated tag files
+tags
+# Persistent undo
+[._]*.un~
+
### Windows ###
# Windows thumbnail cache files
Thumbs.db
@@ -93,17 +201,4 @@ $RECYCLE.BIN/
# Windows shortcuts
*.lnk
-# End of https://www.toptal.com/developers/gitignore/api/windows,linux,macos,hugo
-
-# NixOS:
-.envrc
-shell.nix
-.direnv
-
-# Python
-.venv
-
-# murmeldin
-overview.md
-
-/result
+# End of https://www.toptal.com/developers/gitignore/api/windows,linux,macos,hugo,pycharm+all,vim,direnv
diff --git a/.python-version b/.python-version
deleted file mode 100644
index 2c07333..0000000
--- a/.python-version
+++ /dev/null
@@ -1 +0,0 @@
-3.11
diff --git a/README.md b/README.md
index 3b0af0f..5f72bd0 100644
--- a/README.md
+++ b/README.md
@@ -27,24 +27,20 @@ This is the website of the CCCB.
5. Point your browser to:
-Every change you make on the project will be reflected in your browser as long
-as `hugo serve` is running.
+Every change you make on the project will be reflected in your browser as long as `hugo serve` is running.
-The *"Nächste Veranstaltungen"* table on the home page is generated by
-post-processing in `./build.sh`, not by Hugo, so it is **not** visible under
-`hugo serve`. To preview the fully-built site (including the home-page
-calendar), or to ready the site for upload, run:
+The *"Nächste Veranstaltungen"* table on the home page is generated by post-processing in `./build.sh`, not by Hugo, so
+it is **not** visible under `hugo serve`. To preview the fully built site (including the home-page calendar), or to
+ready the site for upload, run:
```shell
./build.sh
python3 -m http.server -d public 1313
```
-`build.sh` generates `all.ics` and replaces the `CALENDAR` placeholder in
-`index.html` with the upcoming-events table. It depends on Python with the
-`icalendar`, `python-dateutil`, and `pytz` packages, plus a `de_DE.UTF-8`
-locale (used to format weekday names). Inside `nix develop` these are
-provided automatically.
+`build.sh` replaces the `CALENDAR` placeholder in `index.html` with the upcoming-events table. It depends on Python
+with the `icalendar`, `python-dateutil`, and `pytz` packages, plus a `de_DE.UTF-8` locale (used to format weekday
+names). Inside `nix develop` these are provided automatically.
To build with *nix*: `nix build '.?submodules=1#production-content'`
@@ -55,7 +51,8 @@ To build with *nix*: `nix build '.?submodules=1#production-content'`
3. Commit (and push) your change.
4. ~~GitHub Actions is running the release workflow.~~
- If successful, check [Staging Website](https://staging.berlin.ccc.de/) if change is correct.
-5. Create merge request to merge changes from `staging` to `production` branch. Ask somebody to check merge request or if small change, merge yourself.
+5. Create a merge request to merge changes from `staging` to `production` branch. Ask somebody to check merge request or
+ if small change, merge yourself.
6. ~~GitHub Actions is running the release workflow.~~
- If successfull, check [Website](https://berlin.ccc.de/) if change is correct.
7. Profit!
@@ -64,9 +61,12 @@ To build with *nix*: `nix build '.?submodules=1#production-content'`
- After entering the shell with `nix develop`, hugo is available and `hugo serve` should work
- Python including required packages will be available, so the `build.sh` should work without a venv
-- You can build the staging and production builds with `nix build .#staging-content` and `nix build .#production-content`
-- Do not update the nixpkgs branch - 25.05 contains a newer hugo version that is incompatible with the theme (last checked June 2025)
+- You can build the staging and production builds with `nix build .#staging-content` and
+ `nix build .#production-content`
+- Do not update the nixpkgs branch - 25.05 contains a newer hugo version that is incompatible with the theme (last
+ checked June 2025)
---
Made with ❤️ and [Hugo](https://gohugo.io).
+
diff --git a/assets/js/calendar.js b/assets/js/calendar.js
index ffb3c7e..18b4768 100644
--- a/assets/js/calendar.js
+++ b/assets/js/calendar.js
@@ -19,10 +19,10 @@ document.addEventListener('DOMContentLoaded', function() {
if (colonIndex > -1) {
let key = line.substring(0, colonIndex);
let value = line.substring(colonIndex + 1);
-
+
// Handle properties with parameters (like TZID)
const baseKey = key.split(";")[0];
-
+
if (baseKey === "DTSTART") {
event.start = value;
event.startParams = key.includes(";") ? key.substring(key.indexOf(";") + 1) : null;
@@ -46,14 +46,14 @@ document.addEventListener('DOMContentLoaded', function() {
function parseDateString(icsDateStr) {
// Handle different date formats
if (!icsDateStr) return null;
-
+
// For basic date format: YYYYMMDD
if (icsDateStr.length === 8) {
let year = icsDateStr.substring(0, 4);
let month = icsDateStr.substring(4, 6);
let day = icsDateStr.substring(6, 8);
return `${year}-${month}-${day}`;
- }
+ }
// For datetime formats: YYYYMMDDTHHmmssZ or YYYYMMDDTHHmmss
else if (icsDateStr.includes("T")) {
let year = icsDateStr.substring(0, 4);
@@ -67,34 +67,34 @@ document.addEventListener('DOMContentLoaded', function() {
// Extract date components from different date formats
function getDateComponents(icsDateStr) {
if (!icsDateStr) return null;
-
+
// Basic handling - extract YYYY, MM, DD regardless of format
const year = parseInt(icsDateStr.substring(0, 4));
const month = parseInt(icsDateStr.substring(4, 6)) - 1; // 0-based months
const day = parseInt(icsDateStr.substring(6, 8));
-
+
return { year, month, day };
}
function expandRecurringEvents(event, year, month) {
if (!event.rrule) return [event];
-
+
const rruleStr = event.rrule;
-
+
// Get start date components
const startComponents = getDateComponents(event.start);
if (!startComponents) return [event];
-
+
const startDate = new Date(
startComponents.year,
startComponents.month,
startComponents.day
);
-
+
const rangeStart = new Date(year, month, 1);
const rangeEnd = new Date(year, month + 1, 0);
const expandedEvents = [];
-
+
if (rruleStr.includes("FREQ=WEEKLY") && rruleStr.includes("BYDAY")) {
const bydayMatch = rruleStr.match(/BYDAY=([^;]+)/);
if (bydayMatch) {
@@ -103,7 +103,7 @@ document.addEventListener('DOMContentLoaded', function() {
'MO': 1, 'TU': 2, 'WE': 3, 'TH': 4, 'FR': 5, 'SA': 6, 'SU': 0
};
const targetDay = dayMap[dayCode];
-
+
if (targetDay !== undefined) {
// Create events for each matching day in the month
let day = 1;
@@ -112,13 +112,13 @@ document.addEventListener('DOMContentLoaded', function() {
if (testDate.getDay() === targetDay && testDate >= startDate) {
const newEvent = {...event};
const eventDate = formatDateForICS(testDate);
-
+
// Preserve time portion from original event
- const timePart = event.start.includes('T') ?
+ const timePart = event.start.includes('T') ?
event.start.substring(event.start.indexOf('T')) : '';
- const endTimePart = event.end.includes('T') ?
+ const endTimePart = event.end.includes('T') ?
event.end.substring(event.end.indexOf('T')) : '';
-
+
newEvent.start = eventDate + timePart;
newEvent.end = eventDate + endTimePart;
expandedEvents.push(newEvent);
@@ -141,15 +141,15 @@ document.addEventListener('DOMContentLoaded', function() {
const dayMap = {
'MO': 1, 'TU': 2, 'WE': 3, 'TH': 4, 'FR': 5, 'SA': 6, 'SU': 0
};
-
+
bydays.forEach(byday => {
const occurrence = parseInt(byday) || 1;
const dayCode = byday.slice(-2);
const dayIndex = dayMap[dayCode];
-
+
let day = 1;
let count = 0;
-
+
while (day <= rangeEnd.getDate()) {
const testDate = new Date(year, month, day);
if (testDate.getDay() === dayIndex) {
@@ -157,13 +157,13 @@ document.addEventListener('DOMContentLoaded', function() {
if (count === occurrence || (occurrence < 0 && day > rangeEnd.getDate() + occurrence * 7)) {
const newEvent = {...event};
const eventDate = new Date(year, month, day);
-
+
// Preserve time portion from original event
- const timePart = event.start.includes('T') ?
+ const timePart = event.start.includes('T') ?
event.start.substring(event.start.indexOf('T')) : '';
- const endTimePart = event.end.includes('T') ?
+ const endTimePart = event.end.includes('T') ?
event.end.substring(event.end.indexOf('T')) : '';
-
+
newEvent.start = formatDateForICS(eventDate) + timePart;
newEvent.end = formatDateForICS(eventDate) + endTimePart;
expandedEvents.push(newEvent);
@@ -174,7 +174,7 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
}
-
+
return expandedEvents.length > 0 ? expandedEvents : [event];
}
@@ -206,7 +206,7 @@ document.addEventListener('DOMContentLoaded', function() {
function updateEventsForMonth(year, month) {
// Clear existing events for this month view
eventsByDate = {};
-
+
// Process each event, expanding recurring ones
events.forEach(ev => {
if (ev.rrule) {
@@ -228,7 +228,7 @@ document.addEventListener('DOMContentLoaded', function() {
// Check if this event belongs to current month view
const eventYear = parseInt(dateKey.split('-')[0]);
const eventMonth = parseInt(dateKey.split('-')[1]) - 1;
-
+
if (eventYear === year && eventMonth === month) {
if (!eventsByDate[dateKey]) {
eventsByDate[dateKey] = [];
@@ -238,7 +238,7 @@ document.addEventListener('DOMContentLoaded', function() {
}
}
});
-
+
renderCalendar(year, month);
}
@@ -267,16 +267,16 @@ document.addEventListener('DOMContentLoaded', function() {
}
let cell = document.createElement("td");
cell.innerHTML = "" + day + "";
-
+
let dayStr = day < 10 ? "0" + day : day;
let monthStr = (month + 1) < 10 ? "0" + (month + 1) : (month + 1);
let dateKey = year + "-" + monthStr + "-" + dayStr;
-
+
if (eventsByDate[dateKey]) {
let dotsContainer = document.createElement("div");
dotsContainer.className = "event-dots-container";
cell.classList.add("has-event");
-
+
// Gruppe Events nach Typ
const events = eventsByDate[dateKey];
const hasMembersOnly = events.some(e => e.summary.toLowerCase().includes("members only"));
@@ -285,12 +285,12 @@ document.addEventListener('DOMContentLoaded', function() {
const hasSpieleabend = events.some(e => e.summary.toLowerCase().includes("spieleabend"));
const hasRegular = events.some(e => {
const title = e.summary.toLowerCase();
- return !title.includes("members only") &&
- !title.includes("subbotnik") &&
- !title.includes("bastelabend") &&
+ return !title.includes("members only") &&
+ !title.includes("subbotnik") &&
+ !title.includes("bastelabend") &&
!title.includes("spieleabend");
});
-
+
// Füge Dots entsprechend der Event-Typen hinzu
if (hasMembersOnly) {
let dot = document.createElement("div");
@@ -307,7 +307,7 @@ document.addEventListener('DOMContentLoaded', function() {
dot.className = "event-dot event-dot-greenyellow";
dotsContainer.appendChild(dot);
}
-
+
cell.appendChild(dotsContainer);
cell.dataset.dateKey = dateKey;
cell.addEventListener("click", function() {
@@ -336,8 +336,8 @@ document.addEventListener('DOMContentLoaded', function() {
if (match && match[1]) {
return `https://berlin.ccc.de/datengarten/${match[1]}/`;
}
- }
-
+ }
+
// For other titles, convert to lowercase and use as path
const slug = eventTitle.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, '');
return `https://berlin.ccc.de/page/${slug}/`;
@@ -355,13 +355,13 @@ document.addEventListener('DOMContentLoaded', function() {
let eventTitle = document.createElement("div");
eventTitle.className = "event-title";
-
+
// Create a link for the event title
let titleLink = document.createElement("h");
titleLink.textContent = ev.summary;
titleLink.target = "_blank";
eventTitle.appendChild(titleLink);
-
+
eventItem.appendChild(eventTitle);
let eventTime = document.createElement("div");
@@ -372,7 +372,7 @@ document.addEventListener('DOMContentLoaded', function() {
if (ev.description) {
let eventDescription = document.createElement("div");
eventDescription.className = "event-description";
-
+
// Check if the description is a URL and make it a clickable link
if (ev.description.trim().startsWith('http')) {
let linkElement = document.createElement("a");
@@ -384,7 +384,7 @@ document.addEventListener('DOMContentLoaded', function() {
} else {
eventDescription.textContent = ev.description;
}
-
+
eventItem.appendChild(eventDescription);
}
@@ -409,7 +409,7 @@ document.addEventListener('DOMContentLoaded', function() {
function formatTime(icsTimeStr) {
// Format time for display
if (!icsTimeStr) return "";
-
+
if (icsTimeStr.length === 8) {
// All-day event
return "Ganztägig";
@@ -431,20 +431,18 @@ document.addEventListener('DOMContentLoaded', function() {
}
// ICS-Datei abrufen und Events verarbeiten
- fetch('/all.ics')
+ fetch('/calendars/all.ics')
.then(response => response.text())
.then(data => {
events = parseICS(data);
-
+
// Initialize with current date
let today = new Date();
currentYear = today.getFullYear();
currentMonth = today.getMonth();
-
+
// Process events for current month
-
- updateEventsForMonth(currentYear, currentMonth);
-
+ updateEventsForMonth(currentYear, currentMonth);
})
.catch(err => console.error('Fehler beim Laden der ICS-Datei:', err));
})();
diff --git a/assets/js/clubstatus.js b/assets/js/clubstatus.js
index d87eb04..fffd93d 100644
--- a/assets/js/clubstatus.js
+++ b/assets/js/clubstatus.js
@@ -9,11 +9,11 @@ function timeDistanceDE(pastDate) {
const hours = Math.floor(minutes / 60)
const days = Math.floor(hours / 24)
- if (days = 1) return `since ${days}d`
+ if (days === 1) return `since ${days}d`
if (days > 1) return `since ${days}d`
- if (hours = 1) return `since ${hours}h`
+ if (hours === 1) return `since ${hours}h`
if (hours > 1) return `since ${hours}h`
- if (minutes = 1) return `since ${minutes}min`
+ if (minutes === 1) return `since ${minutes}min`
if (minutes > 1) return `since ${minutes}min`
return `gerade eben`
}
diff --git a/build.sh b/build.sh
index 6bc707a..625a262 100755
--- a/build.sh
+++ b/build.sh
@@ -1,12 +1,7 @@
#!/bin/sh
-set -e
-set -x
+set -ex
rm -rf public/ # delete old generated files
hugo $(cat .hugo-params)
-./tools/merge_cals.py
-upcoming="$(tools/gen_upcoming.py static/all.ics 20 5 | tr '\n' ' ')"
-cp static/all.ics public/all.ics
-sed -i "s#CALENDAR#$upcoming#g" public/index.html
diff --git a/config/_default/hugo.toml b/config/_default/hugo.toml
index 13192d6..218ffbb 100644
--- a/config/_default/hugo.toml
+++ b/config/_default/hugo.toml
@@ -28,7 +28,7 @@ filename = "sitemap.xml"
priority = 0.5
[permalinks]
-post = "/post/:year/:month/:day/:title/"
+post = "/post/:year/:month/:day/:slug/"
[outputs]
home = ["html", "rss", "json"]
diff --git a/content/_index.md b/content/_index.md
index 531fa85..4d73f01 100644
--- a/content/_index.md
+++ b/content/_index.md
@@ -25,6 +25,9 @@ description: "Startseite CCCB mit Kurzkalender"
### Nächste Veranstaltungen
-CALENDAR
+
Weitere Termine findest du im [Veranstaltungskalender](/verein/calendar/).
+
+
diff --git a/content/post/Neujahresempfang-2026/index.md b/content/post/Neujahresempfang-2026/index.md
index 61844e1..17a6269 100644
--- a/content/post/Neujahresempfang-2026/index.md
+++ b/content/post/Neujahresempfang-2026/index.md
@@ -1,5 +1,6 @@
---
title: "Der Neujahresempfang 2026 #NJE26"
+slug: "neujahresempfang-2026"
date: 2026-03-05T12:00:00+01:00
showHero: false
tags: ["NJE", "Ankündigung"]
diff --git a/content/veranstaltungen/neujahresempfang.md b/content/veranstaltungen/neujahresempfang.md
index 9a97796..98de614 100644
--- a/content/veranstaltungen/neujahresempfang.md
+++ b/content/veranstaltungen/neujahresempfang.md
@@ -10,4 +10,4 @@ menu:
tag: ["Veranstaltung"]
---
-Am 23. Mai lädt der Chaos Computer Club Berlin zum Neujahresempfang. [Hier geht es zur Infoseite!](https://berlin.ccc.de/post/2026/03/05/der-neujahresempfang-2026-%23nje26/)
\ No newline at end of file
+Am 23. Mai lädt der Chaos Computer Club Berlin zum Neujahresempfang. [Hier geht es zur Infoseite!](https://berlin.ccc.de/post/2026/03/05/neujahresempfang-2026/)
\ No newline at end of file
diff --git a/content/verein/calendar/index.md b/content/verein/calendar/index.md
index fdac4e4..3fccf38 100644
--- a/content/verein/calendar/index.md
+++ b/content/verein/calendar/index.md
@@ -12,4 +12,15 @@ herostyle: big
{{< calendar >}}
-Keinen Termin mehr verpeilen? Einfach den [Veranstaltungskalender abonnieren](/all.ics)!
+Keinen Termin mehr verpeilen? Einfach den [Veranstaltungskalender abonnieren](/calendars/all.ics)!
+
+Oder willst du nur den Kalender von bestimmten Events?
+
+- [Club Discordia](/calendars/Club_Discordia.ics)
+- [Neujahresempfang](/calendars/Neujahresempfang.ics)
+- [Datengarten](/calendars/Datengarten.ics)
+- [Spieleabend](/calendars/Spieleabend.ics)
+- [OpenWRT](/calendars/OpenWRT.ics)
+- [Plenum](/calendars/Plenum.ics)
+- [Amateurfunk](/calendars/Amateurfunk.ics)
+- [Irreguläre Events](/calendars/Random.ics)
diff --git a/flake.lock b/flake.lock
index 2869045..33ab9db 100644
--- a/flake.lock
+++ b/flake.lock
@@ -2,16 +2,16 @@
"nodes": {
"nixpkgs": {
"locked": {
- "lastModified": 1748995628,
- "narHash": "sha256-bFufQGSAEYQgjtc4wMrobS5HWN0hDP+ZX+zthYcml9U=",
+ "lastModified": 1786313170,
+ "narHash": "sha256-9BG7OgUWdu0ONDO5X2q6+K4bsuBITkX/3W4nNJu1Ito=",
"owner": "nixos",
"repo": "nixpkgs",
- "rev": "8eb3b6a2366a7095939cd22f0dc0e9991313294b",
+ "rev": "fcb8fcd6bf2d0adecae5bd491afaaaf8311b758d",
"type": "github"
},
"original": {
"owner": "nixos",
- "ref": "nixos-24.11",
+ "ref": "nixos-26.05",
"repo": "nixpkgs",
"type": "github"
}
diff --git a/flake.nix b/flake.nix
index 39d9b6d..c29b73a 100644
--- a/flake.nix
+++ b/flake.nix
@@ -2,7 +2,7 @@
description = "A flake containing a development environment for the CCCB website.";
inputs = {
- nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11";
+ nixpkgs.url = "github:nixos/nixpkgs/nixos-26.05";
};
outputs =
@@ -34,7 +34,7 @@
devShells = forAllSystems (import ./devShells.nix);
- formatter = forAllSystems ({ pkgs, ... }: pkgs.nixfmt-rfc-style);
+ formatter = forAllSystems ({ pkgs, ... }: pkgs.nixfmt-tree);
lib.mkWwwContent =
{ domain, system }:
@@ -50,10 +50,6 @@
nativeBuildInputs = [
pkgs.hugo
pkgs.glibcLocales
- (pkgs.python3.withPackages (python-pkgs: [
- python-pkgs.icalendar
- python-pkgs.pytz
- ]))
];
# LOCALE_ARCHIVE = builtins.trace pkgs.glibcLocales "${pkgs.glibcLocales}/lib/locale/locale-archive";
@@ -62,11 +58,6 @@
buildPhase = ''
mkdir -p public
hugo --baseURL=https://${domain}/
-
- python3 ./tools/merge_cals.py
- upcoming="$(python3 tools/gen_upcoming.py static/all.ics 20 5 | tr '\n' ' ')"
- cp static/all.ics public/all.ics
- sed -i "s#CALENDAR#$upcoming#g" public/index.html
'';
# Install phase - copy the public directory to the output
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index c61ba7f..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-icalendar==5.0.7
-pytz
\ No newline at end of file
diff --git a/static/js/upcoming.js b/static/js/upcoming.js
new file mode 100644
index 0000000..6b7a711
--- /dev/null
+++ b/static/js/upcoming.js
@@ -0,0 +1,113 @@
+import ICAL from "https://unpkg.com/ical.js/dist/ical.min.js";
+
+/**
+ * Parse an ICS calendar and return upcoming event occurrences.
+ *
+ * @param {string} icsText The contents of the .ics file
+ * @param {Date} now Events must start after this date
+ * @param {number} maxEvents Maximum number of events to return
+ * @param {number} maxDays Maximum number of days into the future
+ * @returns {{start: Date, name: string, url: string}[]}
+ */
+function getUpcomingEvents(icsText, now, maxEvents, maxDays) {
+ const jcal = ICAL.parse(icsText);
+ const calendar = new ICAL.Component(jcal);
+
+ const end = new Date(now.getTime());
+ end.setDate(end.getDate() + maxDays);
+
+ const events = [];
+
+ for (const component of calendar.getAllSubcomponents("vevent")) {
+ const event = new ICAL.Event(component);
+
+ if (!event.startDate) {
+ continue;
+ }
+
+ if (event.isRecurring()) {
+ const iterator = event.iterator();
+
+ while (true) {
+ const occurrence = iterator.next();
+
+ if (!occurrence) {
+ break;
+ }
+
+ const start = occurrence.toJSDate();
+
+ // Recurrences are chronological, so we're done
+ // once we pass the end of our search window.
+ if (start > end) {
+ break;
+ }
+
+ if (start > now) {
+ events.push({
+ start,
+ name: event.summary ?? "",
+ url: event.url ?? "",
+ });
+ }
+ }
+ } else {
+ const start = event.startDate.toJSDate();
+
+ if (start > now && start <= end) {
+ events.push({
+ start,
+ name: event.summary ?? "",
+ url: event.url ?? "",
+ });
+ }
+ }
+ }
+
+ // We have occurrences from multiple events, so sort them
+ // before applying the maximum event count.
+ events.sort((a, b) => a.start - b.start);
+
+ return events.slice(0, maxEvents);
+}
+
+document.addEventListener("DOMContentLoaded", () => {
+ const ics = "/calendars/all.ics";
+ const max_days = 20;
+ const max_items = 5;
+
+ const now = new Date();
+
+ const table = document.getElementById("upcoming");
+ fetch(ics)
+ .then(response => response.text())
+ .then(icsText => {
+ getUpcomingEvents(icsText, now, max_items, max_days).forEach(event => {
+ const row = document.createElement("tr");
+
+ const colBegin = document.createElement("td");
+
+ const formattedStart = event.start.toLocaleString("de-DE", {
+ weekday: "long",
+ day: "2-digit",
+ month: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ }).replace(",", "");
+
+ colBegin.innerText = `${formattedStart.replace(" ", ", ")} Uhr`;
+ row.appendChild(colBegin);
+
+ const colName = document.createElement("td");
+
+ const a = document.createElement("a");
+ a.href = event.url;
+ a.text = event.name;
+
+ colName.appendChild(a);
+ row.appendChild(colName);
+
+ table.appendChild(row);
+ });
+ });
+});
diff --git a/tools/gen_upcoming.py b/tools/gen_upcoming.py
deleted file mode 100755
index 31763f7..0000000
--- a/tools/gen_upcoming.py
+++ /dev/null
@@ -1,108 +0,0 @@
-#!/usr/bin/env python3
-
-import sys
-import logging
-import locale
-from dateutil.parser import parse as _parse
-from datetime import datetime, timedelta
-from dateutil.rrule import rruleset, rrulestr
-
-import icalendar
-
-
-def parse(value):
- # Some templates emit DTSTART with a trailing "Z" (e.g. 20260507T190000Z)
- # which produces tz-aware datetimes, while others emit naive local times.
- # Normalize to naive so comparisons and sorting don't mix the two.
- dt = _parse(value)
- if dt.tzinfo is not None:
- dt = dt.replace(tzinfo=None)
- return dt
-
-
-def vevent_to_event(event, rrstart=None):
- if rrstart == None:
- begin = parse(event["DTSTART"].to_ical())
- else:
- begin = rrstart
-
- return {
- "name": event["SUMMARY"].to_ical(),
- "url": event["URL"].to_ical(),
- "begin": begin
- }
-
-
-def parse_single_event(event, start, end):
- logging.info(f"Processing single event {event['SUMMARY'].to_ical().decode('utf-8')}")
- dtstart = parse(event["DTSTART"].to_ical())
- if dtstart >= start and dtstart < end:
- return vevent_to_event(event)
-
-
-def parse_recurring_event(event, start, end):
- logging.info(f"Processing recurring event {event['SUMMARY'].to_ical().decode('utf-8')}")
- dtstart = parse(event["DTSTART"].to_ical())
- rs = rruleset()
- # dtstart is normalized to naive local time above; pass ignoretz so a
- # tz-aware UNTIL (RFC 5545 requires UTC for it) is treated as naive too,
- # otherwise dateutil rejects the naive/aware mismatch.
- rs.rrule(rrulestr(event["RRULE"].to_ical().decode("utf-8"), dtstart=dtstart, ignoretz=True))
- if "EXDATE" in event.keys():
- for exdate in event["EXDATE"]:
- rs.exdate(parse(exdate.to_ical()))
-
- events = []
- for date in list(rs):
- if date >= start and date < end:
- events.append(vevent_to_event(event, date))
-
- return events
-
-
-def find_events(icsfilestr, start, end, num):
- with open(icsfilestr, "r") as icsfile:
- cal = icalendar.Calendar.from_ical(icsfile.read())
-
- events = []
- for event in cal.subcomponents:
- if event.name == "VEVENT":
- if "RRULE" in event.keys():
- events.extend(parse_recurring_event(event, start, end))
- else:
- ev = parse_single_event(event, start, end)
- if ev is not None:
- events.append(ev)
-
- events = sorted(events, key=lambda k: k["begin"])
- events = events[0:num]
-
- return events
-
-
-def format_events(events):
- print("")
-
-
-if __name__ == "__main__":
- if len(sys.argv) < 3:
- print(f"Usage: {sys.argv[0]} calendar max_days max_items")
- sys.exit(-1)
-
- locale.setlocale(locale.LC_TIME, "de_DE.UTF-8")
- calendar = sys.argv[1]
- max_days = int(sys.argv[2])
- max_items = int(sys.argv[3])
-
- now = datetime.now()
- events = find_events(calendar, now, now + timedelta(days=max_days), max_items)
- format_events(events)
-
diff --git a/tools/merge_cals.py b/tools/merge_cals.py
deleted file mode 100755
index 136904e..0000000
--- a/tools/merge_cals.py
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/usr/bin/env python3
-
-from glob import glob
-
-import pytz
-import icalendar
-
-
-calendars = []
-merged = icalendar.Calendar()
-merged.add("prodid", "-//CCCB Calendar Generator//berlin.ccc.de//")
-merged.add("version", "2.0")
-
-for icsfilestr in glob("public/*/**/*.ics", recursive=True):
- with open(icsfilestr, "r") as icsfile:
- print(f"Importing {icsfilestr}")
- calendars.append(icalendar.Calendar.from_ical(icsfile.read()))
-
-for calendar in calendars:
- for event in calendar.subcomponents:
- if event.name != "VEVENT":
- continue
- if "DTSTART" not in event:
- continue
- merged.add_component(event)
-
-outfile = "static/all.ics"
-with open(outfile, "wb") as f:
- print(f"writing to {outfile}...")
- f.write(merged.to_ical())
-