Compare commits

..

11 commits

Author SHA1 Message Date
Marek Krug 1f048a1dd5 Merge remote-tracking branch 'refs/remotes/forgejo-cccb/production' into production 2025-03-10 04:39:20 +01:00
Marek Krug a63680661e Merge branch 'staging' into production 2025-03-10 04:38:44 +01:00
Marek Krug 03af7ae63a tdoh image, calendar improvements, minor changes to a few articles 2025-03-10 04:31:53 +01:00
Vinzenz Schroeter 2178fe4504 add missing image 2025-03-09 20:58:04 +01:00
Vinzenz Schroeter 4098864386 change link order, fix forgejo has gitlab icon 2025-03-09 20:56:48 +01:00
Marek Krug 914620fe0f Merge branch 'vinzenz-dev' into staging 2025-03-09 20:09:30 +01:00
Vinzenz Schroeter fd3e77d1da Links im Footer auf deutsch 2025-03-09 18:15:15 +01:00
Vinzenz Schroeter ba4ed0804b fix copy-paste 2025-03-09 18:15:15 +01:00
Vinzenz Schroeter dff786e5e7 abort build on error 2025-03-09 18:15:15 +01:00
Vinzenz Schroeter e10cfcdbcc update README.md 2025-03-09 18:15:15 +01:00
Vinzenz Schroeter 77cc995c03 add flake template with hugo binary in dev shell 2025-03-09 18:15:15 +01:00
21 changed files with 451 additions and 137 deletions

3
.gitignore vendored
View file

@ -1,4 +1,5 @@
static/all.ics static/all.ics
.envrc
# Created by https://www.toptal.com/developers/gitignore/api/windows,linux,macos,hugo # 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 # Edit at https://www.toptal.com/developers/gitignore?templates=windows,linux,macos,hugo
@ -100,4 +101,4 @@ shell.nix
.direnv .direnv
# Python # Python
.venv .venv

View file

@ -1,23 +1,19 @@
![CCCB logo](static/img/logo.png)
# CCCB Website # CCCB Website
This is the website of the CCCB. This is the website of the CCCB.
![CCCB logo](assets/img/logo.png)
## Getting started ## Getting started
1. Get Hugo: <https://gohugo.io/getting-started/installing> 1. Get Hugo: <https://gohugo.io/getting-started/installing>
2. Clone this repo 2. Clone this repo (`--recursive` is needed to check out submodules)
```shell ```shell
git clone https://github.com/cccb/www git clone --recursive https://git.berlin.ccc.de/cccb-website-team/www.git cccb-website
``` ```
3. Switch directory 3. Switch directory
```shell ```shell
cd www cd cccb-website
```
3. Fetch Submodules
```shell
git submodule update --recursive --remote --init
``` ```
### Run site locally ### Run site locally
@ -35,13 +31,13 @@ Every change you make on the project will be reflected in your browser as long a
## Making a change ## Making a change
1. Use your local dev setup (see Getting started) or via GitHub editor. 1. Use your local dev setup (see Getting started) or via the Forgejo editor.
2. Make your change in `staging` branch. 2. Make your change in `staging` branch.
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 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!

View file

@ -2,7 +2,8 @@
- DSGVO-compliant Datenschutzerklärung reinbasteln - DSGVO-compliant Datenschutzerklärung reinbasteln
- Entscheiden, welche Seiten sonst noch konvertiert werden sollen und welche in die ewigen Datengründe gehen können - Entscheiden, welche Seiten sonst noch konvertiert werden sollen und welche in die ewigen Datengründe gehen können
- add nix config to repo
# Done # Done
@ -17,4 +18,3 @@
- Bestehende Datengarten-Termine konvertieren - Bestehende Datengarten-Termine konvertieren
- ggf. template mit frontmatter - ggf. template mit frontmatter
- Theme forken, alle assets sollten lokal gehosted sein und nicht von irgendwelchen CDNs bezogen werden (HTTP/2 ftw!) - Theme forken, alle assets sollten lokal gehosted sein und nicht von irgendwelchen CDNs bezogen werden (HTTP/2 ftw!)

View file

@ -1,7 +1,10 @@
#!/bin/sh #!/bin/sh
set -e
set -x
hugo $(cat .hugo-params) hugo $(cat .hugo-params)
./tools/merge_cals.py ./tools/merge_cals.py
upcoming="$(tools/gen_upcoming.py static/all.ics 20 5 | tr '\n' ' ')" upcoming="$(tools/gen_upcoming.py static/all.ics 20 5 | tr '\n' ' ')"
cp static/all.ics public/all.ics cp static/all.ics public/all.ics
sed -i "s#CALENDAR#$upcoming#g" public/index.html sed -i "s#CALENDAR#$upcoming#g" public/index.html

View file

@ -25,53 +25,9 @@ title = "Chaos Computer Club Berlin"
headline = "Willkommen! Wir sind ein Erfa-Kreis des Chaos Computer Club e.V. und die örtliche Niederlassung des CCC in Berlin." headline = "Willkommen! Wir sind ein Erfa-Kreis des Chaos Computer Club e.V. und die örtliche Niederlassung des CCC in Berlin."
# bio = "A little bit about you" # bio = "A little bit about you"
links = [ links = [
{ email = "mailto:mail2025@berlin.ccc.de" },
# { link = "https://link-to-some-website.com/" },
# { amazon = "https://www.amazon.com/hz/wishlist/ls/wishlist-id" },
# { apple = "https://www.apple.com" },
# { blogger = "https://username.blogspot.com/" },
# { bluesky = "https://bsky.app/profile/username" },
# { codepen = "https://codepen.io/username" },
# { dev = "https://dev.to/username" },
# { discord = "https://discord.gg/invitecode" },
# { dribbble = "https://dribbble.com/username" },
# { facebook = "https://facebook.com/username" },
# { flickr = "https://www.flickr.com/photos/username/" },
# { foursquare = "https://foursquare.com/username" },
{ github = "https://github.com/cccb/" },
{ gitlab = "https://git.berlin.ccc.de/explore/repos" },
# { google = "https://www.google.com/" },
# { hashnode = "https://username.hashnode.dev" },
# { instagram = "https://instagram.com/username" },
# { itch-io = "https://username.itch.io" },
# { keybase = "https://keybase.io/username" },
# { kickstarter = "https://www.kickstarter.com/profile/username" },
# { lastfm = "https://lastfm.com/user/username" },
# { linkedin = "https://linkedin.com/in/username" },
{ mastodon = "https://chaos.social/@clubdiscordia" }, { mastodon = "https://chaos.social/@clubdiscordia" },
# { irc = "https://webirc.hackint.org/#ircs://irc.hackint.org/#cccb" }, { forgejo = "https://git.berlin.ccc.de/explore/repos" },
# { medium = "https://medium.com/username" }, { email = "mailto:mail2025@berlin.ccc.de" },
# { microsoft = "https://www.microsoft.com/" }, { github = "https://github.com/cccb/" },
# { orcid = "https://orcid.org/userid" },
# { patreon = "https://www.patreon.com/username" },
# { pinterest = "https://pinterest.com/username" },
# { reddit = "https://reddit.com/user/username" },
# { researchgate = "https://www.researchgate.net/profile/username" },
# { slack = "https://workspace.url/team/userid" },
# { snapchat = "https://snapchat.com/add/username" },
# { soundcloud = "https://soundcloud.com/username" },
# { spotify = "https://open.spotify.com/user/userid" },
# { stack-overflow = "https://stackoverflow.com/users/userid/username" },
# { steam = "https://steamcommunity.com/profiles/userid" },
# { telegram = "https://t.me/username" },
# { threads = "https://www.threads.net/@username" },
# { tiktok = "https://tiktok.com/@username" },
# { tumblr = "https://username.tumblr.com" },
# { twitch = "https://twitch.tv/username" },
# { twitter = "https://twitter.com/username" },
# { x-twitter = "https://twitter.com/username" },
# { whatsapp = "https://wa.me/phone-number" },
# { youtube = "https://youtube.com/username" },
# { ko-fi = "https://ko-fi.com/username" },
# { codeberg = "https://codeberg.org/username"},
] ]

View file

@ -46,7 +46,7 @@
weight = 10 weight = 10
[[footer]] [[footer]]
name = "Categories" name = "Kategorien"
pageRef = "categories" pageRef = "categories"
weight = 20 weight = 20
@ -56,6 +56,6 @@
weight = 500 weight = 500
[[footer]] [[footer]]
name = "Privacy" name = "Datenschutz"
pageRef = "datenschutz" pageRef = "datenschutz"
weight = 600 weight = 600

View file

@ -6,4 +6,4 @@ description: "Startseite CCCB mit Kurzkalender"
CALENDAR CALENDAR
Keinen Termin mehr verpeilen? Einfach den [Veranstaltungskalender abonnieren](all.ics)! Weitere Termine findest du [hier](/verein/calendar/).

View file

@ -1,22 +1,31 @@
--- ---
title: "Tag Des Offenen Hackerspace" title: "Tag Des Offenen Hackspace"
subtitle: "Der CCCB lädt ein zum Kennenlernen und Entdecken." subtitle: "Der CCCB lädt ein zum Kennenlernen und Entdecken."
date: 2025-02-10T12:00:00+02:00 date: 2025-02-10T12:00:00+02:00
dtstart: 20250329T130000 dtstart: 20250329T130000
dtend: 20250329T200000 dtend: 20250329T200000
--- ---
Der [Tag des offenen Hackerspace](https://events.ccc.de/2024/02/23/tag-des-offenen-hackspace-einladung/) findet dieses Jahr auch im CCCB statt. {{< figure
src="/img/tdoh/20250309_001608.jpg"
title="Das Airport Display im CCCB"
alt="Das Airport Display im CCCB"
caption="Das Airport Display im CCCB"
>}}
Der [Tag des offenen Hackspace](https://events.ccc.de/2025/02/28/tag-des-offenen-hackspace-2025/) findet dieses Jahr auch im Chaos Computer Club Berlin statt:
- Führungen durch die Räume - Führungen durch die Räume
- mit Hack- und Lachgeschichten - mit Hack- und Lachgeschichten
- Kurzvorträge ab 17 Uhr zu den Themen: - Kurzvorträge ab 17 Uhr u.a. zu den Themen:
- Der CCCB stellt sich vor!
- Freifunk - Freifunk
- OpenWrt - OpenWrt
- ...und noch ein paar weitere. - Spaß am Gerät mit dem Service-Point
- Spaß am Gerät - Live-Schaltungen zu anderen Hackspaces
- Spiele und Videos auf dem Flughafen Info-Point Display - die Community kennenlernen
- Live-Schaltungen mit Kamera und Musik zu anderen Hackespaces
- die Community kennenlernen
Am 29.03. freuen wir uns, euch ab 13 Uhr in unseren Clubräumen begrüßen zu dürfen! Am 29.03.2025 freuen wir uns, Euch von 13 Uhr bis 20 Uhr in unseren Clubräumen begrüßen zu dürfen!
Anfahrt: https://berlin.ccc.de/verein/anfahrt/
All Creatures Welcome!

View file

@ -3,7 +3,13 @@ title: "Amateurfunk"
subtitle: "Funken und Löten im CCCB" subtitle: "Funken und Löten im CCCB"
date: 2018-05-17T22:41:48+02:00 date: 2018-05-17T22:41:48+02:00
--- ---
![Logo der Chaoswelle](/img/chaoswelle.png "Logo der Chaoswelle")
{{< figure
src="/img/chaoswelle.png"
title="Logo der Chaoswelle"
alt="Logo der Chaoswelle"
caption="Logo der Chaoswelle"
>}}
Vor einiger Zeit fanden gelegentlich Amateurfunkkurse im CCCB statt. Im Zusammenhang damit trafen sich Funkamateure und Interessierte rund um drahtlose Kommunikation auch an den offenen Abenden. Das Treffen war Nahe zur [Interessensgemeinschaft Vor einiger Zeit fanden gelegentlich Amateurfunkkurse im CCCB statt. Im Zusammenhang damit trafen sich Funkamateure und Interessierte rund um drahtlose Kommunikation auch an den offenen Abenden. Das Treffen war Nahe zur [Interessensgemeinschaft
Chaoswelle](http://chaoswelle.de), die sich im DARC e.V. Ortsverband D23 Chaoswelle](http://chaoswelle.de), die sich im DARC e.V. Ortsverband D23

View file

@ -10,7 +10,21 @@ menu:
parent: "Veranstaltungen" parent: "Veranstaltungen"
--- ---
![Lötkolben im CCCB](/img/club/27481933907_f240f4232d.jpg)
{{< figure
src="/img/club/27481933907_f240f4232d.jpg"
title="Lötkolben im CCCB"
alt="Lötkolben im CCCB"
caption="Lötkolben im CCCB"
>}}
**Jeden 1. und 3. Samstag im Monat ist ab 17 Uhr Bastelabend im Club.** **Jeden 1. und 3. Samstag im Monat ist ab 17 Uhr Bastelabend im Club.**
Wenn ihr neu seid und den CCCB zum ersten Mal besuchen wollt, kommt am besten an einem Donnerstag zum [Club Discordia](/page/clubdiscordia/), da samstags nicht immer genug Leute da sind, um euch zu empfangen. Generell sind aber alle herzlich eingeladen, an den Spieleabenden vorbeizukommen.
{{< alert "circle-info" >}}
Wenn ihr neu seid und den CCCB zum ersten Mal besuchen wollt, kommt am besten an einem Donnerstag zum [Club Discordia](/page/clubdiscordia/), da samstags nicht immer genug Leute da sind, um euch zu empfangen.
Generell sind aber alle, die schonmal im Club waren, herzlich eingeladen, an den Bastelabenden vorbeizukommen.
{{< /alert >}}

View file

@ -11,7 +11,12 @@ menu:
parent: "Veranstaltungen" parent: "Veranstaltungen"
--- ---
![Chaosradio logo](/img/chaosradio.png) {{< figure
src="/img/chaosradio.png"
title="Chaosradio logo"
alt="Chaosradio logo"
caption="Chaosradio logo"
>}}
[Chaosradio](https://chaosradio.ccc.de) ist der Podcast des CCCB. Seit 2020 zeichen [Chaosradio](https://chaosradio.ccc.de) ist der Podcast des CCCB. Seit 2020 zeichen
wir ihn remote auf. Gelegentlich finden Sendungen vor Live-Publikum im Club, auf wir ihn remote auf. Gelegentlich finden Sendungen vor Live-Publikum im Club, auf

View file

@ -12,7 +12,13 @@ menu:
parent: "Veranstaltungen" parent: "Veranstaltungen"
--- ---
![E-Lab im CCCB](/img/club/e-lab-kisten.jpg) {{< figure
src="/img/club/e-lab-kisten.jpg"
title="Das E-Lab im CCCB"
alt="Das E-Lab im CCCB"
caption="Das E-Lab im CCCB"
>}}
Der **Club Discordia** ist ein öffentliches Treffen in den Clubräumen des CCC Berlin. Jeder, der Lust hat, ist eingeladen, **Donnerstags so ab ca. 19 Uhr bis 24 Uhr** vorbeizukommen. Wer will, kann seinen Computer mitbringen und sollte das auch tun, sonst ist es langweilig wenn die Nerds erstmal nicht mit einem reden oder wenig los ist. Der **Club Discordia** ist ein öffentliches Treffen in den Clubräumen des CCC Berlin. Jeder, der Lust hat, ist eingeladen, **Donnerstags so ab ca. 19 Uhr bis 24 Uhr** vorbeizukommen. Wer will, kann seinen Computer mitbringen und sollte das auch tun, sonst ist es langweilig wenn die Nerds erstmal nicht mit einem reden oder wenig los ist.

View file

@ -10,7 +10,12 @@ menu:
parent: "Veranstaltungen" parent: "Veranstaltungen"
--- ---
![Chaos Macht Schule Logo](/img/cms-logo.jpg "Chaos Macht Schule Logo") {{< figure
src="/img/cms-logo.jpg"
title="Chaos Macht Schule Logo"
alt="Chaos Macht Schule Logo"
caption="Chaos Macht Schule Logo"
>}}
Unter dem Begriff “Chaos macht Schule” finden sich mehrere regionale Unter dem Begriff “Chaos macht Schule” finden sich mehrere regionale
Projekte diverser ERFA-Kreise des CCC. Bestärkt in der bisherigen Projekte diverser ERFA-Kreise des CCC. Bestärkt in der bisherigen

View file

@ -1,5 +1,5 @@
--- ---
title: "Plenum (club closed, members only)" title: "Plenum (club closed - members only)"
subtitle: "Instrument der internen Konsensfindung" subtitle: "Instrument der internen Konsensfindung"
date: 2018-05-18T01:11:54+02:00 date: 2018-05-18T01:11:54+02:00
dtstart: 20180704T200000 dtstart: 20180704T200000

View file

@ -13,4 +13,9 @@ menu:
![Dorfromantik spielen im CCCB](/img/club/dorfromantik-im-cccb.jpg) ![Dorfromantik spielen im CCCB](/img/club/dorfromantik-im-cccb.jpg)
**Jeden 2. und 4. Samstag im Monat ist ab 17 Uhr Spieleabend im Club.** **Jeden 2. und 4. Samstag im Monat ist ab 17 Uhr Spieleabend im Club.**
Wenn ihr neu seid und den CCCB zum ersten Mal besuchen wollt, kommt am besten an einem Donnerstag zum [Club Discordia](/page/clubdiscordia/), da samstags nicht immer genug Leute da sind, um euch zu empfangen. Generell sind aber alle herzlich eingeladen, an den Bastelabenden vorbeizukommen.
{{< alert "circle-info" >}}
Wenn ihr neu seid und den CCCB zum ersten Mal besuchen wollt, kommt am besten an einem Donnerstag zum [Club Discordia](/page/clubdiscordia/), da samstags nicht immer genug Leute da sind, um euch zu empfangen.
Generell sind aber alle, die schonmal im Club waren, herzlich eingeladen, an den Spieleabenden vorbeizukommen.
{{< /alert >}}

View file

@ -18,4 +18,11 @@ Ungefähr jedes viertel Jahr wird im CCCB von den Mitgliedern einmal richtig dur
Der Begriff [Subbotnik](https://de.wikipedia.org/wiki/Subbotnik) ist eine in Sowjetrussland entstandene Bezeichnung für einen unbezahlten Arbeitseinsatz am Sonnabend, der in den Sprachgebrauch in der DDR und daraufhin auch in den Sprachgebrauch der CCCB-Mitglieder übernommen wurde. Der Begriff [Subbotnik](https://de.wikipedia.org/wiki/Subbotnik) ist eine in Sowjetrussland entstandene Bezeichnung für einen unbezahlten Arbeitseinsatz am Sonnabend, der in den Sprachgebrauch in der DDR und daraufhin auch in den Sprachgebrauch der CCCB-Mitglieder übernommen wurde.
Wenn ihr neu seid und den CCCB zum ersten Mal besuchen wollt, kommt am besten an einem Donnerstag zum [Club Discordia](/page/clubdiscordia/), da samstags nicht immer genug Leute da sind, um euch zu empfangen. Generell sind aber alle herzlich eingeladen, am Subbotnik vorbeizukommen und mitzuhelfen. ---
{{< alert "circle-info" >}}
Wenn ihr neu seid und den CCCB zum ersten Mal besuchen wollt, kommt am besten an einem Donnerstag zum [Club Discordia](/page/clubdiscordia/), da samstags nicht immer genug Leute da sind, um euch zu empfangen.
Generell sind aber alle, die schonmal im Club waren, herzlich eingeladen am Subbotnik vorbeizukommen und mitzuhelfen.
{{< /alert >}}

View file

@ -22,7 +22,7 @@ menu:
} }
#event-panel { #event-panel {
flex: 1; flex: 1;
min-width: 300px; min-width: 30%;
background-color: var(--color-bg-secondary); background-color: var(--color-bg-secondary);
padding: 15px; padding: 15px;
border-radius: 5px; border-radius: 5px;
@ -48,38 +48,62 @@ menu:
border-collapse: collapse; border-collapse: collapse;
} }
#calendar-table th, #calendar-table td { #calendar-table th, #calendar-table td {
border: 1px solid var(--color-border); border: 1px;
padding: 5px; padding: 5px;
text-align: center; text-align: center;
vertical-align: top; vertical-align: top;
height: 60px; height: 50px;
width: 14.28%; width: 15%;
}
#calendar-table th {
background-color: var(--color-bg-secondary);
} }
#calendar-table th
#calendar-table td { #calendar-table td {
background-color: var(--color-bg-primary);
position: relative; position: relative;
cursor: pointer; cursor: pointer;
} }
#calendar-table td:hover { #calendar-table td:hover {
background-color: var(--color-bg-hover); background-color: rgba(255, 255, 255, 0.2);
box-shadow: 0 0 20px rgba(255, 255, 255, 0.2);
} }
.event-dot { .event-dot-greenyellow {
height: 8px; height: 8px;
width: 8px; width: 8px;
background-color: greenyellow; background-color: #9acd32;
border-radius: 50%; border-radius: 50%;
display: block; display: block;
margin: 5px auto 0; margin: 5px auto 0;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
}
.event-dot-orange {
height: 8px;
width: 8px;
background-color: #ffa500;
border-radius: 50%;
display: block;
margin: 5px auto 0;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
}
.event-dot-red {
height: 8px;
width: 8px;
background-color: #ff4500;
border-radius: 50%;
display: block;
margin: 5px auto 0;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
} }
.has-event { .has-event {
background-color: var(--color-bg-secondary) !important;
} }
.selected-day { .selected-day {
background-color: var(--color-bg-hover) !important; background-color: rgba(255, 255, 255, 0.1);
font-weight: bold; box-shadow: 0 0 20px rgba(255, 255, 255, 0.1);
border-radius: 8px;
width: 20px;
height: 20px;
border: 5px solid grey;
font-weight: lighter;
position: relative;
z-index: 1;
} }
#event-date { #event-date {
font-size: 1.2em; font-size: 1.2em;
@ -153,14 +177,22 @@ menu:
if (colonIndex > -1) { if (colonIndex > -1) {
let key = line.substring(0, colonIndex); let key = line.substring(0, colonIndex);
let value = line.substring(colonIndex + 1); let value = line.substring(colonIndex + 1);
if (key.startsWith("DTSTART")) {
// Handle properties with parameters (like TZID)
const baseKey = key.split(";")[0];
if (baseKey === "DTSTART") {
event.start = value; event.start = value;
} else if (key.startsWith("DTEND")) { event.startParams = key.includes(";") ? key.substring(key.indexOf(";") + 1) : null;
} else if (baseKey === "DTEND") {
event.end = value; event.end = value;
} else if (key.startsWith("SUMMARY")) { event.endParams = key.includes(";") ? key.substring(key.indexOf(";") + 1) : null;
} else if (baseKey === "SUMMARY") {
event.summary = value; event.summary = value;
} else if (key.startsWith("DESCRIPTION")) { } else if (baseKey === "DESCRIPTION") {
event.description = value; event.description = value;
} else if (baseKey === "RRULE") {
event.rrule = value;
} }
} }
} }
@ -170,13 +202,18 @@ menu:
// Hilfsfunktion: Parst einen ICS-Datum-String ins Format "YYYY-MM-DD" // Hilfsfunktion: Parst einen ICS-Datum-String ins Format "YYYY-MM-DD"
function parseDateString(icsDateStr) { function parseDateString(icsDateStr) {
// Erwartet entweder ganztägige Daten (YYYYMMDD) oder Datum+Zeit (YYYYMMDDTHHmmssZ) // Handle different date formats
if (!icsDateStr) return null;
// For basic date format: YYYYMMDD
if (icsDateStr.length === 8) { if (icsDateStr.length === 8) {
let year = icsDateStr.substring(0, 4); let year = icsDateStr.substring(0, 4);
let month = icsDateStr.substring(4, 6); let month = icsDateStr.substring(4, 6);
let day = icsDateStr.substring(6, 8); let day = icsDateStr.substring(6, 8);
return `${year}-${month}-${day}`; return `${year}-${month}-${day}`;
} else if (icsDateStr.length >= 15) { }
// For datetime formats: YYYYMMDDTHHmmssZ or YYYYMMDDTHHmmss
else if (icsDateStr.includes("T")) {
let year = icsDateStr.substring(0, 4); let year = icsDateStr.substring(0, 4);
let month = icsDateStr.substring(4, 6); let month = icsDateStr.substring(4, 6);
let day = icsDateStr.substring(6, 8); let day = icsDateStr.substring(6, 8);
@ -185,6 +222,114 @@ menu:
return null; return null;
} }
// 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) {
const dayCode = bydayMatch[1];
const dayMap = {
'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;
while (day <= rangeEnd.getDate()) {
const testDate = new Date(year, month, day);
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') ?
event.start.substring(event.start.indexOf('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);
}
day++;
}
}
}
}
else if (rruleStr.includes("FREQ=MONTHLY") && rruleStr.includes("BYDAY")) {
const bydayMatch = rruleStr.match(/BYDAY=([^;]+)/);
if (bydayMatch) {
const bydays = bydayMatch[1].split(',');
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) {
count++;
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') ?
event.start.substring(event.start.indexOf('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);
}
}
day++;
}
});
}
}
return expandedEvents.length > 0 ? expandedEvents : [event];
}
// Kalender initialisieren // Kalender initialisieren
let currentYear, currentMonth; let currentYear, currentMonth;
const currentMonthElem = document.getElementById("current-month"); const currentMonthElem = document.getElementById("current-month");
@ -199,17 +344,56 @@ menu:
currentMonth = 11; currentMonth = 11;
currentYear--; currentYear--;
} }
renderCalendar(currentYear, currentMonth); updateEventsForMonth(currentYear, currentMonth);
}); });
document.getElementById("next-month").addEventListener("click", function(){ document.getElementById("next-month").addEventListener("click", function(){
currentMonth++; currentMonth++;
if (currentMonth > 11) { if (currentMonth > 11) {
currentMonth = 0; currentMonth = 0;
currentYear++; currentYear++;
} }
renderCalendar(currentYear, currentMonth); updateEventsForMonth(currentYear, currentMonth);
}); });
function updateEventsForMonth(year, month) {
// Clear existing events for this month view
eventsByDate = {};
// Process each event, expanding recurring ones
events.forEach(ev => {
if (ev.rrule) {
// For recurring events, expand them for current month
const expandedEvents = expandRecurringEvents(ev, year, month);
expandedEvents.forEach(expandedEv => {
let dateKey = parseDateString(expandedEv.start);
if (dateKey) {
if (!eventsByDate[dateKey]) {
eventsByDate[dateKey] = [];
}
eventsByDate[dateKey].push(expandedEv);
}
});
} else {
// For regular events, check if they fall in current month
let dateKey = parseDateString(ev.start);
if (dateKey) {
// 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] = [];
}
eventsByDate[dateKey].push(ev);
}
}
}
});
renderCalendar(year, month);
}
function renderCalendar(year, month) { function renderCalendar(year, month) {
// Setze die Monatsbeschriftung (in Deutsch) // Setze die Monatsbeschriftung (in Deutsch)
const monthNames = ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"]; const monthNames = ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"];
@ -241,11 +425,42 @@ menu:
let dateKey = year + "-" + monthStr + "-" + dayStr; let dateKey = year + "-" + monthStr + "-" + dayStr;
if (eventsByDate[dateKey]) { if (eventsByDate[dateKey]) {
let eventDot = document.createElement("div"); let dotsContainer = document.createElement("div");
eventDot.className = "event-dot"; dotsContainer.className = "event-dots-container";
cell.appendChild(document.createElement("br")); cell.classList.add("has-event");
cell.appendChild(eventDot);
// Gruppe Events nach Typ
const events = eventsByDate[dateKey];
const hasMembersOnly = events.some(e => e.summary.toLowerCase().includes("members only"));
const hasSubbotnik = events.some(e => e.summary.toLowerCase().includes("subbotnik"));
const hasBastelabend = events.some(e => e.summary.toLowerCase().includes("bastelabend"));
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") &&
!title.includes("spieleabend");
});
// Füge Dots entsprechend der Event-Typen hinzu
if (hasMembersOnly) {
let dot = document.createElement("div");
dot.className = "event-dot event-dot-red";
dotsContainer.appendChild(dot);
}
if (hasSubbotnik || hasBastelabend || hasSpieleabend) {
let dot = document.createElement("div");
dot.className = "event-dot event-dot-orange";
dotsContainer.appendChild(dot);
}
if (hasRegular) {
let dot = document.createElement("div");
dot.className = "event-dot event-dot-greenyellow";
dotsContainer.appendChild(dot);
}
cell.appendChild(dotsContainer);
cell.dataset.dateKey = dateKey; cell.dataset.dateKey = dateKey;
cell.addEventListener("click", function() { cell.addEventListener("click", function() {
// Clear previous selections // Clear previous selections
@ -294,9 +509,8 @@ menu:
eventTitle.className = "event-title"; eventTitle.className = "event-title";
// Create a link for the event title // Create a link for the event title
let titleLink = document.createElement("a"); let titleLink = document.createElement("h");
titleLink.textContent = ev.summary; titleLink.textContent = ev.summary;
titleLink.href = createEventLink(ev.summary);
titleLink.target = "_blank"; titleLink.target = "_blank";
eventTitle.appendChild(titleLink); eventTitle.appendChild(titleLink);
@ -307,14 +521,26 @@ menu:
eventTime.textContent = `Start: ${formatTime(ev.start)}, End: ${formatTime(ev.end)}`; eventTime.textContent = `Start: ${formatTime(ev.start)}, End: ${formatTime(ev.end)}`;
eventItem.appendChild(eventTime); eventItem.appendChild(eventTime);
if (ev.description) { if (ev.description) {
let eventDescription = document.createElement("div"); let eventDescription = document.createElement("div");
eventDescription.className = "event-description"; eventDescription.className = "event-description";
eventDescription.textContent = ev.description;
eventItem.appendChild(eventDescription); // Check if the description is a URL and make it a clickable link
} if (ev.description.trim().startsWith('http')) {
let linkElement = document.createElement("a");
linkElement.href = ev.description.trim();
linkElement.textContent = ev.description.trim();
linkElement.target = "_blank";
eventDescription.innerHTML = '';
eventDescription.appendChild(linkElement);
} else {
eventDescription.textContent = ev.description;
}
eventItem.appendChild(eventDescription);
}
eventDetailsElem.appendChild(eventItem); eventDetailsElem.appendChild(eventItem);
}); });
} else { } else {
let noEvents = document.createElement("div"); let noEvents = document.createElement("div");
@ -334,37 +560,44 @@ menu:
function formatTime(icsTimeStr) { function formatTime(icsTimeStr) {
// Format time for display // Format time for display
if (!icsTimeStr) return "";
if (icsTimeStr.length === 8) { if (icsTimeStr.length === 8) {
// All-day event // All-day event
return "Ganztägig"; return "Ganztägig";
} else if (icsTimeStr.length >= 15) { } else if (icsTimeStr.includes("T")) {
// Time-specific event // Time-specific event (with or without timezone)
const hour = icsTimeStr.substring(9, 11); const timeStart = icsTimeStr.indexOf("T") + 1;
const minute = icsTimeStr.substring(11, 13); const hour = icsTimeStr.substring(timeStart, timeStart + 2);
const minute = icsTimeStr.substring(timeStart + 2, timeStart + 4);
return `${hour}:${minute}`; return `${hour}:${minute}`;
} }
return icsTimeStr; return icsTimeStr;
} }
function formatDateForICS(date) {
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
return `${year}${month}${day}`;
}
// ICS-Datei abrufen und Events verarbeiten // ICS-Datei abrufen und Events verarbeiten
fetch('/all.ics') fetch('/all.ics')
.then(response => response.text()) .then(response => response.text())
.then(data => { .then(data => {
events = parseICS(data); events = parseICS(data);
events.forEach(ev => {
let dateKey = parseDateString(ev.start); // Initialize with current date
if (dateKey) {
if (!eventsByDate[dateKey]) {
eventsByDate[dateKey] = [];
}
eventsByDate[dateKey].push(ev);
}
});
let today = new Date(); let today = new Date();
currentYear = today.getFullYear(); currentYear = today.getFullYear();
currentMonth = today.getMonth(); currentMonth = today.getMonth();
renderCalendar(currentYear, currentMonth);
// Process events for current month
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));
})(); })();
</script> </script>
Keinen Termin mehr verpeilen? Einfach den [Veranstaltungskalender abonnieren](all.ics)!

27
flake.lock Normal file
View file

@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1741332913,
"narHash": "sha256-ri1e8ZliWS3Jnp9yqpKApHaOo7KBN33W8ECAKA4teAQ=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "20755fa05115c84be00b04690630cb38f0a203ad",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-24.11",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

41
flake.nix Normal file
View file

@ -0,0 +1,41 @@
{
description = "A flake containing a development environment for the CCCB website.";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11";
};
outputs =
{ self, nixpkgs }:
let
forAllSystems =
f:
nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed (
system:
f rec {
pkgs = nixpkgs.legacyPackages.${system};
inherit system;
}
);
in
{
devShells = forAllSystems (
{ pkgs, ... }:
{
default = pkgs.mkShell rec {
packages = with pkgs; [
hugo
go
(pkgs.python3.withPackages (python-pkgs: [
python-pkgs.icalendar
python-pkgs.pytz
]))
shellcheck
];
};
}
);
formatter = forAllSystems ({ pkgs, ... }: pkgs.nixfmt-rfc-style);
};
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB