forked from cccb-website-team/www
switch to ics files; make calendars work; fix some minor issues
This commit is contained in:
parent
5368e75240
commit
c28f04c6e8
19 changed files with 318 additions and 259 deletions
|
|
@ -9,11 +9,6 @@ charset = utf-8
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
|
||||||
[*.py]
|
[*.{js,yaml}]
|
||||||
indent_style = space
|
|
||||||
indent_size = 4
|
|
||||||
|
|
||||||
[*.yaml]
|
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
|
|
|
||||||
1
.gitattributes
vendored
1
.gitattributes
vendored
|
|
@ -1 +0,0 @@
|
||||||
*.ics text eol=crlf
|
|
||||||
133
.gitignore
vendored
133
.gitignore
vendored
|
|
@ -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
|
.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 ###
|
### Hugo ###
|
||||||
# Generated files by hugo
|
# Generated files by hugo
|
||||||
|
|
@ -43,7 +44,6 @@ hugo.linux
|
||||||
# Icon must end with two \r
|
# Icon must end with two \r
|
||||||
Icon
|
Icon
|
||||||
|
|
||||||
|
|
||||||
# Thumbnails
|
# Thumbnails
|
||||||
._*
|
._*
|
||||||
|
|
||||||
|
|
@ -67,6 +67,114 @@ Temporary Items
|
||||||
# iCloud generated files
|
# iCloud generated files
|
||||||
*.icloud
|
*.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 ###
|
||||||
# Windows thumbnail cache files
|
# Windows thumbnail cache files
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
@ -93,17 +201,4 @@ $RECYCLE.BIN/
|
||||||
# Windows shortcuts
|
# Windows shortcuts
|
||||||
*.lnk
|
*.lnk
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/windows,linux,macos,hugo
|
# End of https://www.toptal.com/developers/gitignore/api/windows,linux,macos,hugo,pycharm+all,vim,direnv
|
||||||
|
|
||||||
# NixOS:
|
|
||||||
.envrc
|
|
||||||
shell.nix
|
|
||||||
.direnv
|
|
||||||
|
|
||||||
# Python
|
|
||||||
.venv
|
|
||||||
|
|
||||||
# murmeldin
|
|
||||||
overview.md
|
|
||||||
|
|
||||||
/result
|
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
3.11
|
|
||||||
28
README.md
28
README.md
|
|
@ -27,24 +27,20 @@ This is the website of the CCCB.
|
||||||
|
|
||||||
5. Point your browser to: <http://localhost:1313/>
|
5. Point your browser to: <http://localhost:1313/>
|
||||||
|
|
||||||
Every change you make on the project will be reflected in your browser as long
|
Every change you make on the project will be reflected in your browser as long as `hugo serve` is running.
|
||||||
as `hugo serve` is running.
|
|
||||||
|
|
||||||
The *"Nächste Veranstaltungen"* table on the home page is generated by
|
The *"Nächste Veranstaltungen"* table on the home page is generated by post-processing in `./build.sh`, not by Hugo, so
|
||||||
post-processing in `./build.sh`, not by Hugo, so it is **not** visible under
|
it is **not** visible under `hugo serve`. To preview the fully built site (including the home-page calendar), or to
|
||||||
`hugo serve`. To preview the fully-built site (including the home-page
|
ready the site for upload, run:
|
||||||
calendar), or to ready the site for upload, run:
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
./build.sh
|
./build.sh
|
||||||
python3 -m http.server -d public 1313
|
python3 -m http.server -d public 1313
|
||||||
```
|
```
|
||||||
|
|
||||||
`build.sh` generates `all.ics` and replaces the `CALENDAR` placeholder in
|
`build.sh` replaces the `CALENDAR` placeholder in `index.html` with the upcoming-events table. It depends on Python
|
||||||
`index.html` with the upcoming-events table. It depends on Python with the
|
with the `icalendar`, `python-dateutil`, and `pytz` packages, plus a `de_DE.UTF-8` locale (used to format weekday
|
||||||
`icalendar`, `python-dateutil`, and `pytz` packages, plus a `de_DE.UTF-8`
|
names). Inside `nix develop` these are provided automatically.
|
||||||
locale (used to format weekday names). Inside `nix develop` these are
|
|
||||||
provided automatically.
|
|
||||||
|
|
||||||
To build with *nix*: `nix build '.?submodules=1#production-content'`
|
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.
|
3. Commit (and push) your change.
|
||||||
4. ~~GitHub Actions is running the release workflow.~~
|
4. ~~GitHub Actions is running the release workflow.~~
|
||||||
- If successful, check [Staging Website](https://staging.berlin.ccc.de/) if change is correct.
|
- 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.~~
|
6. ~~GitHub Actions is running the release workflow.~~
|
||||||
- If successfull, check [Website](https://berlin.ccc.de/) if change is correct.
|
- If successfull, check [Website](https://berlin.ccc.de/) if change is correct.
|
||||||
7. Profit!
|
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
|
- 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
|
- 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`
|
- You can build the staging and production builds with `nix build .#staging-content` and
|
||||||
- Do not update the nixpkgs branch - 25.05 contains a newer hugo version that is incompatible with the theme (last checked June 2025)
|
`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).
|
Made with ❤️ and [Hugo](https://gohugo.io).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -431,7 +431,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ICS-Datei abrufen und Events verarbeiten
|
// ICS-Datei abrufen und Events verarbeiten
|
||||||
fetch('/all.ics')
|
fetch('/calendars/all.ics')
|
||||||
.then(response => response.text())
|
.then(response => response.text())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
events = parseICS(data);
|
events = parseICS(data);
|
||||||
|
|
@ -442,9 +442,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
currentMonth = today.getMonth();
|
currentMonth = today.getMonth();
|
||||||
|
|
||||||
// Process events for current month
|
// Process events for current month
|
||||||
|
updateEventsForMonth(currentYear, currentMonth);
|
||||||
updateEventsForMonth(currentYear, currentMonth);
|
|
||||||
|
|
||||||
})
|
})
|
||||||
.catch(err => console.error('Fehler beim Laden der ICS-Datei:', err));
|
.catch(err => console.error('Fehler beim Laden der ICS-Datei:', err));
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,11 @@ function timeDistanceDE(pastDate) {
|
||||||
const hours = Math.floor(minutes / 60)
|
const hours = Math.floor(minutes / 60)
|
||||||
const days = Math.floor(hours / 24)
|
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 (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 (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`
|
if (minutes > 1) return `since ${minutes}min`
|
||||||
return `gerade eben`
|
return `gerade eben`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
7
build.sh
7
build.sh
|
|
@ -1,12 +1,7 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
set -e
|
set -ex
|
||||||
set -x
|
|
||||||
|
|
||||||
rm -rf public/ # delete old generated files
|
rm -rf public/ # delete old generated files
|
||||||
|
|
||||||
hugo $(cat .hugo-params)
|
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
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ filename = "sitemap.xml"
|
||||||
priority = 0.5
|
priority = 0.5
|
||||||
|
|
||||||
[permalinks]
|
[permalinks]
|
||||||
post = "/post/:year/:month/:day/:title/"
|
post = "/post/:year/:month/:day/:slug/"
|
||||||
|
|
||||||
[outputs]
|
[outputs]
|
||||||
home = ["html", "rss", "json"]
|
home = ["html", "rss", "json"]
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,9 @@ description: "Startseite CCCB mit Kurzkalender"
|
||||||
|
|
||||||
### Nächste Veranstaltungen
|
### Nächste Veranstaltungen
|
||||||
|
|
||||||
CALENDAR
|
<table id="upcoming" class="table table-condensed">
|
||||||
|
</table>
|
||||||
|
|
||||||
Weitere Termine findest du im [Veranstaltungskalender](/verein/calendar/).
|
Weitere Termine findest du im [Veranstaltungskalender](/verein/calendar/).
|
||||||
|
|
||||||
|
<script type="module" src="/js/upcoming.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
---
|
---
|
||||||
title: "Der Neujahresempfang 2026 #NJE26"
|
title: "Der Neujahresempfang 2026 #NJE26"
|
||||||
|
slug: "neujahresempfang-2026"
|
||||||
date: 2026-03-05T12:00:00+01:00
|
date: 2026-03-05T12:00:00+01:00
|
||||||
showHero: false
|
showHero: false
|
||||||
tags: ["NJE", "Ankündigung"]
|
tags: ["NJE", "Ankündigung"]
|
||||||
|
|
|
||||||
|
|
@ -10,4 +10,4 @@ menu:
|
||||||
tag: ["Veranstaltung"]
|
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/)
|
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/)
|
||||||
|
|
@ -12,4 +12,15 @@ herostyle: big
|
||||||
|
|
||||||
{{< calendar >}}
|
{{< 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)
|
||||||
|
|
|
||||||
8
flake.lock
generated
8
flake.lock
generated
|
|
@ -2,16 +2,16 @@
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1748995628,
|
"lastModified": 1786313170,
|
||||||
"narHash": "sha256-bFufQGSAEYQgjtc4wMrobS5HWN0hDP+ZX+zthYcml9U=",
|
"narHash": "sha256-9BG7OgUWdu0ONDO5X2q6+K4bsuBITkX/3W4nNJu1Ito=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "8eb3b6a2366a7095939cd22f0dc0e9991313294b",
|
"rev": "fcb8fcd6bf2d0adecae5bd491afaaaf8311b758d",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"ref": "nixos-24.11",
|
"ref": "nixos-26.05",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
flake.nix
13
flake.nix
|
|
@ -2,7 +2,7 @@
|
||||||
description = "A flake containing a development environment for the CCCB website.";
|
description = "A flake containing a development environment for the CCCB website.";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11";
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-26.05";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs =
|
outputs =
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
|
|
||||||
devShells = forAllSystems (import ./devShells.nix);
|
devShells = forAllSystems (import ./devShells.nix);
|
||||||
|
|
||||||
formatter = forAllSystems ({ pkgs, ... }: pkgs.nixfmt-rfc-style);
|
formatter = forAllSystems ({ pkgs, ... }: pkgs.nixfmt-tree);
|
||||||
|
|
||||||
lib.mkWwwContent =
|
lib.mkWwwContent =
|
||||||
{ domain, system }:
|
{ domain, system }:
|
||||||
|
|
@ -50,10 +50,6 @@
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
pkgs.hugo
|
pkgs.hugo
|
||||||
pkgs.glibcLocales
|
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";
|
# LOCALE_ARCHIVE = builtins.trace pkgs.glibcLocales "${pkgs.glibcLocales}/lib/locale/locale-archive";
|
||||||
|
|
@ -62,11 +58,6 @@
|
||||||
buildPhase = ''
|
buildPhase = ''
|
||||||
mkdir -p public
|
mkdir -p public
|
||||||
hugo --baseURL=https://${domain}/
|
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
|
# Install phase - copy the public directory to the output
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
icalendar==5.0.7
|
|
||||||
pytz
|
|
||||||
113
static/js/upcoming.js
Normal file
113
static/js/upcoming.js
Normal file
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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("<table class=\"table table-condensed\">")
|
|
||||||
for event in events:
|
|
||||||
print(
|
|
||||||
"<tr>"
|
|
||||||
f"<td>{event['begin'].strftime('%A, %d.%m um %H:%M Uhr')}</td>"
|
|
||||||
f"<td><a href=\"{event['url'].decode('utf-8')}\">{event['name'].decode('utf-8')}</a></td>"
|
|
||||||
"</tr>"
|
|
||||||
)
|
|
||||||
print("</table><!--/.table .table-condensed-->")
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
@ -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())
|
|
||||||
|
|
||||||
Loading…
Reference in a new issue