From f807bf0982e8b3bf92767fa12618a72758e8568a Mon Sep 17 00:00:00 2001 From: ViMaSter Date: Sun, 9 Nov 2025 03:44:58 +0100 Subject: [PATCH] Add hackertours/generate-table.js --- hackertours/generate-table.js | 115 ++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 hackertours/generate-table.js diff --git a/hackertours/generate-table.js b/hackertours/generate-table.js new file mode 100644 index 0000000..178121e --- /dev/null +++ b/hackertours/generate-table.js @@ -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; +} \ No newline at end of file