Add hackertours/generate-table.js
This commit is contained in:
commit
f807bf0982
1 changed files with 115 additions and 0 deletions
115
hackertours/generate-table.js
Normal file
115
hackertours/generate-table.js
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue