adjust styling and bump dates etc

This commit is contained in:
April John 2026-08-28 15:39:56 +02:00
commit a584a5252f
40 changed files with 678 additions and 294 deletions

View file

@ -30,21 +30,20 @@ This is the website of the CCCB.
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"* calendar on the home page is rendered by
post-processing in `./build.sh`, not by Hugo, so it is **not** visible under Hugo via the `calendar` shortcode, which fetches `all.ics` client-side.
`hugo serve`. To preview the fully-built site (including the home-page `all.ics` is generated from the individual event pages by `./tools/merge_cals.py`.
calendar), or to ready the site for upload, run: To preview the fully-built site (including `all.ics`), 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` runs `hugo`, generates `all.ics`, and copies it into `public/`.
`index.html` with the upcoming-events table. It depends on Python with the It depends on Python with the `icalendar`, `python-dateutil`, and `pytz`
`icalendar`, `python-dateutil`, and `pytz` packages, plus a `de_DE.UTF-8` packages. 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'`

View file

@ -11,10 +11,10 @@ speaker:
#speaker_url: #speaker_url:
date: {{ .Date }} date: {{ .Date }}
event: event:
- start: {{ .Date }} start: {{ .Date }}
- end: {{ .Date }} end: {{ .Date }}
location: CCCB location: CCCB
language: Englisch language: de
streaming: true streaming: true
#recording: https://media.ccc.de/v/dg-{{ .File.TranslationBaseName }} #recording: https://media.ccc.de/v/dg-{{ .File.TranslationBaseName }}
--- ---

View file

@ -36,15 +36,12 @@
border-collapse: collapse; border-collapse: collapse;
} }
#calendar-table th, #calendar-table td { #calendar-table th, #calendar-table td {
border: 1px; border: none;
padding: 5px; padding: 5px;
text-align: center; text-align: center;
vertical-align: top; vertical-align: top;
height: 50px; height: 50px;
width: 15%; width: 15%;
}
#calendar-table th
#calendar-table td {
position: relative; position: relative;
cursor: pointer; cursor: pointer;
} }
@ -83,8 +80,6 @@
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
box-shadow: 0 0 20px rgba(255, 255, 255, 0.1); box-shadow: 0 0 20px rgba(255, 255, 255, 0.1);
border-radius: 8px; border-radius: 8px;
width: 20px;
height: 20px;
border: 5px solid grey; border: 5px solid grey;
font-weight: lighter; font-weight: lighter;
position: relative; position: relative;

411
assets/css/custom.css Normal file
View file

@ -0,0 +1,411 @@
/* ── Self-hosted fonts ──────────────────────────────────────────────────── */
@font-face {
font-family: 'Press Start 2P';
src: url('/fonts/PressStart2P.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap;
}
/* ── Modern micro-improvements ─────────────────────────────────────────── */
/* Smooth scrolling */
html {
scroll-behavior: smooth;
}
/* Thin, modern scrollbar on WebKit */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
background: rgba(var(--color-neutral-500), 0.5);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(var(--color-primary-400), 0.7);
}
/* ── Typography ─────────────────────────────────────────────────────────── */
/* Tighter, more confident headings */
h1, h2, h3 {
letter-spacing: -0.02em;
}
/* Slightly warmer body text contrast in dark mode */
.dark .prose {
color: rgba(var(--color-neutral-200), 0.92);
}
/* ── Cards ──────────────────────────────────────────────────────────────── */
/* Lift + glow on card hover */
article.cursor-pointer,
.cursor-pointer[class*="backdrop"] {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
article.cursor-pointer:hover,
.cursor-pointer[class*="backdrop"]:hover {
transform: translateY(-3px);
box-shadow: 0 12px 32px rgba(var(--color-primary-400), 0.12);
}
/* ── Links ──────────────────────────────────────────────────────────────── */
/* Animated underline: grows from left on hover */
.prose a:not([class]) {
text-decoration: none;
background-image: linear-gradient(
rgba(var(--color-primary-400), 0.9),
rgba(var(--color-primary-400), 0.9)
);
background-size: 0 1.5px;
background-position: 0 100%;
background-repeat: no-repeat;
transition: background-size 0.2s ease;
}
.prose a:not([class]):hover {
background-size: 100% 1.5px;
}
/* ── Header ─────────────────────────────────────────────────────────────── */
/* Crisper blur on the fixed-fill-blur header */
#menu-blur {
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
background: rgba(var(--color-neutral-50), 0.85) !important;
}
.dark #menu-blur {
background: rgba(var(--color-neutral-900), 0.72) !important;
}
/* ── Hero / Background ──────────────────────────────────────────────────── */
/* Subtle gradient vignette over hero images */
.single_hero_background::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(
to bottom,
transparent 40%,
rgba(var(--color-neutral-200), 0.5) 100%
);
pointer-events: none;
}
.dark .single_hero_background::after {
background: linear-gradient(
to bottom,
transparent 40%,
rgba(var(--color-neutral-900), 0.6) 100%
);
}
/* ── Focus rings ────────────────────────────────────────────────────────── */
/* Modern, rounded focus ring */
:focus-visible {
outline: 2px solid rgba(var(--color-primary-400), 0.9);
outline-offset: 3px;
border-radius: 4px;
}
/* ── Buttons / Tags ─────────────────────────────────────────────────────── */
/* Pill-shaped tags with subtle glow on hover */
a[href*="/tags/"],
a[href*="/categories/"] {
border-radius: 9999px;
transition: box-shadow 0.2s ease, background 0.2s ease;
}
a[href*="/tags/"]:hover,
a[href*="/categories/"]:hover {
box-shadow: 0 0 0 3px rgba(var(--color-primary-400), 0.2);
}
/* ── Code blocks ────────────────────────────────────────────────────────── */
/* Rounded corners and a border on code blocks */
.dark .highlight {
border: 1px solid rgba(var(--color-neutral-700), 1);
border-radius: 8px;
overflow: hidden;
}
/* ── Retro-terminal layer ───────────────────────────────────────────────── */
/* Monospace headings + neon glow in dark mode */
.dark .prose h1,
.dark .prose h2,
.dark .prose h3 {
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace;
text-shadow:
0 0 8px rgba(var(--color-primary-400), 0.55),
0 0 28px rgba(var(--color-primary-400), 0.2),
0 0 2px rgba(192, 132, 252, 0.35);
letter-spacing: 0;
}
/* Scanline texture (dark mode only) */
.dark body::after {
content: "";
position: fixed;
inset: 0;
z-index: 9998;
pointer-events: none;
background: repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.04) 0px,
rgba(0, 0, 0, 0.04) 1px,
transparent 1px,
transparent 4px
);
}
/* Terminal window chrome on code blocks (traffic-light dots via radial gradients) */
.dark .highlight::before {
content: "";
display: block;
height: 28px;
background-color: rgba(var(--color-neutral-800), 1);
background-image:
radial-gradient(circle 5px at 16px 14px, #ff5f56 0%, #ff5f56 100%),
radial-gradient(circle 5px at 32px 14px, #ffbd2e 0%, #ffbd2e 100%),
radial-gradient(circle 5px at 48px 14px, #27c93f 0%, #27c93f 100%);
background-repeat: no-repeat;
border-bottom: 1px solid rgba(var(--color-neutral-700), 0.8);
}
/* Phosphor glow on hovered prose links in dark mode */
.dark .prose a:not([class]):hover {
text-shadow: 0 0 8px rgba(var(--color-primary-400), 0.4);
}
/* Cyan glow on card hover in dark mode */
.dark article.cursor-pointer:hover,
.dark .cursor-pointer[class*="backdrop"]:hover {
box-shadow: 0 12px 40px rgba(var(--color-primary-400), 0.18),
0 0 0 1px rgba(var(--color-primary-400), 0.1);
}
/* Slide-in "> " prompt before nav items on hover (dark mode) */
.dark .main-menu a p::before {
content: "> ";
display: inline-block;
max-width: 0;
overflow: hidden;
color: rgba(var(--color-primary-400), 0.65);
font-family: ui-monospace, monospace;
font-weight: normal;
transition: max-width 0.15s ease;
vertical-align: bottom;
}
.dark .main-menu a:hover p::before {
max-width: 1.5em;
}
/* Blinking block cursor keyframe (reusable) */
@keyframes cursor-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
/* ── Pixel font — site title ────────────────────────────────────────────── */
/* Navbar site title only the first nav (space-x-3) holds the site title link;
the menu items live in the second nav (hidden md:flex) */
.main-menu .flex-1 nav:first-of-type a {
font-family: 'Press Start 2P', ui-monospace, monospace !important;
font-size: 0.6rem !important;
transform: scaleY(1.4);
display: inline-block;
letter-spacing: 0 !important;
transition: color 0.2s ease, text-shadow 0.2s ease;
}
/* Homepage hero h1 (font-extrabold is set only on the background layout h1) */
h1.font-extrabold {
font-family: 'Press Start 2P', ui-monospace, monospace !important;
letter-spacing: 0 !important;
transition: color 0.2s ease, text-shadow 0.2s ease;
}
/* Scale down the homepage h1 — pixel fonts read best smaller */
h1.font-extrabold {
font-size: clamp(1rem, 2.5vw, 1.6rem) !important;
line-height: 1.8 !important;
}
/* Purple glow on hover — navbar */
.main-menu .flex-1 nav:first-of-type a:hover {
color: #c084fc !important;
text-shadow:
0 0 6px rgba(192, 132, 252, 0.9),
0 0 18px rgba(168, 85, 247, 0.6),
0 0 40px rgba(139, 92, 246, 0.3);
}
/* Purple glow on hover — homepage h1 */
h1.font-extrabold:hover {
color: #c084fc !important;
text-shadow:
0 0 8px rgba(192, 132, 252, 0.9),
0 0 24px rgba(168, 85, 247, 0.6),
0 0 50px rgba(139, 92, 246, 0.3);
cursor: default;
}
/* ── Cyberpunk layer ─────────────────────────────────────────────────────── */
/* Text selection — neon cyan highlight */
.dark ::selection {
background: rgba(34, 211, 238, 0.2);
color: rgba(var(--color-neutral-50), 1);
}
/* Cyberpunk background: ambient glows + drifting grid (dark mode) */
@keyframes grid-drift {
from { background-position: 0 0, 0 0, 0 0, 0 0; }
to { background-position: 0 0, 0 0, 48px 48px, 48px 48px; }
}
.dark body::before {
content: "";
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
background-image:
radial-gradient(ellipse 55vw 55vh at 88% 4%, rgba(34, 211, 238, 0.07), transparent),
radial-gradient(ellipse 45vw 45vh at 8% 96%, rgba(192, 132, 252, 0.06), transparent),
linear-gradient(rgba(34, 211, 238, 0.018) 1px, transparent 1px),
linear-gradient(90deg, rgba(34, 211, 238, 0.018) 1px, transparent 1px);
background-size: 100% 100%, 100% 100%, 48px 48px, 48px 48px;
animation: grid-drift 28s linear infinite;
}
/* Neon horizontal rule — cyan-to-magenta gradient */
.dark .prose hr {
border: none;
height: 1px;
background: linear-gradient(
90deg,
transparent,
rgba(34, 211, 238, 0.6) 20%,
rgba(192, 132, 252, 0.6) 50%,
rgba(34, 211, 238, 0.6) 80%,
transparent
);
box-shadow: 0 0 10px rgba(34, 211, 238, 0.25);
margin: 2em 0;
}
/* Glitch keyframes for site title */
@keyframes neon-glitch {
0%, 88%, 100% { text-shadow: inherit; }
89% {
text-shadow:
-2px 0 rgba(255, 0, 128, 0.85),
2px 0 rgba(34, 211, 238, 0.85);
}
91% {
text-shadow:
2px 0 rgba(255, 0, 128, 0.85),
-2px 0 rgba(34, 211, 238, 0.85);
}
93% { text-shadow: inherit; }
94% {
text-shadow:
-1px 0 rgba(255, 0, 128, 0.6),
1px 0 rgba(34, 211, 238, 0.6);
}
96% { text-shadow: inherit; }
}
/* Apply glitch to pixel-font site title in dark mode */
.dark .main-menu .flex-1 nav:first-of-type a {
animation: neon-glitch 9s infinite;
}
/* Neon card border glow — enhanced dual-color on hover */
.dark article.cursor-pointer:hover,
.dark .cursor-pointer[class*="backdrop"]:hover {
box-shadow:
0 12px 40px rgba(34, 211, 238, 0.22),
0 0 0 1px rgba(34, 211, 238, 0.18),
0 0 20px rgba(192, 132, 252, 0.08) inset;
}
/* Neon code-block border */
.dark .highlight {
border-color: rgba(34, 211, 238, 0.3);
box-shadow: 0 0 14px rgba(34, 211, 238, 0.07);
}
/* ── Fairydust rocket ───────────────────────────────────────────────────── */
.fairydust-wrapper {
position: fixed;
z-index: 9997;
pointer-events: none;
width: 40px;
overflow: visible;
}
.fairydust-wrapper.go-right {
left: -100px;
animation: fairydust-fly-right var(--fly-duration, 5s) linear forwards;
}
.fairydust-wrapper.go-left {
right: -100px;
animation: fairydust-fly-left var(--fly-duration, 5s) linear forwards;
}
/* Comet trail */
.fairydust-wrapper::after {
content: "";
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 130px;
height: 6px;
border-radius: 3px;
}
.fairydust-wrapper.go-right::after {
right: 100%;
background: linear-gradient(to right, transparent, rgba(232, 121, 249, 0.5));
}
.fairydust-wrapper.go-left::after {
left: 100%;
background: linear-gradient(to left, transparent, rgba(232, 121, 249, 0.5));
}
.fairydust-rocket {
width: 40px;
height: 80px;
animation: fairydust-glow 0.85s ease-in-out infinite alternate;
}
@keyframes fairydust-glow {
from {
filter:
drop-shadow(0 0 5px rgba(232, 121, 249, 0.8))
drop-shadow(0 0 14px rgba(232, 121, 249, 0.4));
}
to {
filter:
drop-shadow(0 0 10px rgba(232, 121, 249, 1))
drop-shadow(0 0 26px rgba(168, 85, 247, 0.65));
}
}
@keyframes fairydust-fly-right {
from { left: -100px; }
to { left: calc(100vw + 100px); }
}
@keyframes fairydust-fly-left {
from { right: -100px; }
to { right: calc(100vw + 100px); }
}

View file

@ -0,0 +1,37 @@
/* Fira scheme — deep navy + cyan + emerald */
:root {
--color-neutral: 255, 255, 255;
/* Deep cool-slate — richer than pure black in dark mode */
--color-neutral-50: 247, 250, 252;
--color-neutral-100: 234, 241, 248;
--color-neutral-200: 210, 225, 238;
--color-neutral-300: 168, 196, 218;
--color-neutral-400: 120, 148, 178;
--color-neutral-500: 80, 105, 135;
--color-neutral-600: 50, 70, 100;
--color-neutral-700: 32, 48, 70;
--color-neutral-800: 20, 30, 48;
--color-neutral-900: 10, 15, 25;
/* Cyan — electric, technical */
--color-primary-50: 236, 254, 255;
--color-primary-100: 207, 250, 254;
--color-primary-200: 165, 243, 252;
--color-primary-300: 103, 232, 249;
--color-primary-400: 34, 211, 238;
--color-primary-500: 6, 182, 212;
--color-primary-600: 8, 145, 178;
--color-primary-700: 14, 116, 144;
--color-primary-800: 21, 94, 117;
--color-primary-900: 22, 78, 99;
/* Emerald — nod to CCC's green */
--color-secondary-50: 240, 253, 244;
--color-secondary-100: 220, 252, 231;
--color-secondary-200: 187, 247, 208;
--color-secondary-300: 134, 239, 172;
--color-secondary-400: 74, 222, 128;
--color-secondary-500: 34, 197, 94;
--color-secondary-600: 22, 163, 74;
--color-secondary-700: 21, 128, 61;
--color-secondary-800: 22, 101, 52;
--color-secondary-900: 20, 83, 45;
}

View file

@ -35,6 +35,8 @@ document.addEventListener('DOMContentLoaded', function() {
event.description = value; event.description = value;
} else if (baseKey === "RRULE") { } else if (baseKey === "RRULE") {
event.rrule = value; event.rrule = value;
} else if (baseKey === "URL") {
event.url = value;
} }
} }
} }
@ -281,13 +283,11 @@ document.addEventListener('DOMContentLoaded', function() {
const events = eventsByDate[dateKey]; const events = eventsByDate[dateKey];
const hasMembersOnly = events.some(e => e.summary.toLowerCase().includes("members only")); const hasMembersOnly = events.some(e => e.summary.toLowerCase().includes("members only"));
const hasSubbotnik = events.some(e => e.summary.toLowerCase().includes("subbotnik")); 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 hasSpieleabend = events.some(e => e.summary.toLowerCase().includes("spieleabend"));
const hasRegular = events.some(e => { const hasRegular = events.some(e => {
const title = e.summary.toLowerCase(); const title = e.summary.toLowerCase();
return !title.includes("members only") && return !title.includes("members only") &&
!title.includes("subbotnik") && !title.includes("subbotnik") &&
!title.includes("bastelabend") &&
!title.includes("spieleabend"); !title.includes("spieleabend");
}); });
@ -297,7 +297,7 @@ document.addEventListener('DOMContentLoaded', function() {
dot.className = "event-dot event-dot-red"; dot.className = "event-dot event-dot-red";
dotsContainer.appendChild(dot); dotsContainer.appendChild(dot);
} }
if (hasSubbotnik || hasBastelabend || hasSpieleabend) { if (hasSubbotnik || hasSpieleabend) {
let dot = document.createElement("div"); let dot = document.createElement("div");
dot.className = "event-dot event-dot-orange"; dot.className = "event-dot event-dot-orange";
dotsContainer.appendChild(dot); dotsContainer.appendChild(dot);
@ -329,20 +329,6 @@ document.addEventListener('DOMContentLoaded', function() {
calendarBody.appendChild(row); calendarBody.appendChild(row);
} }
function createEventLink(eventTitle) {
if (eventTitle.startsWith("Datengarten")) {
// Extract the number after "Datengarten "
const match = eventTitle.match(/Datengarten\s+(\d+)/i);
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}/`;
}
function showEventDetails(dateKey) { function showEventDetails(dateKey) {
const events = eventsByDate[dateKey]; const events = eventsByDate[dateKey];
eventDateElem.textContent = formatDate(dateKey); eventDateElem.textContent = formatDate(dateKey);
@ -357,10 +343,16 @@ document.addEventListener('DOMContentLoaded', function() {
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("h"); if (ev.url) {
titleLink.textContent = ev.summary; let titleLink = document.createElement("a");
titleLink.target = "_blank"; titleLink.textContent = ev.summary;
eventTitle.appendChild(titleLink); titleLink.href = ev.url;
titleLink.target = "_blank";
titleLink.rel = "noopener noreferrer";
eventTitle.appendChild(titleLink);
} else {
eventTitle.textContent = ev.summary;
}
eventItem.appendChild(eventTitle); eventItem.appendChild(eventTitle);

View file

@ -1,25 +1,21 @@
const spaceapiUrl = "https://spaceapi.hamburg.ccc.de" const spaceapiUrl = "https://spaceapi.hamburg.ccc.de"
const interval_ms = 60000 const interval_ms = 60000
console.log("clubstatus.js geladen!"); /* TODO: ENTFERNEN */
function timeDistanceDE(pastDate) { function timeDistanceDE(pastDate) {
const seconds = Math.floor((Date.now() - pastDate.getTime()) / 1000) const seconds = Math.floor((Date.now() - pastDate.getTime()) / 1000)
const minutes = Math.floor(seconds / 60) const minutes = Math.floor(seconds / 60)
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 `seit ${days}d`
if (days > 1) return `since ${days}d` if (hours >= 1) return `seit ${hours}h`
if (hours = 1) return `since ${hours}h` if (minutes >= 1) return `seit ${minutes}min`
if (hours > 1) return `since ${hours}h`
if (minutes = 1) return `since ${minutes}min`
if (minutes > 1) return `since ${minutes}min`
return `gerade eben` return `gerade eben`
} }
function updateRoomState(openState, lastChangeTimestamp) { function updateRoomState(openState, lastChangeTimestamp) {
const roomstate = document.querySelector('#clubstatus-badge') const roomstate = document.querySelector('#clubstatus-badge')
if (!roomstate) return
const statusItem = roomstate.querySelector(".state") const statusItem = roomstate.querySelector(".state")
const durationItem = roomstate.querySelector(".duration") const durationItem = roomstate.querySelector(".duration")
@ -30,17 +26,20 @@ function updateRoomState(openState, lastChangeTimestamp) {
if (openState === null || lastChangeTimestamp === null) { if (openState === null || lastChangeTimestamp === null) {
statusItem.textContent = "unbekannt" statusItem.textContent = "unbekannt"
roomstate.classList.add("unknown")
statusItem.classList.add("unknown") statusItem.classList.add("unknown")
durationItem.textContent = "(since unbekannt)" durationItem.textContent = "(seit unbekannt)"
} else { } else {
const changeDate = new Date(lastChangeTimestamp * 1000) const changeDate = new Date(lastChangeTimestamp * 1000)
const sinceText = timeDistanceDE(changeDate) const sinceText = timeDistanceDE(changeDate)
if (openState) { if (openState) {
statusItem.textContent = "open" statusItem.textContent = "open"
roomstate.classList.add("open")
statusItem.classList.add("open") statusItem.classList.add("open")
} else { } else {
statusItem.textContent = "closed" statusItem.textContent = "closed"
roomstate.classList.add("closed")
statusItem.classList.add("closed") statusItem.classList.add("closed")
} }

62
assets/js/fairydust.js Normal file
View file

@ -0,0 +1,62 @@
(function () {
'use strict';
var SVG = [
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 80" width="40" height="80">',
// Exhaust flame
'<path d="M13,65 Q20,80 27,65" fill="#f97316" opacity="0.85"/>',
'<path d="M15,63 Q20,74 25,63" fill="#fbbf24"/>',
// Fins
'<path d="M11,53 L3,70 L14,58" fill="#a855f7"/>',
'<path d="M29,53 L37,70 L26,58" fill="#a855f7"/>',
// Body
'<ellipse cx="20" cy="42" rx="11" ry="22" fill="#e879f9"/>',
// Nose cone
'<path d="M20,10 L10,32 L30,32 Z" fill="#c026d3"/>',
// Window
'<circle cx="20" cy="44" r="7" fill="#0ea5e9" opacity="0.82"/>',
'<circle cx="22" cy="41" r="2.5" fill="white" opacity="0.35"/>',
// Body highlight
'<ellipse cx="15" cy="36" rx="3" ry="8" fill="white" opacity="0.15" transform="rotate(-15,15,36)"/>',
'</svg>'
].join('');
function isDark() {
return document.documentElement.classList.contains('dark');
}
function launch() {
if (!isDark()) { scheduleNext(); return; }
var goRight = Math.random() > 0.5;
var topPct = 6 + Math.random() * 72;
var duration = (3.5 + Math.random() * 4).toFixed(1);
var wrapper = document.createElement('div');
wrapper.className = 'fairydust-wrapper ' + (goRight ? 'go-right' : 'go-left');
wrapper.style.top = topPct + 'vh';
wrapper.style.setProperty('--fly-duration', duration + 's');
var rocket = document.createElement('div');
rocket.className = 'fairydust-rocket';
rocket.style.transform = 'rotate(' + (goRight ? 90 : -90) + 'deg)';
rocket.innerHTML = SVG;
wrapper.appendChild(rocket);
document.body.appendChild(wrapper);
wrapper.addEventListener('animationend', function (e) {
if (e.animationName === 'fairydust-fly-right' ||
e.animationName === 'fairydust-fly-left') {
wrapper.remove();
}
});
scheduleNext();
}
function scheduleNext() {
setTimeout(launch, 250000 + Math.random() * 950000); // 2501200 s
}
})();

View file

@ -7,6 +7,4 @@ rm -rf public/ # delete old generated files
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' ' ')"
cp static/all.ics public/all.ics cp static/all.ics public/all.ics
sed -i "s#CALENDAR#$upcoming#g" public/index.html

View file

@ -1,7 +1,6 @@
theme = "blowfish" theme = "blowfish"
baseURL = "/" baseURL = "/"
defaultContentLanguage = "de" defaultContentLanguage = "de"
relativeURLs = true
canonifyURLs = true canonifyURLs = true
pluralizeListTitles = false pluralizeListTitles = false
enableRobotsTXT = true enableRobotsTXT = true

View file

@ -28,7 +28,7 @@ giteaDefaultServer = "https://git.fsfe.org"
forgejoDefaultServer = "https://git.berlin.ccc.de" forgejoDefaultServer = "https://git.berlin.ccc.de"
[header] [header]
layout = "basic" # valid options: basic, fixed, fixed-fill, fixed-gradient, fixed-fill-blur layout = "fixed-fill-blur" # valid options: basic, fixed, fixed-fill, fixed-gradient, fixed-fill-blur
[footer] [footer]
showMenu = true showMenu = true
@ -46,7 +46,7 @@ showMoreLink = true
showMoreLinkDest = "/post/" showMoreLinkDest = "/post/"
cardView = true cardView = true
cardViewScreenWidth = false cardViewScreenWidth = false
layoutBackgroundBlur = false # only used when layout equals background layoutBackgroundBlur = true # only used when layout equals background
[article] [article]
showDate = true showDate = true
@ -85,14 +85,14 @@ showHero = false
layoutBackgroundBlur = true # only used when heroStyle equals background or thumbAndBackground layoutBackgroundBlur = true # only used when heroStyle equals background or thumbAndBackground
layoutBackgroundHeaderSpace = true # only used when heroStyle equals background layoutBackgroundHeaderSpace = true # only used when heroStyle equals background
showBreadcrumbs = false showBreadcrumbs = false
showSummary = false showSummary = true
showViews = false showViews = false
showLikes = false showLikes = false
showTableOfContents = false showTableOfContents = false
showCards = false showCards = true
orderByWeight = false orderByWeight = false
groupByYear = true groupByYear = true
cardView = false cardView = true
cardViewScreenWidth = false cardViewScreenWidth = false
constrainItemsWidth = false constrainItemsWidth = false

View file

@ -25,6 +25,6 @@ description: "Startseite CCCB mit Kurzkalender"
### Nächste Veranstaltungen ### Nächste Veranstaltungen
CALENDAR {{< calendar >}}
Weitere Termine findest du im [Veranstaltungskalender](/verein/calendar/). Weitere Termine findest du im [Veranstaltungskalender](/verein/calendar/).

View file

@ -1,6 +1,6 @@
--- ---
title: "Der Neujahresempfang 2026 #NJE26" title: "Der Neujahresempfang 2026 #NJE26"
date: 2026-03-05T12:00:00+01:00 date: 2026-05-23T12:00:00+01:00
showHero: false showHero: false
tags: ["NJE", "Ankündigung"] tags: ["NJE", "Ankündigung"]

View file

@ -1,130 +0,0 @@
---
title: "Oma Paulsen erzählt von früher"
date: 2018-05-20T23:16:30+02:00
menu:
main:
parent: "sonstiges"
draft: true
---
Rundfunk
--------
- [Bilder zur Geschichte des Rundfunks bis
1945](http://www.dra.de/galerie.htm) Bilder DRA
- [Rundfunkwerbung der dreißiger Jahre von Elly
Heuss-Knapp](http://www.dra.de/dok_0504.htm)
- [Hans Fritsche. Rundfunk im totalen Krieg. (Reichsrundfunk 1944/45,
Heft 13, S
135ff.)](http://www.mediaculture-online.de/fileadmin/bibliothek/fritzsche_total/fritzsche_total.pdf)
(PDF)
- [Verbot des Niggerjazz. Teil einer auf der Intendantentagung am 12.
Oktober 1935 gehaltenen
Rede.](http://www.mediaculture-online.de/fileadmin/bibliothek/hadamovsky_jazz/hadamovsky_jazz.pdf)
(PDF)
- [Zensur im Rundfunk. Niederschrift zur Sitzung des Programmrates des
deutschen Rundfunks aus dem Jahr
1929](http://www.mediaculture-online.de/fileadmin/bibliothek/hardt_zensur/hardt_zensur.pdf)
(PDF)
- [Calling .... 2LO
Calling](http://www.sciencemuseum.org.uk/exhibitions/2lo/) The set
of equipment which transmitted the BBCs first-ever programme
Computer
--------
- [Fördermaßnahmen zur Etablierung der Datenverarbeitung in
(West-)Deutschland. Von Konrad Zuse bis zum 1. DV-Programm des
Bundes 1967.](http://www.susas.de/computergeschichte_02.htm)
- [Informatik und Rechentechnik in der
DDR](http://waste.informatik.hu-berlin.de/robotron/studienarbeit/index.html)
- [EDV-Einführung in der DDR in der Epoche des "Neuen ökonomischen
Systems](http://www.susas.de/einfuehrung_edv_ddr_r300/index.htm)
- [Kleincomputer in der
DDR](http://waste.informatik.hu-berlin.de/robotron/diplom/texte/einleitung/einleitung.html)
- [Computersendungen im Rundfunk der
DDR](http://waste.informatik.hu-berlin.de/robotron/studienarbeit/files/literatur/rundfunk/rundfunk.html)
- [Computergeschichte und
Militär](http://www.susas.de/computer_militaer/index.htm)
- [Computer während Weltkrieg
zwei](http://robotron.informatik.hu-berlin.de/WW2/default.html)
- [Adam Thornton: A Brief History of the Rice Computer.
1959-1971](http://www.princeton.edu/~adam/R1/r1rpt.html)
- [Ivan Sutherland. Sketchpad: A man-machine graphical communication
system. Massachusetts: Ph. D. Thesis,
1963](http://www.cl.cam.ac.uk/TechReports/UCAM-CL-TR-574.pdf)
elektronische Version, 2003 (PDF)
- [Jeff Johnson, Teresa L. Roberts: The Xerox "Star": A
Retrospective](http://digibarn.com/friends/curbow/star/retrospect/index.html)
- [David Canfield Smith et al.Designing the Star User
Interface](http://www.aci.com.pl/mwichary/guidebook/articles/designingthestaruserinterface)
- [Alan C. Kay. The Early History of
Smalltalk](http://gagne.homedns.org/~tgagne/contrib/EarlyHistoryST.html)
- [Alan Kay: Doing with Images Makes Symbols Pt
1](http://www.archive.org/movies/details-db.php?collection=opensource_movies&collectionid=AV_Geek_Skip_20021212061248)
- [Alan Kay: Doing with Images Makes Symbols Pt
2](http://www.archive.org/movies/details-db.php?collection=opensource_movies&collectionid=AV_Geek_Skip_20021212071234)
- [Adele Goldberg, Alan C. Kay. Smalltalk72 Instruction
Manual](http://bitsavers.org/pdf/xerox/alto/Smalltalk72_Manual.pdf)
(PDF)
- [Licklider, J. C. R. Man-Computer Symbiosis. IRE Transactions on
Human Factors in Electronics, HFE-1, 4-11,
1960](http://memex.org/licklider.pdf) (PDF)
- [Licklider, J. C. R., & Taylor, R. The Computer as a Communication
Device. Science and Technology (4), 21-41,
1968](http://memex.org/licklider.pdf)
- [William Henry Gates. An Open Letter to Hobbyists. February 3,
1976](http://www.blinkenlights.com/classiccmp/gateswhine.html)
- [Dennis Mocigemba. Ideengeschichte der Computernutzung. Metaphern
der Computernutzung und
Qualitätssicherungsstrategien](http://edocs.tu-berlin.de/diss/2003/mocigemba_dennis.htm)
- [Computer
Chronicles](http://www.archive.org/movies/computerchronicles.php) -
618 episodes
Roboter
-------
- [Leave It to
Roll-Oh](http://www.archive.org/movies/details-db.php?collection=prelinger&collectionid=07898)
Tongue-in-cheek film showing a domestic robot freeing housewives of
their chores (and intimating that their work is hardly necessary);
actually a promo showing how relays and switches function in the
modern automobile. Shown at the New York World's Fair in 1940.
(1940)
- [Robots makes
radios](http://www.sciencemuseum.org.uk/on-line/ecme/) (1947)
Vermischtes
-----------
- [Der Schwarze Kanal 1960-1989](http://dra.orb.de/) digitalisierte
Sendemanuskripte
- [Technikgeschichte](http://fotothek.slub-dresden.de/index.html?/technik/technik.html)
Deutsche Fothothek, SUB Dresden
- [The Early History of Talking
Machines](http://www.haskins.yale.edu/haskins/HEADS/simulacra.html)
- ["Die telegraphischen Verbindungen Deutschlands mit seinen Kolonien"
von Ober Postpraktikant H. Thurn
Koblenz](http://www.jaduland.de/kolonien/text/televerbindung.html)
- [Die modernen Medien der Weimarer
Republik](http://www.ralf-hecht.de/weimar/)
- [120 Years of Electronic Music](http://www.obsolete.com/120_years/)
Electronic Musical Instrument 1870 - 1990
- [Verbotene Bilder, manipulierte Filme
(I+II).](http://www.deutsches-filminstitut.de/dt2tai01.htm) Zur
Edition der Zensurentscheidungen der Berliner Film-Oberprüfstelle
aus den Jahren 1920 bis 1938
Filme aus dem Archiv der CBC (in behämmertem Format)
- [A Network called
"Internet"](http://archives.cbc.ca/IDC-1-75-710-4205/science_technology/computers/) 8.
Oktober 1993
- [Kids and their
Computers](http://archives.cbc.ca/400d.asp?id=1-75-710-4186) 26.
Oktober 1982
- [IDA, der
Geldautomat](http://archives.cbc.ca/400d.asp?id=1-75-710-4201) 12.
April 1968

View file

@ -1,32 +0,0 @@
---
title: "Bastelabend"
subtitle: "Der Bastelabend jeden 1. und 3. Samstag im Monat"
date: 2025-01-08T20:00:00+02:00
dtstart: 20250118T170000
dtend: 20250118T230000
rrule: "FREQ=MONTHLY;BYDAY=1SA,3SA;WKST=MO"
menu:
main:
parent: "Veranstaltungen"
tag: ["Veranstaltung"]
draft: true
---
{{< 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.**
{{< alert "circle-info" >}}
Wenn ihr neu seid und den CCCB zum ersten Mal besuchen wollt, kommt am besten an einem Donnerstag zum [Club Discordia](/veranstaltungen/clubdiscordia/), da samstags nicht immer genug Leute da sind, um euch zu empfangen.
Generell sind aber alle, die schon mal im Club waren, herzlich eingeladen, an den Bastelabenden vorbeizukommen.
{{< /alert >}}

View file

@ -19,7 +19,7 @@ tag: ["Veranstaltung"]
caption="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 zeichnen
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
oder auf CCC-Veranstaltungen statt. Dies kündigen wir im Vorfeld separat an. oder auf CCC-Veranstaltungen statt. Dies kündigen wir im Vorfeld separat an.

View file

@ -42,7 +42,7 @@ Wissen der Menschheit. All diese Ziele werden auch vom Gesamtkonzept
Internet getragen, weshalb es dem CCC naheliegt, sich genau dort als Internet getragen, weshalb es dem CCC naheliegt, sich genau dort als
öffentlicher Ansprechpartner einzufinden, wo das Internet als Werkzeug öffentlicher Ansprechpartner einzufinden, wo das Internet als Werkzeug
im Alltag überall Einzug hält. Von: im Alltag überall Einzug hält. Von:
\[[München](http://schule.muc.ccc.de/%5DCCC) [München CCC Schule](http://schule.muc.ccc.de/)
Regelmäßige Treffen Regelmäßige Treffen
------------------- -------------------
@ -208,7 +208,7 @@ Mailingliste
Die Mailingliste richtet sich an alle, die bei Chaos macht Schule in Die Mailingliste richtet sich an alle, die bei Chaos macht Schule in
Berlin aktiv sind. Subscribe-Anfragen von uns nicht bekannten Personen Berlin aktiv sind. Subscribe-Anfragen von uns nicht bekannten Personen
können wir deshalb leider nicht annehmen. Falls wir dich nicht nicht können wir deshalb leider nicht annehmen. Falls wir dich nicht
kennen, komm doch gerne mal bei einem unserer Treffen vorbei. kennen, komm doch gerne mal bei einem unserer Treffen vorbei.
- **Mitglieder-Liste:** <chaos-macht-schule@berlin.ccc.de> - **Mitglieder-Liste:** <chaos-macht-schule@berlin.ccc.de>

View file

@ -1,7 +1,7 @@
--- ---
title: "Neujahresempfang" title: "Neujahresempfang"
subtitle: "#NJE 26" subtitle: "#NJE 26"
date: 2025-03-05T18:00:00+02:00 date: 2026-05-23T18:00:00+02:00
dtstart: 20260523T180000 dtstart: 20260523T180000
dtend: 20260524T100000 dtend: 20260524T100000
menu: menu:

View file

@ -2,16 +2,16 @@
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: 20180710T200000
dtend: 20180704T220000 dtend: 20180710T220000
rrule: "FREQ=MONTHLY;BYDAY=2TU,4TU;INTERVAL=1;WKST=MO" rrule: "FREQ=MONTHLY;BYDAY=2TU,4TU;INTERVAL=1;WKST=MO"
tags: ["Mitgliedschaft", "Veranstaltung"] members_only: true
draft: true tag: ["Mitgliedschaft", "Veranstaltung"]
--- ---
Das Plenum findet in der Regel am 2. und 4. Dienstag im Monat ab 20:00 Uhr in den Clubräumen Das Plenum findet in der Regel am 2. und 4. Dienstag im Monat ab 20:00 Uhr in den Clubräumen
des CCCB statt. des CCCB statt.
Als Instrument der internen Konsensbildung, bei dem Interna diskutiert werden, Als Instrument der internen Konsensbildung, bei dem Interna diskutiert werden,
richtet sich das Plenum **aussließlich an Mitglieder oder explizit eingeladene Gäste**. richtet sich das Plenum **ausschließlich an Mitglieder oder explizit eingeladene Gäste**.

View file

@ -2,8 +2,8 @@
title: "Spieleabend" title: "Spieleabend"
subtitle: "Der Spieleabend jeden 2. und 4. Samstag im Monat" subtitle: "Der Spieleabend jeden 2. und 4. Samstag im Monat"
date: 2025-01-08T20:00:00+02:00 date: 2025-01-08T20:00:00+02:00
dtstart: 20250125T170000 dtstart: 20250111T170000
dtend: 20250125T230000 dtend: 20250111T230000
rrule: "FREQ=MONTHLY;BYDAY=2SA,4SA;WKST=MO" rrule: "FREQ=MONTHLY;BYDAY=2SA,4SA;WKST=MO"
menu: menu:
main: main:

View file

@ -9,7 +9,6 @@ menu:
main: main:
parent: "Veranstaltungen" parent: "Veranstaltungen"
tag: ["Veranstaltung"] tag: ["Veranstaltung"]
draft: true
--- ---
![Ein possierlicher Nager im CCCB](/img/club/41627141884_48e4bb4dec.jpg) ![Ein possierlicher Nager im CCCB](/img/club/41627141884_48e4bb4dec.jpg)

13
dev.sh Executable file
View file

@ -0,0 +1,13 @@
#!/bin/sh
set -e
# Regenerate the merged calendar so the JS calendar widget has fresh data
./tools/merge_cals.py 2>/dev/null || echo "Note: merge_cals.py skipped (no public/ dir yet)"
hugo server \
--buildDrafts \
--buildFuture \
--disableFastRender \
--bind 0.0.0.0 \
--port 1313

View file

@ -64,9 +64,7 @@
hugo --baseURL=https://${domain}/ hugo --baseURL=https://${domain}/
python3 ./tools/merge_cals.py 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 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

View file

@ -2,10 +2,10 @@
<events> <events>
{{ range .Pages }} {{ range .Pages }}
<event> <event>
<title>{{ .Title }}</title> <title>{{ .Title | html }}</title>
<link>{{ .Permalink }}</link> <link>{{ .Permalink }}</link>
<date>{{ .Date.Format "2006-01-02" }}</date> <date>{{ .Date.Format "2006-01-02" }}</date>
<description>{{ .Summary | plainify }}</description> <description>{{ .Summary | plainify | html }}</description>
{{ with .Params.location }}<location>{{ . }}</location>{{ end }} {{ with .Params.location }}<location>{{ . }}</location>{{ end }}
</event> </event>
{{ end }} {{ end }}

View file

@ -0,0 +1,21 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//CCCB//Calendar//DE
{{ with .Title }}
X-WR-CALNAME:{{ . }}
{{ end }}
CALSCALE:GREGORIAN
METHOD:PUBLISH
{{ if and .Date (not .Params.members_only) }}
BEGIN:VEVENT
UID:{{ .File.UniqueID }}@berlin.ccc.de
DTSTAMP:{{ .Date.UTC.Format "20060102T150405Z" }}
DTSTART:{{ .Date.UTC.Format "20060102T150405Z" }}
{{ with .Params.event.end }}DTEND:{{ dateFormat "20060102T150405Z" . }}{{ end }}
SUMMARY:{{ .Title }}
DESCRIPTION:{{ .Summary | plainify }}
URL:{{ .Permalink }}
{{ with .Params.location }}LOCATION:{{ . }}{{ end }}
END:VEVENT
{{ end }}
END:VCALENDAR

View file

@ -21,6 +21,7 @@ END:STANDARD
END:VTIMEZONE END:VTIMEZONE
{{ range .Pages }} {{ range .Pages }}
{{ if and .Params.event.start .Params.event.end -}}
BEGIN:VEVENT BEGIN:VEVENT
ORGANIZER;CN="CCCB":mailto:datengarten@berlin.ccc.de ORGANIZER;CN="CCCB":mailto:datengarten@berlin.ccc.de
SUMMARY:{{ .Title }}{{ with .Params.subtitle }}: {{ . }}{{ end }} SUMMARY:{{ .Title }}{{ with .Params.subtitle }}: {{ . }}{{ end }}
@ -34,5 +35,6 @@ LOCATION:{{ with .Params.location }}{{ . }}{{ else }}CCCB{{ end }}
URL:{{ .Permalink }} URL:{{ .Permalink }}
DESCRIPTION:{{ .Permalink }} DESCRIPTION:{{ .Permalink }}
END:VEVENT END:VEVENT
{{ end -}}
{{ end }} {{ end }}
END:VCALENDAR END:VCALENDAR

View file

@ -11,21 +11,21 @@
{{range .Pages.ByDate -}} {{range .Pages.ByDate -}}
<day index="{{.Params.no}}" date="{{dateFormat "2006-01-02" .Params.event.start}}" start="{{dateFormat "2006-01-02T15:04:05" .Params.event.start}}" end="{{dateFormat "2006-01-02T15:04:05" .Params.event.end}}"> <day index="{{.Params.no}}" date="{{dateFormat "2006-01-02" .Params.event.start}}" start="{{dateFormat "2006-01-02T15:04:05" .Params.event.start}}" end="{{dateFormat "2006-01-02T15:04:05" .Params.event.end}}">
<room name="Saal23"> <room name="Saal23">
<event id="{{.Params.no}}" guid="456fb60c-5ce7-11e8-be47-1fb2912eb8{{.Params.no}}"> <event id="{{.Params.no}}" guid="456fb60c-5ce7-11e8-be47-{{ printf "%012x" .Params.no }}">
<date>{{dateFormat "2006-01-02T15:04:05" .Params.event.start}}</date> <date>{{dateFormat "2006-01-02T15:04:05" .Params.event.start}}</date>
<start>{{dateFormat "15:04:05" .Params.event.start}}</start> <start>{{dateFormat "15:04:05" .Params.event.start}}</start>
<duration>{{ $end := time .Params.event.end }}{{ $t := time "1970-01-01"}}{{ dateFormat "15:04" ( $t.Add ( $end.Sub (time .Params.event.start) ) ) }}</duration> <duration>{{ $end := time .Params.event.end }}{{ $t := time "1970-01-01"}}{{ dateFormat "15:04" ( $t.Add ( $end.Sub (time .Params.event.start) ) ) }}</duration>
<room>Saal23</room> <room>Saal23</room>
<slug>dg-{{.Params.no}}</slug> <slug>dg-{{.Params.no}}</slug>
<title>{{htmlEscape .Title}}</title> <title>{{.Title | html}}</title>
<subtitle>{{with .Params.subtitle}}{{htmlEscape .}}{{end}}</subtitle> <subtitle>{{with .Params.subtitle}}{{. | html}}{{end}}</subtitle>
<track/> <track/>
<type/> <type/>
<language>{{.Params.language}}</language> <language>{{.Params.language}}</language>
<abstract><![CDATA[{{htmlUnescape .Plain|safeHTML}}]]></abstract> <abstract><![CDATA[{{.Plain | safeHTML}}]]></abstract>
<description><![CDATA[{{htmlUnescape .Plain|safeHTML}}]]></description> <description><![CDATA[{{.Plain | safeHTML}}]]></description>
<persons> <persons>
<person id="0">{{htmlEscape .Params.speaker}}</person> <person id="0">{{.Params.speaker | html}}</person>
</persons> </persons>
<links/> <links/>
</event> </event>

View file

@ -25,7 +25,7 @@ SUMMARY:{{.Title}}
UID:{{lower (replace .Title " " "")}}@berlin.ccc.de UID:{{lower (replace .Title " " "")}}@berlin.ccc.de
SEQUENCE:0 SEQUENCE:0
STATUS:CONFIRMED STATUS:CONFIRMED
DTSTAMP:20180508T200000Z DTSTAMP:{{ now.Format "20060102T150405Z" }}
DTSTART;TZID=Europe/Berlin:{{.Params.dtstart}} DTSTART;TZID=Europe/Berlin:{{.Params.dtstart}}
DTEND;TZID=Europe/Berlin:{{.Params.dtend}} DTEND;TZID=Europe/Berlin:{{.Params.dtend}}
RRULE:{{.Params.rrule}} RRULE:{{.Params.rrule}}

View file

@ -0,0 +1,2 @@
{{ $fairydust := resources.Get "js/fairydust.js" | resources.Minify | resources.Fingerprint "sha512" }}
<script src="{{ $fairydust.RelPermalink }}" integrity="{{ $fairydust.Data.Integrity }}" defer></script>

View file

@ -52,7 +52,7 @@
</div> </div>
<script> <script>
{{ if not .Site.Params.disableImageZoom | default true }} {{ if not .Site.Params.disableImageZoom }}
mediumZoom(document.querySelectorAll("img:not(.nozoom)"), { mediumZoom(document.querySelectorAll("img:not(.nozoom)"), {
margin: 24, margin: 24,
background: 'rgba(0,0,0,0.5)', background: 'rgba(0,0,0,0.5)',

View file

@ -5,20 +5,20 @@
<strong><i class="far fa-user fa-fw"></i>👤 Speaker:</strong> {{ .Page.Params.Speaker }}<br/> <strong><i class="far fa-user fa-fw"></i>👤 Speaker:</strong> {{ .Page.Params.Speaker }}<br/>
{{ end }} {{ end }}
<strong><i class="far fa-calendar fa-fw"></i>🗓️ Date:</strong> {{ dateFormat "02.01.2006, 15:04" .Page.Params.Event.start }} Uhr<br/> <strong><i class="far fa-calendar fa-fw"></i>🗓️ Date:</strong> {{ dateFormat "02.01.2006, 15:04" .Page.Params.Event.start }} Uhr<br/>
{{ with (eq $.Page.Params.Language "de") }} {{ if eq $.Page.Params.Language "de" }}
<strong><i class="fa fa-language fa-fw"></i>🌐 Language:</strong> Deutsch/German<br/> <strong><i class="fa fa-language fa-fw"></i>🌐 Language:</strong> Deutsch/German<br/>
{{ end }} {{ end }}
{{ with (eq $.Page.Params.Language "en") }} {{ if eq $.Page.Params.Language "en" }}
<strong><i class="fa fa-language fa-fw"></i>🌐 Language:</strong> Englisch/English<br/> <strong><i class="fa fa-language fa-fw"></i>🌐 Language:</strong> Englisch/English<br/>
{{ end }} {{ end }}
<strong><i class="fa fa-map-marker fa-fw"></i>📍 Location:</strong> {{ partial "location" .Page.Params.Location }}<br/> <strong><i class="fa fa-map-marker fa-fw"></i>📍 Location:</strong> {{ partial "location" .Page.Params.Location }}<br/>
{{ if isset .Page.Params "recording" }} {{ if isset .Page.Params "recording" }}
<strong><i class="hover:underline hover:decoration-primary-400 hover:text-primary-500"></i>📹 Livestream: </strong> <a href="{{ .Page.Params.Recording }}">{{ .Page.Params.Recording }}</a></p> <strong><i class="hover:underline hover:decoration-primary-400 hover:text-primary-500"></i>📹 Video/Stream: </strong> <a href="{{ .Page.Params.Recording }}">{{ .Page.Params.Recording }}</a>
{{ else }} {{ else }}
{{ if and (eq .Page.Params.Streaming true) (not (isset .Page.Params "recording" )) }} {{ if and (eq .Page.Params.Streaming true) (not (isset .Page.Params "recording" )) }}
<p><strong>Stream:</strong> <a class="hover:underline hover:decoration-primary-400 hover:text-primary-500" href="https://streaming.media.ccc.de/datengarten">streaming.media.ccc.de/datengarten</a></p> <strong>Stream:</strong> <a class="hover:underline hover:decoration-primary-400 hover:text-primary-500" href="https://streaming.media.ccc.de/datengarten">streaming.media.ccc.de/datengarten</a>
{{ else }} {{ else }}
<p><strong><i class="fa fa-exclamation-triangle fa-fw"></i></strong> Dieser Vortrag wird <strong>nicht</strong> gestreamed</a>! / This talk will <strong>not</strong> be streamed!</p> <strong><i class="fa fa-exclamation-triangle fa-fw"></i></strong> Dieser Vortrag wird <strong>nicht</strong> gestreamt! / This talk will <strong>not</strong> be streamed!
{{ end }} {{ end }}
{{ end }} {{ end }}
</p> </p>

View file

@ -3,8 +3,8 @@ Put this file in /layouts/shortcodes/gallery.html
Documentation and licence at https://github.com/liwenyip/hugo-easy-gallery/ Documentation and licence at https://github.com/liwenyip/hugo-easy-gallery/
--> -->
<!-- count how many times we've called this shortcode; load the css if it's the first time --> <!-- count how many times we've called this shortcode; load the css if it's the first time -->
{{- if not ($.Page.Scratch.Get "figurecount") }}<link rel="stylesheet" href="{{ "css/hugo-easy-gallery.css" | absURL }}" />{{ end }} {{- if not ($.Page.Store.Get "figurecount") }}<link rel="stylesheet" href="{{ "css/hugo-easy-gallery.css" | absURL }}" />{{ end }}
{{- $.Page.Scratch.Add "figurecount" 1 }} {{- $.Page.Store.Add "figurecount" 1 }}
{{ $baseURL := .Site.BaseURL }} {{ $baseURL := .Site.BaseURL }}
<div class="gallery caption-position-{{ with .Get "caption-position" | default "bottom" }}{{.}}{{end}} caption-effect-{{ with .Get "caption-effect" | default "slide" }}{{.}}{{end}} hover-effect-{{ with .Get "hover-effect" | default "zoom" }}{{.}}{{end}} {{ if ne (.Get "hover-transition") "none" }}hover-transition{{end}}" itemscope itemtype="http://schema.org/ImageGallery"> <div class="gallery caption-position-{{ with .Get "caption-position" | default "bottom" }}{{.}}{{end}} caption-effect-{{ with .Get "caption-effect" | default "slide" }}{{.}}{{end}} hover-effect-{{ with .Get "hover-effect" | default "zoom" }}{{.}}{{end}} {{ if ne (.Get "hover-transition") "none" }}hover-transition{{end}}" itemscope itemtype="http://schema.org/ImageGallery">
{{- with (.Get "dir") -}} {{- with (.Get "dir") -}}

View file

@ -21,7 +21,7 @@
{{ if $art.Params.recording }} {{ if $art.Params.recording }}
<td><a href="{{ $art.Params.recording }}"><i class="fa fa-video fa-fw"></i></a></td> <td><a href="{{ $art.Params.recording }}"><i class="fa fa-video fa-fw"></i></a></td>
{{ else }} {{ else }}
<td><i class="fa fa-video-slash fa-fw"></i></a></td> <td><i class="fa fa-video-slash fa-fw"></i></td>
{{ end }} {{ end }}
</tr> </tr>
{{ end }} {{ end }}

View file

@ -6,20 +6,23 @@ CALSCALE:GREGORIAN
METHOD:PUBLISH METHOD:PUBLISH
X-WR-CALNAME:CCCB Veranstaltungen X-WR-CALNAME:CCCB Veranstaltungen
X-WR-TIMEZONE:{{ $time_zone }} X-WR-TIMEZONE:{{ $time_zone }}
{{ range .Pages }} {{- range .Pages }}
{{- if not .Params.members_only }}
BEGIN:VEVENT BEGIN:VEVENT
UID:{{ .File.BaseFileName }}@berlin.ccc.de UID:{{ .File.BaseFileName }}@berlin.ccc.de
SUMMARY:{{ .Title }} SUMMARY:{{ .Title }}
{{ with .Params.subtitle }} {{ with .Params.subtitle -}}
DESCRIPTION:{{ . }} DESCRIPTION:{{ . }}\nLink: https://berlin.ccc.de{{ $.Page.RelPermalink }}
{{ end -}}
Link: https://berlin.ccc.de{{ $.RelPermalink }}
{{ end }}
URL:https://berlin.ccc.de{{ .RelPermalink }} URL:https://berlin.ccc.de{{ .RelPermalink }}
{{ with .Params.dtstart }}DTSTART;TZID=Europe/Berlin:{{ . }}{{ end }} {{ with .Params.dtstart -}}DTSTART;TZID=Europe/Berlin:{{ . }}
{{ with .Params.dtend }}DTEND;TZID=Europe/Berlin:{{ . }}{{ end }} {{ end -}}
{{ with .Params.rrule }}RRULE:{{ . }}{{ end }} {{ with .Params.dtend -}}DTEND;TZID=Europe/Berlin:{{ . }}
{{ end -}}
{{ with .Params.rrule -}}RRULE:{{ . }}
{{ end -}}
DTSTAMP:{{ now.Format "20060102T150405Z" }} DTSTAMP:{{ now.Format "20060102T150405Z" }}
END:VEVENT END:VEVENT
{{ end }} {{ end -}}
{{ end -}}
END:VCALENDAR END:VCALENDAR

View file

@ -1,2 +1,2 @@
icalendar==5.0.7 icalendar>=6.0.1
pytz python-dateutil

Binary file not shown.

Binary file not shown.

View file

@ -2,16 +2,16 @@
if [ $# -lt 2 ]; then if [ $# -lt 2 ]; then
echo "Usage: $0 Mediawiki_Page_Name [page|post]" echo "Usage: $0 Mediawiki_Page_Name [page|post]"
exit exit 1
fi fi
echo Converting $1... echo "Converting $1..."
page=$(echo $1 | sed 's,/,_,g' | tr '[:upper:]' '[:lower:]') page=$(printf '%s\n' "$1" | sed 's,/,_,g' | tr '[:upper:]' '[:lower:]')
hugo new $2/$page.md hugo new "$2/$page.md"
curl -s "https://berlin.ccc.de/api.php?action=query&prop=revisions&rvprop=content&format=json&titles=$1" \ curl -s "https://berlin.ccc.de/api.php?action=query&prop=revisions&rvprop=content&format=json&titles=$1" \
| jq -r '.query.pages |..| objects|.["*"]' \ | jq -r '.query.pages |..| objects|.["*"]' \
| sed '/null/d' \ | sed '/null/d' \
| pandoc -f mediawiki -t markdown \ | pandoc -f mediawiki -t markdown \
>> content/$2/$page.md >> "content/$2/$page.md"

View file

@ -1,48 +1,60 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import sys import sys
import html
import logging import logging
import locale import locale
from dateutil.parser import parse from datetime import datetime, timedelta, timezone, date
from datetime import datetime, timedelta
from dateutil.rrule import rruleset, rrulestr from dateutil.rrule import rruleset, rrulestr
import icalendar import icalendar
def _to_aware(dt):
"""Normalize a date or datetime to a timezone-aware UTC datetime."""
if isinstance(dt, date) and not isinstance(dt, datetime):
return datetime(dt.year, dt.month, dt.day, tzinfo=timezone.utc)
if dt.tzinfo is None:
return dt.replace(tzinfo=timezone.utc)
return dt.astimezone(timezone.utc)
def vevent_to_event(event, rrstart=None): def vevent_to_event(event, rrstart=None):
if rrstart == None: if rrstart is None:
begin = parse(event["DTSTART"].to_ical()) begin = _to_aware(event["DTSTART"].dt)
else: else:
begin = rrstart begin = _to_aware(rrstart)
return { return {
"name": event["SUMMARY"].to_ical(), "name": str(event.get("SUMMARY", "")),
"url": event["URL"].to_ical(), "url": str(event.get("URL", "")),
"begin": begin "begin": begin
} }
def parse_single_event(event, start, end): def parse_single_event(event, start, end):
logging.info(f"Processing single event {event['SUMMARY'].to_ical().decode('utf-8')}") logging.info(f"Processing single event {event.get('SUMMARY', '')}")
dtstart = parse(event["DTSTART"].to_ical()) dtstart = _to_aware(event["DTSTART"].dt)
if dtstart >= start and dtstart < end: if dtstart >= start and dtstart < end:
return vevent_to_event(event) return vevent_to_event(event)
def parse_recurring_event(event, start, end): def parse_recurring_event(event, start, end):
logging.info(f"Processing recurring event {event['SUMMARY'].to_ical().decode('utf-8')}") logging.info(f"Processing recurring event {event.get('SUMMARY', '')}")
dtstart = parse(event["DTSTART"].to_ical()) dtstart = _to_aware(event["DTSTART"].dt)
rs = rruleset() rs = rruleset()
rs.rrule(rrulestr(event["RRULE"].to_ical().decode("utf-8"), dtstart=dtstart)) rs.rrule(rrulestr(event["RRULE"].to_ical().decode("utf-8"), dtstart=dtstart))
if "EXDATE" in event.keys(): if "EXDATE" in event.keys():
for exdate in event["EXDATE"]: exdates = event["EXDATE"]
rs.exdate(parse(exdate.to_ical())) if not isinstance(exdates, list):
exdates = [exdates]
for exdate_list in exdates:
for dt in exdate_list.dts:
rs.exdate(_to_aware(dt.dt))
events = [] events = []
for date in list(rs): for dt in rs.between(start, end, inc=True):
if date >= start and date < end: events.append(vevent_to_event(event, dt))
events.append(vevent_to_event(event, date))
return events return events
@ -70,26 +82,27 @@ def find_events(icsfilestr, start, end, num):
def format_events(events): def format_events(events):
print("<table class=\"table table-condensed\">") print("<table class=\"table table-condensed\">")
for event in events: for event in events:
name = html.escape(event['name'])
url = html.escape(event['url'])
print( print(
"<tr>" "<tr>"
f"<td>{event['begin'].strftime('%A, %d.%m um %H:%M Uhr')}</td>" 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>" f"<td><a href=\"{url}\">{name}</a></td>"
"</tr>" "</tr>"
) )
print("</table><!--/.table .table-condensed-->") print("</table><!--/.table .table-condensed-->")
if __name__ == "__main__": if __name__ == "__main__":
if len(sys.argv) < 3: if len(sys.argv) < 4:
print(f"Usage: {sys.argv[0]} calendar max_days max_items") print(f"Usage: {sys.argv[0]} calendar max_days max_items")
sys.exit(-1) sys.exit(1)
locale.setlocale(locale.LC_TIME, "de_DE.UTF-8") locale.setlocale(locale.LC_TIME, "de_DE.UTF-8")
calendar = sys.argv[1] calendar = sys.argv[1]
max_days = int(sys.argv[2]) max_days = int(sys.argv[2])
max_items = int(sys.argv[3]) max_items = int(sys.argv[3])
now = datetime.now() now = datetime.now(timezone.utc)
events = find_events(calendar, now, now + timedelta(days=max_days), max_items) events = find_events(calendar, now, now + timedelta(days=max_days), max_items)
format_events(events) format_events(events)

View file

@ -1,8 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import logging
from glob import glob from glob import glob
import pytz
import icalendar import icalendar
@ -12,9 +12,12 @@ merged.add("prodid", "-//CCCB Calendar Generator//berlin.ccc.de//")
merged.add("version", "2.0") merged.add("version", "2.0")
for icsfilestr in glob("public/*/**/*.ics", recursive=True): for icsfilestr in glob("public/*/**/*.ics", recursive=True):
with open(icsfilestr, "r") as icsfile: try:
print(f"Importing {icsfilestr}") with open(icsfilestr, "r") as icsfile:
calendars.append(icalendar.Calendar.from_ical(icsfile.read())) print(f"Importing {icsfilestr}")
calendars.append(icalendar.Calendar.from_ical(icsfile.read()))
except Exception as e:
logging.warning(f"Skipping {icsfilestr}: {e}")
for calendar in calendars: for calendar in calendars:
for event in calendar.subcomponents: for event in calendar.subcomponents: