world-2026-hub/assets/js/calendar.js

49 lines
1.7 KiB
JavaScript

// calendar.js — "Add to calendar" export (RFC 5545). One VEVENT per match,
// DTSTART/DTEND in UTC with a fixed 2h duration, CRLF line endings (required
// by the spec — some calendar apps reject \n). Knockout team names come from
// resolveBracketTeams().
import { matchDateUTC } from './app.js';
import { resolveBracketTeams } from './bracket.js';
import { translatePhase } from './i18n.js';
// Date → 20260611T160000Z
function icsDate(date) {
return date.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
}
// RFC 5545 TEXT escaping: backslash, comma, semicolon, newline
function icsEscape(text) {
return String(text).replace(/\\/g, '\\\\').replace(/,/g, '\\,').replace(/;/g, '\\;').replace(/\n/g, '\\n');
}
export function exportMatchToICS(match, stadium) {
const start = matchDateUTC(match);
const end = new Date(start.getTime() + 2 * 60 * 60 * 1000);
const { home, away } = resolveBracketTeams(match);
const location = stadium ? `${stadium.name}, ${stadium.city}` : match.city;
const lines = [
'BEGIN:VCALENDAR',
'VERSION:2.0',
'PRODID:-//WorldCup2026Hub//EN',
'BEGIN:VEVENT',
`UID:match-${match.id}@worldcup2026hub`,
`DTSTAMP:${icsDate(start)}`,
`DTSTART:${icsDate(start)}`,
`DTEND:${icsDate(end)}`,
`SUMMARY:${icsEscape(`${home.label} x ${away.label}${translatePhase(match.phase)}`)}`,
`LOCATION:${icsEscape(location)}`,
'END:VEVENT',
'END:VCALENDAR',
];
const blob = new Blob([lines.join('\r\n') + '\r\n'], { type: 'text/calendar' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `match-${match.id}.ics`;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(link.href);
}