Add hackertours/generate-table.js

This commit is contained in:
Vincent Mahnke 2025-11-09 03:44:58 +01:00
commit f807bf0982

View file

@ -0,0 +1,115 @@
// Usage: Replace the 4 constants below, then run `await generateMarkdownTable()` to create markdown
// Requires the event to be an `event series`; `39c3ht` is not so this will fail: https://docs.pretix.eu/guides/event-series/?h=dates#how-to-create-an-event-series
const token = ""; // generate using https://docs.pretix.eu/dev/api/tokenauth.html#obtaining-an-api-token
const baseUrl = 'https://tickets.hamburg.ccc.de/api/v2';
const organizer = "hackertours"; // example extracted from https://tickets.hamburg.ccc.de/hackertours/39c3ht
const eventName = "39c3ht"; // example extracted from https://tickets.hamburg.ccc.de/hackertours/39c3ht
async function generateMarkdownTable() {
async function fetchAllSubevents() {
const results = [];
let url = `${baseUrl}/organizers/${organizer}/events/${eventName}/subevents/`;
while (url) {
const resp = await fetch(url, {
headers: {
'Authorization': `Token ${token}`,
'Accept': 'application/json'
}
});
if (!resp.ok) {
throw new Error(`Failed to fetch subevents: ${resp.status} ${await resp.text()}`);
}
const json = await resp.json();
results.push(...json.results);
url = json.next;
}
return results;
}
function slugify(name) {
return name
.toLowerCase()
.replace(/[“”"']/g, '')
.replace(/\(.*?\)/g, '')
.replace(/&/g, 'and')
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
function langTags(title) {
const tags = [];
const hasGerman = /\(german\)|\bgerman\b|[äöüß]|stadtrundgang|rote flora|gängeviertel/.test(title);
const hasEnglish = /\(english\)|\benglish\b/.test(title);
const bilingual = /\b(bilingual)\b/.test(title);
if (bilingual) {
tags.push('{{< lang de >}}', '{{< lang en >}}');
} else {
if (hasGerman) tags.push('{{< lang de >}}');
if (hasEnglish) tags.push('{{< lang en >}}');
}
return tags.join(' ');
}
function formatDay(date) {
return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
}
const subevents = await fetchAllSubevents();
if (!subevents.length) return '| Zeit | | |\n|------|---|---|';
const dayMap = new Map();
for (const s of subevents) {
if (!s.date_from) continue;
const d = new Date(s.date_from);
const dayKey = d.toISOString().slice(0, 10);
if (!dayMap.has(dayKey)) {
dayMap.set(dayKey, { dateObj: d, events: [] });
}
const hour = d.getUTCHours();
const minute = d.getUTCMinutes();
let title = ((s.name && (s.name.en || Object.values(s.name)[0])) || 'Untitled').replace(/^.*?-\s*/, '');
const slug = slugify(title);
const tags = langTags(title);
title = title.replace("(German)", "").replace("(English)", "").replace("(Bilingual)", "");
const md = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')} [${title}](./${slug}/)${tags ? ' ' + tags : ''}`;
dayMap.get(dayKey).events.push({ hour, minute, md });
}
const days = Array.from(dayMap.entries()).sort((a, b) => a[1].dateObj - b[1].dateObj);
const hourSet = new Set();
for (const [, { events }] of days) {
for (const e of events) hourSet.add(e.hour);
}
const hoursSorted = Array.from(hourSet).sort((a, b) => a - b);
const baseDate = new Date('2025-12-26T00:00:00Z');
const headerCells = days.map(([key, val]) => {
const dayNumber = Math.floor((val.dateObj - baseDate) / (1000 * 60 * 60 * 24));
return `Tag ${dayNumber} (${formatDay(val.dateObj)})`;
});
let md = '| Zeit | ' + headerCells.join(' | ') + ' |\n';
md += '|------|' + headerCells.map(() => '---').join('|') + '|\n';
for (const hour of hoursSorted) {
const hourLabel = `${hour.toString().padStart(2, '0')}:00`;
const perDay = days.map(([, { events }]) =>
events
.filter(e => e.hour === hour)
.sort((a, b) => a.minute - b.minute)
);
const maxRows = Math.max(0, ...perDay.map(list => list.length));
if (maxRows === 0) continue;
for (let i = 0; i < maxRows; i++) {
const rowCells = perDay.map(list => list[i] ? list[i].md : '-');
const zeit = i === 0 ? hourLabel : '';
md += `| ${zeit} | ${rowCells.join(' | ')} |\n`;
}
}
return md;
}