mirror of
https://github.com/fabrice404/olympics-calendar.git
synced 2025-12-11 13:29:35 +00:00
wip milano-cortina 2026
This commit is contained in:
23
.github/dependabot.yml
vendored
23
.github/dependabot.yml
vendored
@ -2,7 +2,28 @@ version: 2
|
||||
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: "/"
|
||||
directory: "/scraper"
|
||||
schedule:
|
||||
interval: daily
|
||||
ignore:
|
||||
- dependency-name: "*"
|
||||
update-types: ["version-update:semver-major"]
|
||||
commit-message:
|
||||
prefix: "npm"
|
||||
include: "scope"
|
||||
groups:
|
||||
dependencies-patch-and-minor:
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
open-pull-requests-limit: 50
|
||||
reviewers:
|
||||
- fabrice404
|
||||
assignees:
|
||||
- fabrice404
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: "/ui"
|
||||
schedule:
|
||||
interval: daily
|
||||
ignore:
|
||||
|
||||
16
.vscode/settings.json
vendored
Normal file
16
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"Cortina",
|
||||
|
||||
"NOC", // National Olympic Committee
|
||||
"NOCs", // National Olympic Committees
|
||||
|
||||
// color palette
|
||||
"azzurro",
|
||||
"giallo",
|
||||
"rosa",
|
||||
"rosso",
|
||||
"verde",
|
||||
"viola"
|
||||
]
|
||||
}
|
||||
3
LICENSE
Normal file
3
LICENSE
Normal file
@ -0,0 +1,3 @@
|
||||
Copyright (c) 2025 Fabrice Lamant. All rights reserved.
|
||||
|
||||
No part of this work may be copied, reproduced, distributed, transmitted, displayed, performed, or otherwise used, in whole or in part, by any means or in any form, without the prior written permission of the copyright holder.
|
||||
@ -4,7 +4,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"
|
||||
const debug = Debug(`olympics-calendar:cache`);
|
||||
|
||||
const cachePath = (key: string): string => {
|
||||
return `../cache/${key}`;
|
||||
return `../cache/${key}.cached`;
|
||||
}
|
||||
|
||||
export const get = (key: string): string | null => {
|
||||
|
||||
150
scraper/ics.ts
Normal file
150
scraper/ics.ts
Normal file
@ -0,0 +1,150 @@
|
||||
// BEGIN:VCALENDAR
|
||||
// VERSION:2.0
|
||||
// PRODID:-//fabrice404//olympics-calendar//archery/AUS//EN
|
||||
// X-WR-CALNAME:🇦🇺 Australia Archery | Paris 2024
|
||||
// NAME:🇦🇺 Australia Archery | Paris 2024
|
||||
// BEGIN:VEVENT
|
||||
// UID:20240725T073000Z-archery-WOMENS-INDIVIDUAL-RANKING-ROUND
|
||||
// DTSTAMP:20240725T073000Z
|
||||
// DTSTART:20240725T073000Z
|
||||
// DTEND:20240725T103000Z
|
||||
// DESCRIPTION:Archery - Women's Individual Ranking Round\n🇨🇳 AN
|
||||
// Qixuan\n🇲🇽 Alejandra VALENCIA\n🇲🇩 Alexandra MIRCA\n🇵🇷 Alondra
|
||||
// RIVERA\n🇫🇷 Amelie CORDEAU\n🇧🇷 Ana Luiza SLIACHTICAS CAETANO\n🇨🇴 Ana
|
||||
// RENDON MARTINEZ\n🇲🇽 Ana VAZQUEZ\n🇲🇽 Angela RUIZ\n🇮🇳 Ankita
|
||||
// BHAKAT\n🇲🇾 Ariana Nur Dania MOHAMAD ZAIRI\n🇮🇳 Bhajan KAUR\n🇬🇧
|
||||
// Bryony PITMAN\n🇹🇼 CHIU Yi-Ching\n🇫🇷 Caroline LOPEZ\n🇺🇸 Casey
|
||||
// KAUFHOLD\n🇺🇸 Catalina GNORIEGA\n🇩🇪 Charline SCHWARZ\n🇮🇹 Chiara
|
||||
// REBAGLIATI\n🇮🇳 Deepika KUMARI\n🇸🇰 Denisa BARANKOVA\n🇮🇩 Diananda
|
||||
// CHOIRUNISA\n🇪🇸 Elia CANALES\n🇹🇷 Elif Berra GOKKIR\n🇦🇹 Elisabeth
|
||||
// STRAKA\n🇬🇳 Fatoumata SYLLA\n🇳🇱 Gaby SCHLOESSER\n🇸🇲 Giorgia
|
||||
// CESARINI\n🇰🇷 JEON Hunyoung\n🇪🇬 Jana ALI\n🇺🇸 Jennifer MUCINO\n🇩🇪
|
||||
// Katharina BAUER\n🇩🇰 Kirstine DANSTRUP ANDERSEN\n🇹🇼 LEI
|
||||
// Chien-Ying\n🇨🇳 LI Jiaman\n🇹🇼 LI Tsai-Chi\n🇰🇷 LIM Sihyeon\n🇦🇺
|
||||
// Laura PAEGLIS\n🇳🇱 Laura van der WINKEL\n🇫🇷 Lisa BARBELIN\n🇷🇴
|
||||
// Madalina AMAISTROAIE\n🇨🇿 Marie HORACKOVA\n🇬🇧 Megs HAVERS\n🇩🇪
|
||||
// Michelle KROPPEN\n🇮🇱 Mikaella MOSHE\n🇮🇷 Mobina FALLAH\n🇰🇷 NAM
|
||||
// Suhyeon\n🇯🇵 NODA Satsuki\n🇲🇾 Nurul Azreena MOHAMAD FAZIL\n🇬🇧 Penny
|
||||
// HEALEY\n🇳🇱 Quinty ROEFFEN\n🇪🇪 Reena PARNAT\n🇮🇩 Rezza OCTAVIA\n🇹🇳
|
||||
// Rihab ELWALID\n🇲🇾 Syaqiera MASHAYIKH\n🇮🇩 Syifa Nurafifah KAMAL\n🇻🇳
|
||||
// Thi Anh Nguyet DO\n🇺🇦 Veronika MARCHENKO\n🇨🇦 Virginie CHENIER\n🇵🇱
|
||||
// Wioleta MYSZOR\n🇨🇳 YANG Xiaolei\n🇦🇿 Yaylagul RAMAZANOVA\n🇸🇮 Zana
|
||||
// PINTARIC\n🇺🇿 Ziyodakhon ABDUSATTOROVA
|
||||
// SUMMARY:🏹 Women's Individual Ranking Round
|
||||
// LOCATION:Invalides
|
||||
// END:VEVENT
|
||||
|
||||
import { mkdirSync, writeFileSync } from "fs";
|
||||
import { Calendar } from "./types";
|
||||
import { getFlag } from "./nocs";
|
||||
|
||||
|
||||
// BEGIN:VCALENDAR
|
||||
// VERSION:2.0
|
||||
// PRODID:-//fabrice404//olympics-calendar//3x3-basketball/AUS//EN
|
||||
// X-WR-CALNAME:🇦🇺 Australia 3x3 Basketball | Paris 2024
|
||||
// NAME:🇦🇺 Australia 3x3 Basketball | Paris 2024
|
||||
// BEGIN:VEVENT
|
||||
// UID:20240730T160000Z-3x3-basketball-WOMENS-POOL-ROUND-AUS-CAN
|
||||
// DTSTAMP:20240730T160000Z
|
||||
// DTSTART:20240730T160000Z
|
||||
// DTEND:20240730T162500Z
|
||||
// DESCRIPTION:3x3 Basketball - Women's Pool Round
|
||||
// SUMMARY:🏀 AUS 🇦🇺 - 🇨🇦 CAN
|
||||
// LOCATION:La Concorde 1
|
||||
// END:VEVENT
|
||||
|
||||
// END:VCALENDAR
|
||||
|
||||
export const generateICSFiles = (calendar: Calendar): void => {
|
||||
|
||||
generateICSFile(calendar, null, null);
|
||||
|
||||
calendar.sports.forEach((sport) => {
|
||||
generateICSFile(calendar, sport.key, null);
|
||||
calendar.nocs.forEach((noc) => {
|
||||
generateICSFile(calendar, sport.key, noc.key);
|
||||
});
|
||||
});
|
||||
|
||||
calendar.nocs.forEach((noc) => {
|
||||
generateICSFile(calendar, null, noc.key);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const generateICSFile = (calendar: Calendar, sportKey: string | null, nocKey: string | null): void => {
|
||||
calendar.languages.forEach((lang) => {
|
||||
const pathSportKey = sportKey ? sportKey : "all-sports";
|
||||
const pathNocKey = nocKey ? nocKey : "calendar"
|
||||
|
||||
const filepath = `../ui/public/data/${lang.code.toLowerCase()}/${pathSportKey.toLowerCase()}/${pathNocKey.toLowerCase()}.ics`;
|
||||
mkdirSync(filepath.split('/').slice(0, -1).join('/'), { recursive: true });
|
||||
|
||||
const titleComponents = [];
|
||||
if (nocKey) {
|
||||
titleComponents.push(`${calendar.nocs.find(n => n.key === nocKey)!.name[lang.code]}`);
|
||||
}
|
||||
if (sportKey) {
|
||||
titleComponents.push(calendar.sports.find(s => s.key === sportKey)!.name[lang.code]);
|
||||
}
|
||||
titleComponents.push("Milano Cortina 2026");
|
||||
|
||||
const title = titleComponents.join(' - ');
|
||||
|
||||
const lines = [];
|
||||
|
||||
lines.push("BEGIN:VCALENDAR");
|
||||
lines.push("VERSION:2.0");
|
||||
lines.push(`PRODID:-//fabrice404//olympics-calendar//${lang.code}/${pathSportKey}/${pathNocKey}`);
|
||||
lines.push(`X-WR-CALNAME:${title}`);
|
||||
lines.push(`NAME:${title}`);
|
||||
|
||||
calendar.events
|
||||
.filter((event) => {
|
||||
if (sportKey && event.sport !== sportKey) return false;
|
||||
if (nocKey) {
|
||||
if (event.match) {
|
||||
const team1Key = event.match.team1.key;
|
||||
const team2Key = event.match.team2.key;
|
||||
if (team1Key !== nocKey && team2Key !== nocKey) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.forEach((event) => {
|
||||
lines.push("BEGIN:VEVENT");
|
||||
lines.push(`UID:${event.key.replace(/--/g, '-')}`);
|
||||
lines.push(`DTSTAMP:${event.start.replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z')}`);
|
||||
lines.push(`DTSTART:${event.start.replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z')}`);
|
||||
lines.push(`DTEND:${event.end.replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z')}`);
|
||||
lines.push(`LOCATION:${event.location[lang.code] || ''}`);
|
||||
|
||||
const sport = calendar.sports.find(s => s.key === event.sport)!;
|
||||
lines.push(`DESCRIPTION:${sport.name[lang.code]} - ${event.name[lang.code] || ''}`);
|
||||
let summary = `SUMMARY:${event.name[lang.code] || ''}`
|
||||
|
||||
if (event.match) {
|
||||
const team1Name = event.match.team1.name[lang.code] || event.match.team1.key;
|
||||
const team1Flag = getFlag(event.match.team1.key);
|
||||
const team2Name = event.match.team2.name[lang.code] || event.match.team2.key;
|
||||
const team2Flag = getFlag(event.match.team2.key);
|
||||
if (team1Name && team2Name) {
|
||||
lines.push(`SUMMARY:${team1Flag} ${team1Name} - ${team2Name} ${team2Flag}`);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push(summary);
|
||||
lines.push(`END:VEVENT`);
|
||||
|
||||
})
|
||||
|
||||
lines.push("END:VCALENDAR");
|
||||
|
||||
writeFileSync(filepath, lines.join('\n'));
|
||||
});
|
||||
};
|
||||
@ -1,12 +1,16 @@
|
||||
import Debug from "debug";
|
||||
|
||||
import * as cache from "./cache";
|
||||
import { mkdirSync, writeFileSync } from "fs";
|
||||
import { Calendar, Event, Sport, Team } from "./types";
|
||||
import { generateICSFiles } from "./ics";
|
||||
|
||||
const baseUrl = "https://www.olympics.com";
|
||||
const basePath = "/milano-cortina-2026/schedule/overview";
|
||||
|
||||
const debug = Debug(`olympics-calendar:index`);
|
||||
|
||||
|
||||
const getScheduleOverview = async (language: string) => {
|
||||
debug(`getScheduleOverview: language=${language}`);
|
||||
|
||||
@ -48,7 +52,7 @@ const getScheduleSport = async (language: string, sportCode: string) => {
|
||||
|
||||
const scheduleSport = JSON.parse(cache.get(scheduleSportKey)!);
|
||||
return scheduleSport;
|
||||
}
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const overview = await getScheduleOverview("en");
|
||||
@ -59,9 +63,9 @@ const main = async () => {
|
||||
name: lang.label,
|
||||
}))
|
||||
|
||||
const sports: any = [];
|
||||
const events: any[] = [];
|
||||
let nocs: any[] = [];
|
||||
const sports: Sport[] = [];
|
||||
const events: Event[] = [];
|
||||
let nocs: Team[] = [];
|
||||
|
||||
for (const lang of languages) {
|
||||
const scheduleOverview = await getScheduleOverview(lang.code);
|
||||
@ -76,7 +80,7 @@ const main = async () => {
|
||||
if (sports.find((s: any) => s.key === key) == null) {
|
||||
sports.push({ key, name: {}, order: -1 })
|
||||
}
|
||||
const sport = sports.find((s: any) => s.key === key)
|
||||
const sport = sports.find((s: any) => s.key === key)!;
|
||||
sport.name[lang.code] = discipline.description;
|
||||
sport.order = discipline.order;
|
||||
|
||||
@ -93,10 +97,12 @@ const main = async () => {
|
||||
isTraining: scheduleListElement.isTraining,
|
||||
medal: scheduleListElement.medal,
|
||||
name: {},
|
||||
location: {},
|
||||
})
|
||||
}
|
||||
const event = events.find(e => e.key === scheduleListElement.unitCode);
|
||||
const event = events.find(e => e.key === scheduleListElement.unitCode)!;
|
||||
event.name[lang.code] = scheduleListElement.description;
|
||||
event.location[lang.code] = scheduleListElement.venue?.description || ''
|
||||
|
||||
if (scheduleListElement.match) {
|
||||
if (event.match == null) {
|
||||
@ -114,7 +120,7 @@ const main = async () => {
|
||||
if (nocs.find(n => n.key === nocKey) == null) {
|
||||
nocs.push({ key: nocKey, name: {} });
|
||||
}
|
||||
const noc = nocs.find(n => n.key === nocKey);
|
||||
const noc = nocs.find(n => n.key === nocKey)!;
|
||||
noc.name[lang.code] = (team.description || '').replace(/\,/gi, '');
|
||||
}
|
||||
}
|
||||
@ -125,7 +131,11 @@ const main = async () => {
|
||||
|
||||
nocs = nocs.filter((noc) => noc.key !== noc.name.en);
|
||||
|
||||
cache.set('calendar.json', JSON.stringify({ languages, sports, nocs, events }));
|
||||
const dataFolder = "../ui/public/data";
|
||||
mkdirSync(dataFolder, { recursive: true });
|
||||
const calendar: Calendar = { languages, sports, nocs, events };
|
||||
writeFileSync(`${dataFolder}/calendar.json`, JSON.stringify(calendar));
|
||||
generateICSFiles(calendar);
|
||||
};
|
||||
|
||||
main();
|
||||
|
||||
212
scraper/nocs.ts
Normal file
212
scraper/nocs.ts
Normal file
@ -0,0 +1,212 @@
|
||||
export const flags: { [key: string]: string } = {
|
||||
AFG: "🇦🇫",
|
||||
ALB: "🇦🇱",
|
||||
ALG: "🇩🇿",
|
||||
AIN: "🇦🇸",
|
||||
AND: "🇦🇩",
|
||||
ANG: "🇦🇴",
|
||||
ANT: "🇦🇬",
|
||||
ARG: "🇦🇷",
|
||||
ARM: "🇦🇲",
|
||||
ARU: "🇦🇼",
|
||||
ASA: "🇦🇸",
|
||||
AUS: "🇦🇺",
|
||||
AUT: "🇦🇹",
|
||||
AZE: "🇦🇿",
|
||||
BAH: "🇧🇸",
|
||||
BAN: "🇧🇩",
|
||||
BAR: "🇧🇧",
|
||||
BDI: "🇧🇮",
|
||||
BEL: "🇧🇪",
|
||||
BEN: "🇧🇯",
|
||||
BER: "🇧🇲",
|
||||
BHU: "🇧🇹",
|
||||
BIH: "🇧🇦",
|
||||
BIZ: "🇧🇿",
|
||||
BOL: "🇧🇴",
|
||||
BOT: "🇧🇼",
|
||||
BRA: "🇧🇷",
|
||||
BRN: "🇧🇭",
|
||||
BRU: "🇧🇳",
|
||||
BUL: "🇧🇬",
|
||||
BUR: "🇧🇫",
|
||||
CAF: "🇨🇫",
|
||||
CAM: "🇰🇭",
|
||||
CAN: "🇨🇦",
|
||||
CAY: "🇰🇾",
|
||||
CGO: "🇨🇬",
|
||||
CHA: "🇹🇩",
|
||||
CHI: "🇨🇱",
|
||||
CHN: "🇨🇳",
|
||||
CIV: "🇨🇮",
|
||||
CMR: "🇨🇲",
|
||||
COD: "🇨🇩",
|
||||
COK: "🇨🇰",
|
||||
COL: "🇨🇴",
|
||||
COM: "🇰🇲",
|
||||
CPV: "🇨🇻",
|
||||
CRC: "🇨🇷",
|
||||
CRO: "🇭🇷",
|
||||
CUB: "🇨🇺",
|
||||
CYP: "🇨🇾",
|
||||
CZE: "🇨🇿",
|
||||
DEN: "🇩🇰",
|
||||
DJI: "🇩🇯",
|
||||
DMA: "🇩🇲",
|
||||
DOM: "🇩🇴",
|
||||
ECU: "🇪🇨",
|
||||
EGY: "🇪🇬",
|
||||
EOR: "🏳️",
|
||||
ERI: "🇪🇷",
|
||||
ESA: "🇸🇻",
|
||||
ESP: "🇪🇸",
|
||||
EST: "🇪🇪",
|
||||
ETH: "🇪🇹",
|
||||
FIJ: "🇫🇯",
|
||||
FIN: "🇫🇮",
|
||||
FRA: "🇫🇷",
|
||||
FSM: "🇫🇲",
|
||||
GAB: "🇬🇦",
|
||||
GAM: "🇬🇲",
|
||||
GBR: "🇬🇧",
|
||||
GBS: "🇬🇼",
|
||||
GEO: "🇬🇪",
|
||||
GEQ: "🇬🇶",
|
||||
GER: "🇩🇪",
|
||||
GHA: "🇬🇭",
|
||||
GRE: "🇬🇷",
|
||||
GRN: "🇬🇩",
|
||||
GUA: "🇬🇹",
|
||||
GUI: "🇬🇳",
|
||||
GUM: "🇬🇺",
|
||||
GUY: "🇬🇾",
|
||||
HAI: "🇭🇹",
|
||||
HKG: "🇭🇰",
|
||||
HON: "🇭🇳",
|
||||
HUN: "🇭🇺",
|
||||
INA: "🇮🇩",
|
||||
IND: "🇮🇳",
|
||||
IRI: "🇮🇷",
|
||||
IRL: "🇮🇪",
|
||||
IRQ: "🇮🇶",
|
||||
ISL: "🇮🇸",
|
||||
ISR: "🇮🇱",
|
||||
ISV: "🇻🇮",
|
||||
ITA: "🇮🇹",
|
||||
IVB: "🇻🇬",
|
||||
JAM: "🇯🇲",
|
||||
JOR: "🇯🇴",
|
||||
JPN: "🇯🇵",
|
||||
KAZ: "🇰🇿",
|
||||
KEN: "🇰🇪",
|
||||
KGZ: "🇰🇬",
|
||||
KIR: "🇰🇮",
|
||||
KOR: "🇰🇷",
|
||||
KOS: "🇽🇰",
|
||||
KSA: "🇸🇦",
|
||||
KUW: "🇰🇼",
|
||||
LAO: "🇱🇦",
|
||||
LAT: "🇱🇻",
|
||||
LBA: "🇱🇾",
|
||||
LBN: "🇱🇧",
|
||||
LBR: "🇱🇷",
|
||||
LCA: "🇱🇨",
|
||||
LES: "🇱🇸",
|
||||
LIE: "🇱🇮",
|
||||
LTU: "🇱🇹",
|
||||
LUX: "🇱🇺",
|
||||
MAD: "🇲🇬",
|
||||
MAR: "🇲🇦",
|
||||
MAS: "🇲🇾",
|
||||
MAW: "🇲🇼",
|
||||
MDA: "🇲🇩",
|
||||
MDV: "🇲🇻",
|
||||
MEX: "🇲🇽",
|
||||
MGL: "🇲🇳",
|
||||
MHL: "🇲🇭",
|
||||
MKD: "🇲🇰",
|
||||
MLI: "🇲🇱",
|
||||
MLT: "🇲🇹",
|
||||
MNE: "🇲🇪",
|
||||
MON: "🇲🇨",
|
||||
MOZ: "🇲🇿",
|
||||
MRI: "🇲🇺",
|
||||
MTN: "🇲🇷",
|
||||
MYA: "🇲🇲",
|
||||
NAM: "🇳🇦",
|
||||
NCA: "🇳🇮",
|
||||
NED: "🇳🇱",
|
||||
NEP: "🇳🇵",
|
||||
NGR: "🇳🇬",
|
||||
NIG: "🇳🇪",
|
||||
NOR: "🇳🇴",
|
||||
NRU: "🇳🇷",
|
||||
NZL: "🇳🇿",
|
||||
OMA: "🇴🇲",
|
||||
PAK: "🇵🇰",
|
||||
PAN: "🇵🇦",
|
||||
PAR: "🇵🇾",
|
||||
PER: "🇵🇪",
|
||||
PHI: "🇵🇭",
|
||||
PLE: "🇵🇸",
|
||||
PLW: "🇵🇼",
|
||||
PNG: "🇵🇬",
|
||||
POL: "🇵🇱",
|
||||
POR: "🇵🇹",
|
||||
PRK: "🇰🇵",
|
||||
PUR: "🇵🇷",
|
||||
QAT: "🇶🇦",
|
||||
ROU: "🇷🇴",
|
||||
RSA: "🇿🇦",
|
||||
RWA: "🇷🇼",
|
||||
SAM: "🇼🇸",
|
||||
SEN: "🇸🇳",
|
||||
SEY: "🇸🇨",
|
||||
SGP: "🇸🇬",
|
||||
SKN: "🇰🇳",
|
||||
SLE: "🇸🇱",
|
||||
SLO: "🇸🇮",
|
||||
SMR: "🇸🇲",
|
||||
SOL: "🇸🇧",
|
||||
SOM: "🇸🇴",
|
||||
SRB: "🇷🇸",
|
||||
SRI: "🇱🇰",
|
||||
SSD: "🇸🇸",
|
||||
STP: "🇸🇹",
|
||||
SUD: "🇸🇩",
|
||||
SUI: "🇨🇭",
|
||||
SUR: "🇸🇷",
|
||||
SVK: "🇸🇰",
|
||||
SWE: "🇸🇪",
|
||||
SWZ: "🇸🇿",
|
||||
SYR: "🇸🇾",
|
||||
TAN: "🇹🇿",
|
||||
TGA: "🇹🇴",
|
||||
THA: "🇹🇭",
|
||||
TJK: "🇹🇯",
|
||||
TKM: "🇹🇲",
|
||||
TLS: "🇹🇱",
|
||||
TOG: "🇹🇬",
|
||||
TPE: "🇹🇼",
|
||||
TTO: "🇹🇹",
|
||||
TUN: "🇹🇳",
|
||||
TUR: "🇹🇷",
|
||||
TUV: "🇹🇻",
|
||||
UAE: "🇦🇪",
|
||||
UGA: "🇺🇬",
|
||||
UKR: "🇺🇦",
|
||||
URU: "🇺🇾",
|
||||
USA: "🇺🇸",
|
||||
UZB: "🇺🇿",
|
||||
VAN: "🇻🇺",
|
||||
VEN: "🇻🇪",
|
||||
VIE: "🇻🇳",
|
||||
VIN: "🇻🇨",
|
||||
YEM: "🇾🇪",
|
||||
ZAM: "🇿🇲",
|
||||
ZIM: "🇿🇼",
|
||||
};
|
||||
|
||||
export const getFlag = (nocKey: string): string => {
|
||||
return flags[nocKey.toUpperCase()] || "🏳️";
|
||||
}
|
||||
@ -7,7 +7,9 @@
|
||||
"type": "commonjs",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "DEBUG=olympics-calendar* nodemon index.ts"
|
||||
"start": "find ./cache/**/*.cached -mmin +10 -exec rm -f {} \\; | DEBUG=olympics-calendar* ts-node index.ts",
|
||||
"dev": "DEBUG=olympics-calendar* nodemon index.ts",
|
||||
"lint": "eslint . --ext .ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": "^4.4.3",
|
||||
|
||||
44
scraper/types.d.ts
vendored
Normal file
44
scraper/types.d.ts
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
|
||||
export interface MultilingualString {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface Language {
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Sport {
|
||||
key: string;
|
||||
name: MultilingualString;
|
||||
order: number;
|
||||
}
|
||||
|
||||
export interface Team {
|
||||
key: string;
|
||||
name: MultilingualString;
|
||||
}
|
||||
|
||||
export interface Match {
|
||||
team1: Team;
|
||||
team2: Team;
|
||||
}
|
||||
|
||||
export interface Event {
|
||||
key: string;
|
||||
start: string;
|
||||
end: string;
|
||||
sport: string;
|
||||
isTraining: boolean;
|
||||
medal: '0' | '1' | '3';
|
||||
name: MultilingualString;
|
||||
location: MultilingualString;
|
||||
match?: Match;
|
||||
}
|
||||
|
||||
export interface Calendar {
|
||||
languages: Language[];
|
||||
sports: Sport[];
|
||||
events: Event[];
|
||||
nocs: Team[];
|
||||
}
|
||||
215
test.html
Normal file
215
test.html
Normal file
@ -0,0 +1,215 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Add to Calendar - All Sports Calendar</title>
|
||||
<style>
|
||||
.add-to-calendar-wrapper {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
max-width: 480px;
|
||||
margin: 2rem auto;
|
||||
padding: 1.5rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e2e2e2;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,.04);
|
||||
}
|
||||
|
||||
.add-to-calendar-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.add-to-calendar-subtitle {
|
||||
font-size: 0.9rem;
|
||||
color: #555;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.atc-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.atc-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.45rem;
|
||||
padding: 0.6rem 0.9rem;
|
||||
border-radius: 999px;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
border: 1px solid transparent;
|
||||
transition: transform 0.08s ease, box-shadow 0.08s ease, background-color 0.08s ease;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.atc-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,.08);
|
||||
}
|
||||
|
||||
.atc-btn svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Variants */
|
||||
.atc-google {
|
||||
background: #fff;
|
||||
color: #1a73e8;
|
||||
border-color: #d2e3fc;
|
||||
}
|
||||
|
||||
.atc-outlook {
|
||||
background: #0078d4;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.atc-office365 {
|
||||
background: #ea4c1d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.atc-apple {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.atc-yahoo {
|
||||
background: #5f01d1;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.atc-ics {
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
border-color: #ddd;
|
||||
}
|
||||
|
||||
.atc-footer {
|
||||
margin-top: 0.9rem;
|
||||
font-size: 0.8rem;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
@media (min-width: 520px) {
|
||||
.atc-buttons {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.atc-btn {
|
||||
flex: 1 1 calc(50% - 0.5rem);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="add-to-calendar-wrapper">
|
||||
<div class="add-to-calendar-title">Add “All Sports Calendar”</div>
|
||||
<div class="add-to-calendar-subtitle">
|
||||
Subscribe to keep all sports events up to date in your favorite calendar app.
|
||||
</div>
|
||||
|
||||
<div class="atc-buttons">
|
||||
<!-- Google Calendar -->
|
||||
<a
|
||||
class="atc-btn atc-google"
|
||||
href="https://calendar.google.com/calendar/u/0/r?cid=http%3A%2F%2Flocalhost%3A3000%2Fdata%2Fen%2Fall-sports%2Fcalendar.ics"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<!-- simple calendar icon -->
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<rect x="3" y="4" width="18" height="17" rx="2" ry="2" fill="none" stroke="currentColor" stroke-width="1.6"/>
|
||||
<line x1="3" y1="9" x2="21" y2="9" stroke="currentColor" stroke-width="1.6"/>
|
||||
<line x1="8" y1="3" x2="8" y2="7" stroke="currentColor" stroke-width="1.6"/>
|
||||
<line x1="16" y1="3" x2="16" y2="7" stroke="currentColor" stroke-width="1.6"/>
|
||||
</svg>
|
||||
<span>Google Calendar</span>
|
||||
</a>
|
||||
|
||||
<!-- Outlook.com (personal accounts) -->
|
||||
<a
|
||||
class="atc-btn atc-outlook"
|
||||
href="https://outlook.live.com/calendar/0/deeplink/subscribe?url=http%3A%2F%2Flocalhost%3A3000%2Fdata%2Fen%2Fall-sports%2Fcalendar.ics&name=All%20Sports%20Calendar"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<rect x="3" y="5" width="10" height="14" rx="1.5" fill="none" stroke="#ffffff" stroke-width="1.6"/>
|
||||
<path d="M13 7h8v10h-8" fill="none" stroke="#ffffff" stroke-width="1.6"/>
|
||||
</svg>
|
||||
<span>Outlook.com</span>
|
||||
</a>
|
||||
|
||||
<!-- Office 365 / Microsoft 365 (business) -->
|
||||
<a
|
||||
class="atc-btn atc-office365"
|
||||
href="https://outlook.office.com/calendar/0/deeplink/subscribe?url=http%3A%2F%2Flocalhost%3A3000%2Fdata%2Fen%2Fall-sports%2Fcalendar.ics&name=All%20Sports%20Calendar"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M5 5h8v14H5z" fill="none" stroke="#ffffff" stroke-width="1.6"/>
|
||||
<path d="M13 7h6v10h-6" fill="none" stroke="#ffffff" stroke-width="1.6"/>
|
||||
</svg>
|
||||
<span>Office 365</span>
|
||||
</a>
|
||||
|
||||
<!-- Apple Calendar (iCal) -->
|
||||
<a
|
||||
class="atc-btn atc-apple"
|
||||
href="webcal://localhost:3000/data/en/all-sports/calendar.ics"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<rect x="3" y="4" width="18" height="17" rx="2" ry="2" fill="none" stroke="#ffffff" stroke-width="1.6"/>
|
||||
<line x1="3" y1="9" x2="21" y2="9" stroke="#ffffff" stroke-width="1.6"/>
|
||||
<line x1="8" y1="3" x2="8" y2="7" stroke="#ffffff" stroke-width="1.6"/>
|
||||
<line x1="16" y1="3" x2="16" y2="7" stroke="#ffffff" stroke-width="1.6"/>
|
||||
</svg>
|
||||
<span>Apple Calendar</span>
|
||||
</a>
|
||||
|
||||
<!-- Yahoo Calendar -->
|
||||
<a
|
||||
class="atc-btn atc-yahoo"
|
||||
href="https://calendar.yahoo.com/?ics=http%3A%2F%2Flocalhost%3A3000%2Fdata%2Fen%2Fall-sports%2Fcalendar.ics"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="9" fill="none" stroke="#ffffff" stroke-width="1.6"/>
|
||||
<path d="M9 9l3 6 3-6" fill="none" stroke="#ffffff" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>Yahoo Calendar</span>
|
||||
</a>
|
||||
|
||||
<!-- Direct ICS download (for any other app) -->
|
||||
<a
|
||||
class="atc-btn atc-ics"
|
||||
href="http://localhost:3000/data/en/all-sports/calendar.ics"
|
||||
download="all-sports-calendar.ics"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M12 3v12" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
|
||||
<path d="M8 11l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<rect x="5" y="17" width="14" height="4" rx="1" fill="none" stroke="currentColor" stroke-width="1.6"/>
|
||||
</svg>
|
||||
<span>Download .ics</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="atc-footer">
|
||||
Note: this demo uses <code>localhost</code>. Replace it with your production domain when you deploy.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user