wip milano-cortina 2026

This commit is contained in:
Fabrice Lamant
2025-11-30 15:57:59 +01:00
parent b77e4d17b3
commit bc4359c937
24 changed files with 5 additions and 6915 deletions

View File

@ -1,44 +1,8 @@
# Olympics/Paralympics Calendars
This repository contains scripts to generate calendar files (.ics) for the Paris 2024 Summer Games, allowing you to sync sports events with your Google Calendar.
This repository contains scripts t
Work in progress for Milano-Cortina 2026 Olympics and Paralympics calendars.
> [!NOTE]
> Olympics calendars have been archived to the [olympics-closing](https://github.com/fabrice404/olympics-calendar/tree/olympics-closing) branch. \
> Paralympics calendars have been archived to the [olympics-closing](https://github.com/fabrice404/olympics-calendar/tree/paralympics-closing) branch.
## Usage
All calendar files are available at this URL: https://fabrice404.github.io/olympics-calendar/ \
Use the provided URLs to sync with your Google Calendar instead of downloading the files.
## Features
- **Automatic Updates**: Calendars update automatically during the event.
- **Customizable**: Select specific sports or teams you're interested in.
- **Easy Integration**: Sync directly with Google Calendar using provided URLs.
## Translation
The translations have been generated automatically. If you notice any incorrect translations, please accept my apologies and feel free to open an issue with the correct translation.
## Contribution
Contributions are welcome! Feel free to open issues or submit pull requests.
## Installation
1. Clone the repository:
```bash
git clone https://github.com/fabrice404/olympics-calendar.git
cd olympics-calendar
```
2. Install dependencies:
```bash
npm install
```
3. Generate the calendar files:
```bash
npm start
```
## License
This project is licensed under the MIT License.
> Paris 2024 Olympics calendars have been archived to the [2024-paris-olympics](https://github.com/fabrice404/olympics-calendar/tree/2024-paris-olympics) branch.\
> Paris 2024 Paralympics calendars have been archived to the [2024-paris-paralympics](https://github.com/fabrice404/olympics-calendar/tree/2024-paris-paralympics) branch.

View File

@ -1,26 +0,0 @@
import globals from "globals";
import pluginJs from "@eslint/js";
export default [
{
files: ["**/*.js"],
languageOptions: {
sourceType: "commonjs",
},
},
{
languageOptions: {
globals: globals.node,
},
},
pluginJs.configs.recommended,
{
rules: {
"comma-dangle": ["error", "always-multiline"],
complexity: ["error", 15],
quotes: ["error", "double"],
semi: ["error", "always"],
},
},
];

View File

@ -1,9 +0,0 @@
{
"verbose": false,
"ignore": [
"node_modules",
"cache/**",
"docs/**"
],
"ext": "ts,json,html,css"
}

5201
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +0,0 @@
{
"name": "olympics-calendar",
"version": "1.0.0",
"main": "src/index.js",
"scripts": {
"start": "find ./cache/**/*.html -mmin +10 -exec rm -f {} \\; | DEBUG=paris2024:* ts-node src/index.ts",
"dev": "DEBUG=paris2024:* nodemon src/index.ts",
"build": "tsc",
"lint": "eslint . --fix",
"test": "vitest run --coverage"
},
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/debug": "^4.1.12",
"@types/node": "^22.19.1",
"@vitest/coverage-istanbul": "^2.1.9",
"autoprefixer": "^10.4.22",
"cheerio": "^1.1.2",
"daisyui": "^4.12.24",
"debug": "^4.4.3",
"eslint": "^9.39.1",
"globals": "^15.15.0",
"nodemon": "^3.1.11",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.18",
"ts-node": "^10.9.2",
"typescript": "^5.9.3",
"vitest": "^2.0.5"
}
}

View File

@ -1,548 +0,0 @@
import Debug from "debug";
import autoprefixer from "autoprefixer";
import postcss from "postcss";
import tailwindcss from "tailwindcss";
import { Event, Medal, NOC, Sport } from "./types";
import { getAllSportsKeys, getSportIcon } from "./sports";
import { existsSync, writeFileSync } from "fs";
import { hasFile, readFile, saveFile } from "./io";
import * as cheerio from "cheerio";
import { getNOCFlag, getNOCName, isValidNOC } from "./nocs";
import * as translate from "./translate";
import { generateICS } from "./ics";
export class Calendar {
private language: string;
private debug: Debug.Debugger;
private events: Event[] = [];
private nocs: string[] = [];
private sports: Sport[] = [];
private medals: Medal[] = [];
constructor(language: string) {
this.language = language;
this.debug = Debug(`paris2024:calendar:${language}`);
}
private addSport(sportKey: string, sportName: string) {
if (!this.sports.find(sport => sport.key === sportKey)) {
// this.debug(`Adding sport: ${sportName} (${sportKey})`);
this.sports.push({
key: sportKey,
name: sportName,
NOCS: [],
});
}
}
private addNOC(noc: string) {
if (!this.nocs.includes(noc)) {
// this.debug(`Adding NOC: ${noc}`);
this.nocs.push(noc);
}
}
private addSportNOC(sportKey: string, sportName: string, noc: string) {
this.addSport(sportKey, sportName);
const sport = this.sports.find((sport) => sport.key === sportKey)!;
if (!sport.NOCS.includes(noc)) {
// this.debug(`Adding NOC: ${noc} to sport: ${sportKey}`);
sport.NOCS.push(noc);
}
};
public async generate() {
this.debug(`Generating calendar for ${this.language}`);
await Promise.all(getAllSportsKeys().map((sportKey) => this.getSportCalendar(sportKey)));
this.genereateEventsCeremonies();
this.generateCalendars();
this.generateMainPage();
this.generateTodaysPage();
this.genereateMedalsPage();
this.generateCSS();
}
private async getSportCalendar(sportKey: string) {
const schedule = await this.downloadScheduleFromOfficialWebsite(sportKey);
this.generateEventsFromSchedule(sportKey, schedule);
}
private async downloadScheduleFromOfficialWebsite(sportKey: string) {
// this.debug(`Checking cache for schedule for ${sportKey}`);
const cacheFile = `${__dirname}/../cache/${this.language}/${sportKey}.html`;
if (!hasFile(cacheFile)) {
this.debug(`Downloading schedule for ${sportKey} in ${this.language}`);
const response = await fetch(`https://olympics.com/${this.language}/paris-2024/paralympic-games/schedule/${sportKey}`);
const content = await response.text();
saveFile(cacheFile, content);
}
const html = readFile(cacheFile);
const $ = cheerio.load(html);
return JSON.parse($("#__NEXT_DATA__").text());
}
private generateEventsFromSchedule(sportKey: string, data: any) {
const sportName = data.query.pDisciplineLabel;
const sportIcon = getSportIcon(sportKey);
this.addSport(sportKey, sportName);
data.props.pageProps.scheduleDataSource.initialSchedule.units.forEach((unit: any) => {
unit.startDateTimeUtc = new Date(unit.startDate).toISOString().replace(".000", "");
unit.endDateTimeUtc = new Date(unit.endDate).toISOString().replace(".000", "");
const slugify = (text: string) => text.toLowerCase().replace(/\s/g, "-")
.replace(/[^a-z0-9-]/g, "")
.replace(/-+/g, "-");
const event: Event = {
UID: `${unit.startDateTimeUtc.replace(/[:-]/g, "")}-${sportKey}-${slugify(unit.eventUnitName).toUpperCase()}`,
DTSTAMP: unit.startDateTimeUtc.replace(/[:-]/g, ""),
DTSTART: unit.startDateTimeUtc.replace(/[:-]/g, ""),
DTEND: unit.endDateTimeUtc.replace(/[:-]/g, ""),
DESCRIPTION: `${sportName} - ${unit.eventUnitName}`,
SUMMARY: `${sportIcon} ${unit.eventUnitName}`.trim(),
LOCATION: unit.venueDescription,
_SPORT: sportKey,
_NOCS: [],
_COMPETITORS: [],
_UNITNAME: unit.eventUnitName,
_MEDAL: !!unit.medalFlag,
_GENDER: unit.genderCode,
};
if (unit.competitors) {
const competitors = unit.competitors
.filter((competitor: any) => competitor.noc && isValidNOC(competitor.noc))
.sort((a: any, b: any) => a.order > b.order ? 1 : -1);
for (const competitor of competitors) {
this.addSportNOC(sportKey, sportName, competitor.noc);
this.addNOC(competitor.noc);
event._COMPETITORS.push({ noc: competitor.noc, name: competitor.name });
if (!event._NOCS.includes(competitor.noc)) {
event._NOCS.push(competitor.noc);
}
switch (competitor.results?.medalType) {
case "ME_GOLD": this.medals.push({ name: competitor.name, noc: competitor.noc, sport: sportName, unit: unit.eventUnitName, date: unit.endDateTimeUtc, color: "gold" }); break;
case "ME_SILVER": this.medals.push({ name: competitor.name, noc: competitor.noc, sport: sportName, unit: unit.eventUnitName, date: unit.endDateTimeUtc, color: "silver" }); break;
case "ME_BRONZE": this.medals.push({ name: competitor.name, noc: competitor.noc, sport: sportName, unit: unit.eventUnitName, date: unit.endDateTimeUtc, color: "bronze" }); break;
}
}
// two competitors, we put them in the summary
if (competitors.length === 2) {
const competitor1 = competitors.shift();
const competitor2 = competitors.shift();
event.UID += `-${competitor1.noc}-${competitor2.noc}`;
if (competitor1.name !== getNOCName(competitor1.noc)) {
event.SUMMARY = `${sportIcon} ${competitor1.name} ${getNOCFlag(competitor1.noc)} - ${getNOCFlag(competitor2.noc)} ${competitor2.name}`;
} else {
event.SUMMARY = `${sportIcon} ${competitor1.noc} ${getNOCFlag(competitor1.noc)} - ${getNOCFlag(competitor2.noc)} ${competitor2.noc}`;
}
} else if (competitors.length !== 0) {
// more than two, we put them in the description
competitors
.sort((a: any, b: any) => a.name > b.name ? 1 : -1)
.forEach((competitor: any) => {
if (competitor.name !== getNOCName(competitor.noc)) {
event.DESCRIPTION += `\\n${getNOCFlag(competitor.noc)} ${competitor.name}`;
} else {
event.DESCRIPTION += `\\n${getNOCFlag(competitor.noc)} ${competitor.noc}`;
}
});
}
}
this.events.push(event);
});
}
private genereateEventsCeremonies() {
let startDateUtc = new Date("2024-08-28T18:00:00Z").toISOString().replace(".000", "");
let endDateUtc = new Date("2024-08-28T21:00:00Z").toISOString().replace(".000", "");
const opening: Event = {
UID: `${startDateUtc.replace(/[:-]/g, "")}-opening-ceremony`,
DTSTAMP: startDateUtc.replace(/[:-]/g, ""),
DTSTART: startDateUtc.replace(/[:-]/g, ""),
DTEND: endDateUtc.replace(/[:-]/g, ""),
DESCRIPTION: translate.translate(`Paris 2024 - {{translate_openingCeremony}}`, this.language),
SUMMARY: translate.translate(`Paris 2024 - {{translate_openingCeremony}}`, this.language),
LOCATION: "Paris",
_COMPETITORS: [],
_GENDER: "",
_MEDAL: false,
_NOCS: this.nocs,
_SPORT: "",
_UNITNAME: translate.translate(`Paris 2024 - {{translate_openingCeremony}}`, this.language),
};
this.events.push(opening);
startDateUtc = new Date("2024-09-08T19:00:00Z").toISOString().replace(".000", "");
endDateUtc = new Date("2024-09-08T22:00:00Z").toISOString().replace(".000", "");
const closing: Event = {
UID: `${startDateUtc.replace(/[:-]/g, "")}-closing-ceremony`,
DTSTAMP: startDateUtc.replace(/[:-]/g, ""),
DTSTART: startDateUtc.replace(/[:-]/g, ""),
DTEND: endDateUtc.replace(/[:-]/g, ""),
DESCRIPTION: translate.translate(`Paris 2024 - {{translate_closingCeremony}}`, this.language),
SUMMARY: translate.translate(`Paris 2024 - {{translate_closingCeremony}}`, this.language),
LOCATION: "Stade de France, Saint-Denis",
_COMPETITORS: [],
_GENDER: "",
_MEDAL: false,
_NOCS: this.nocs,
_SPORT: "",
_UNITNAME: translate.translate(`Paris 2024 - {{translate_closingCeremony}}`, this.language),
};
this.events.push(closing);
}
private getKey(sportKey: string, noc: string) {
return `${this.language}/${sportKey}/${noc}`;
}
private sortEvents(a: Event, b: Event) {
if (a.DTSTART !== b.DTSTART) {
return a.DTSTART > b.DTSTART ? 1 : -1;
}
if (a.DTEND !== b.DTEND) {
return a.DTEND > b.DTEND ? 1 : -1;
}
if (a.SUMMARY !== b.SUMMARY) {
return a.SUMMARY > b.SUMMARY ? 1 : -1;
}
if (a.DESCRIPTION !== b.DESCRIPTION) {
return a.DESCRIPTION > b.DESCRIPTION ? 1 : -1;
}
return 0;
}
private generateCalendars() {
// sports
for (const sport of this.sports) {
// sport/general
let events = this.events
.filter((event) => event._SPORT === sport.key)
.sort(this.sortEvents);
let key = this.getKey(sport.key, "general");
let title = `${getSportIcon(sport.key)} ${sport.name} | Paris 2024`;
if (events.length > 0) {
generateICS(title, key, events);
}
// sport/medals
events = this.events
.filter((event) => event._SPORT === sport.key && event._MEDAL)
.sort(this.sortEvents);
key = this.getKey(sport.key, "medals");
title = `${getSportIcon(sport.key)} ${sport.name} 🏅 | Paris 2024`;
if (events.length > 0) {
generateICS(title, key, events);
}
// sport/noc
for (const noc of sport.NOCS) {
events = this.events
.filter((event) => event._SPORT === sport.key && event._NOCS.includes(noc))
.sort(this.sortEvents);
key = this.getKey(sport.key, noc);
title = `${getNOCFlag(noc)} ${getNOCName(noc)} ${sport.name} | Paris 2024`;
if (events.length > 0) {
generateICS(title, key, events);
}
}
}
// nocs
for (const noc of this.nocs) {
// general/noc
let events = this.events
.filter((event) => event._NOCS.includes(noc))
.sort(this.sortEvents);
let key = this.getKey("general", noc);
let title = `${getNOCFlag(noc)} ${getNOCName(noc)} | Paris 2024`;
if (events.length > 0) {
generateICS(title, key, events);
}
// medals/noc
events = this.events
.filter((event) => event._NOCS.includes(noc) && event._MEDAL)
.sort(this.sortEvents);
key = this.getKey("medals", noc);
title = `${getNOCFlag(noc)} ${getNOCName(noc)} 🏅 | Paris 2024`
if (events.length > 0) {
generateICS(title, key, events);
}
}
// general/general
const events = this.events
.sort(this.sortEvents);
const key = this.getKey("general", "general");
const title = `Paris 2024`;
if (events.length > 0) {
generateICS(title, key, events);
}
// medals/general
const medals = this.events
.filter((event) => event._MEDAL)
.sort(this.sortEvents);
const medalsKey = this.getKey("medals", "general");
const medalsTitle = `🏅 Paris 2024`;
if (medals.length > 0) {
generateICS(medalsTitle, medalsKey, medals);
}
}
private generateMainPage() {
const accordionClass = "collapse collapse-arrow bg-gray-100 mb-1"
const buttonClass = "btn btn-sm bg-gray-300 min-w-24 mb-1";
const calendars: string[] = [];
calendars.push(`<div class="${accordionClass}">`);
calendars.push(` <input type="radio" name="accordion" checked="checked">`);
calendars.push(` <div class="collapse-title text-xl font-medium">{{translate_allSports}}</div>`);
calendars.push(` <div class="collapse-content text-center">`)
calendars.push(` <div>`);
calendars.push(` <button class="${buttonClass}" onclick="showModal('general/general', '${this.language}');">{{translate_fullSchedule}}</button>`);
calendars.push(` </div>`);
for (const noc of this.nocs.sort()) {
calendars.push(` <button class="${buttonClass}" onclick="showModal('general/${noc}', '${this.language}');">${getNOCFlag(noc)} ${noc}</button>`);
}
calendars.push(` </div>`);
calendars.push(`</div>`);
calendars.push(`<div class="${accordionClass}">`);
calendars.push(` <input type="radio" name="accordion">`);
calendars.push(` <div class="collapse-title text-xl font-medium">🏅 {{translate_medalEvents}}</div>`);
calendars.push(` <div class="collapse-content text-center">`)
calendars.push(` <div>`);
calendars.push(` <button class="${buttonClass}" onclick="showModal('medals/general', '${this.language}');">{{translate_fullSchedule}}</button>`);
calendars.push(` </div>`);
for (const noc of this.nocs.sort()) {
calendars.push(` <button class="${buttonClass}" onclick="showModal('medals/${noc}', '${this.language}');">${getNOCFlag(noc)} ${noc}</button>`);
}
calendars.push(` </div>`);
calendars.push(`</div>`);
calendars.push(`<div class="${accordionClass}">`);
calendars.push(` <input type="radio" name="accordion">`);
calendars.push(` <div class="collapse-title text-xl font-medium">📅 {{translate_todaysEvents}}</div>`);
calendars.push(` <div class="collapse-content text-center">`)
for (const noc of this.nocs.sort()) {
calendars.push(` <a class="${buttonClass}" href="./today.html?noc=${noc}">${getNOCFlag(noc)} ${noc}</a>`);
}
calendars.push(` </div>`);
calendars.push(`</div>`);
for (const sport of this.sports.sort((a: Sport, b: Sport) => a.name > b.name ? 1 : -1)) {
calendars.push(`<div class="${accordionClass}">`);
calendars.push(` <input type="radio" name="accordion">`);
calendars.push(` <div class="collapse-title text-xl font-medium">${getSportIcon(sport.key)} ${sport.name}</div>`);
calendars.push(` <div class="collapse-content text-center">`)
calendars.push(` <div>`);
calendars.push(` <button class="${buttonClass}" onclick="showModal('${sport.key}/general', '${this.language}');">{{translate_fullSchedule}}</button>`);
calendars.push(` <button class="${buttonClass}" onclick="showModal('${sport.key}/medals', '${this.language}');">🏅 {{translate_medalEvents}}</button>`);
calendars.push(` </div>`);
for (const noc of sport.NOCS.sort()) {
calendars.push(` <button class="${buttonClass}" onclick="showModal('${sport.key}/${noc}', '${this.language}');">${getNOCFlag(noc)} ${noc}</button>`);
}
calendars.push(` </div>`);
calendars.push(`</div>`);
}
const template = readFile(`${__dirname}/index/template.html`);
const output = translate.translate(
template.replace("{{calendars}}", calendars.join("\r\n")),
this.language,
);
saveFile(
this.language === "en" ?
"docs/index.html" :
`docs/${this.language}/index.html`,
output);
}
private generateTodaysPage() {
const content: string[] = [];
for (const event of this.events.sort(this.sortEvents)) {
let sport = this.sports.find((sport) => sport.key === event._SPORT);
if (!sport) {
sport = {
name: "Ceremony",
key: "",
NOCS: [],
};
}
let medalColor = "";
if (event._MEDAL) {
if (event._UNITNAME.match(/bronze/gi)) {
medalColor = "bg-orange-700";
} else {
medalColor = "bg-amber-400";
}
}
content.push(`<div class="event py-4 ${medalColor}" data-start="${event.DTSTART}" data-end="${event.DTEND}" data-noc="${event._NOCS.sort().join(",")}" >`);
content.push(` <div class=\"time w-1/4 align-top text-right inline-block text-5xl text-center tabular-nums pr-2 border-r border-slate-900/10\">`);
content.push(" <span class=\"time-start\">__:__</span>");
content.push(" <div class=\"time-end text-xs\">__:__</div>");
content.push(" </div>");
content.push(" <div class=\"w-3/5 align-top inline-block text-black pl-2\">");
content.push(" <div class=\"text-2xl\">");
content.push(` ${event._MEDAL ? "🏅" : ""}`);
content.push(` ${sport.name.toUpperCase()}`);
if (event._GENDER === "M") {
content.push(` <span class=\"text-xs align-middle bg-blue-400 text-white py-1 px-2 rounded-xl\">{{translate_genderMen}}</span>`);
} else if (event._GENDER === "W") {
content.push(` <span class=\"text-xs align-middle bg-pink-400 text-white py-1 px-2 rounded-xl\">{{translate_genderWomen}}</span>`);
}
content.push(" </div>");
content.push(` <div>${event._UNITNAME}</div>`);
if (event._COMPETITORS) {
if (event._COMPETITORS.length === 2) {
content.push(` <div class="competitors">`);
content.push(` ${event._COMPETITORS[0].name}`);
content.push(` ${getNOCFlag(event._COMPETITORS[0].noc)}`);
content.push(` -`);
content.push(` ${getNOCFlag(event._COMPETITORS[1].noc)}`);
content.push(` ${event._COMPETITORS[1].name}`);
content.push(` </div>`);
} else {
event._COMPETITORS.sort((a, b) => a.name > b.name ? 1 : -1).forEach((competitor) => {
content.push(` <div class="competitor ${competitor.noc}">${getNOCFlag(competitor.noc)} ${competitor.name} </div>`);
});
}
}
content.push(" </div>");
content.push("</div>");
}
const template = readFile(`${__dirname}/today/template.html`);
const output = translate.translate(
template.replace("{{events}}", content.join("\r\n")),
this.language,
);
saveFile(
this.language === "en" ?
"docs/today.html" :
`docs/${this.language}/today.html`,
output
);
}
private genereateMedalsPage() {
const table: any[] = [];
for (const medal of this.medals) {
if (!table.find((noc) => noc.noc === medal.noc)) {
table.push({ noc: medal.noc, gold: 0, silver: 0, bronze: 0 });
}
table.find((noc) => noc.noc === medal.noc)[medal.color] += 1;
}
const content: string[] = [];
content.push(`<div class="collapse bg-gray-100 mb-1">`);
content.push(` <div class="flex collapse-title text-xl font-medium">`);
content.push(` <span class="inline-block flex-auto"></span>`);
content.push(` <span class="inline-block text-center w-1/6 flex-none gold">&#9679;</span>`);
content.push(` <span class="inline-block text-center w-1/6 flex-none silver">&#9679;</span>`);
content.push(` <span class="inline-block text-center w-1/6 flex-none bronze">&#9679;</span>`);
content.push(` <span class="inline-block text-center w-1/6 flex-none">TOTAL</span>`);
content.push(` </div>`);
content.push(`</div>`);
table.sort((a, b) => {
if (a.gold !== b.gold) {
return a.gold < b.gold ? 1 : -1;
}
if (a.silver !== b.silver) {
return a.silver < b.silver ? 1 : -1;
}
if (a.bronze !== b.bronze) {
return a.bronze < b.bronze ? 1 : -1;
}
return getNOCName(a.noc) > getNOCName(b.noc) ? 1 : -1;
}).forEach((noc) => {
content.push(`<div class="collapse collapse-arrow bg-gray-100 mb-1">`);
content.push(` <input type="radio" name="accordion">`);
content.push(` <div class="flex collapse-title text-xl font-medium">`);
content.push(` <span class="inline-block flex-auto">${getNOCFlag(noc.noc)} ${getNOCName(noc.noc)}</span>`);
content.push(` <span class="inline-block text-center w-1/6 flex-none">${noc.gold}</span>`);
content.push(` <span class="inline-block text-center w-1/6 flex-none">${noc.silver}</span>`);
content.push(` <span class="inline-block text-center w-1/6 flex-none">${noc.bronze}</span>`);
content.push(` <span class="inline-block text-center w-1/6 flex-none">${noc.gold + noc.silver + noc.bronze}</span>`);
content.push(` </div>`);
content.push(` <div class="collapse-content">`)
content.push(` <table class="table-full">`);
let lastDate = "";
for (const medal of this.medals
.filter((m) => m.noc === noc.noc)
.sort((a, b) => {
if (a.date !== b.date) {
return a.date > b.date ? -1 : 1
}
const colors = ["gold", "silver", "bronze"];
if (a.color !== b.color) {
return colors.indexOf(a.color) > colors.indexOf(b.color) ? 1 : -1;
}
return a.name > b.name ? 1 : -1;
})
) {
let medalDate = medal.date.substring(0, 10);
if (medalDate !== lastDate) {
content.push(` <tr><td colspan="3" class="font-medium">${medalDate}</td></tr>`);
}
lastDate = medalDate;
content.push(` <tr>`);
content.push(` <td class="${medal.color}">&#9679;</td>`);
content.push(` <td>${medal.name}</td>`);
content.push(` <td>${medal.sport} - ${medal.unit}</td>`);
content.push(` </tr>`);
}
content.push(` </table>`);
content.push(` </div>`);
content.push(`</div>`);
})
const template = readFile(`${__dirname}/medals/template.html`);
const output = translate.translate(
template.replace("{{medals}}", content.join("\r\n")),
this.language,
);
saveFile(
this.language === "en" ?
"docs/medals.html" :
`docs/${this.language}/medals.html`,
output
);
}
private generateCSS() {
postcss([autoprefixer, tailwindcss])
.process(readFile(`${__dirname}/index/template.css`), { from: "index/template.css", to: "docs/main.css" })
.then((result) => {
saveFile("docs/main.css", result.css);
});
;
}
}

View File

@ -1,60 +0,0 @@
import * as fs from "node:fs";
import Debug from "debug";
import { Event } from "./types";
const debug = Debug("paris2024:ics");
/**
* generateICS generates the calendar for given events on ICS format
* @param {string} title
* @param {string} key
* @param {object[]} events
*/
export const generateICS = (title: string, key: string, events: Event[]): void => {
// debug(`Generating ICS file for ${title} (${key}) with ${events.length} events`);
const lines: string[] = [];
lines.push("BEGIN:VCALENDAR");
lines.push("VERSION:2.0");
lines.push(`PRODID:-//fabrice404//olympics-calendar//${key}//EN`);
lines.push(`X-WR-CALNAME:${title}`);
lines.push(`NAME:${title}`);
events.forEach((event) => {
lines.push("BEGIN:VEVENT");
lines.push(
...Object.entries(event)
.filter(([key]) => !key.startsWith("_"))
.map(([key, value]) => {
let result = `${key}:${value}`;
if (result.length > 75) {
if (key === "DESCRIPTION") {
const lines = [];
while (result.length > 75) {
let index = 75;
while (result[index] !== " " && index > 0) {
index--;
}
lines.push(result.slice(0, index));
result = " " + result.slice(index).trim();
}
lines.push(result);
return lines.join("\r\n").trim();
}
return `${key}:${value}`.slice(0, 75);
}
return `${key}:${value}`;
}),
);
lines.push("END:VEVENT");
});
lines.push("END:VCALENDAR");
const calendarPath = `${__dirname}/../docs/${key}.ics`;
const folder = calendarPath.split("/").slice(0, -1).join("/");
fs.mkdirSync(folder, { recursive: true });
fs.writeFileSync(calendarPath, lines.join("\r\n"));
};

View File

@ -1,11 +0,0 @@
import { Calendar } from "./calendar";
const main = async () => {
// for (const language of ["en", "ja", "ko", "ru", "zh"]) {
for (const language of ["en"]) {
const cal = new Calendar(language)
await cal.generate();
}
};
main();

View File

@ -1,39 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html {
font-size: 12px;
}
.blue {
color: #0081C8;
}
.yellow {
color: #FCB131;
}
.black {
color: #000000;
}
.green {
color: #00A651;
}
.red {
color: #EE334E;
}
.gold {
color: #FFD700;
}
.silver {
color: #C0C0C0;
}
.bronze {
color: #CD7F32;
}

View File

@ -1,81 +0,0 @@
<!DOCTYPE html>
<html data-theme="cmyk">
<head>
<title>Paris 2024 - {{translate_calendars}}</title>
<link href="https://fabrice404.github.io/olympics-calendar/main.css?refresh={{refresh}}" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Paris 2024 - {{translate_calendars}}">
<meta name="keywords" content="Paris 2024 - {{translate_calendars}}">
<meta name="author" content="Fabrice LAMANT">
</head>
<body>
<div class="p-4">
<div class="navbar bg-base-100">
<div class="navbar-start">
<div class="dropdown">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7" />
</svg>
</div>
<ul tabindex="0" class="menu menu-sm dropdown-content bg-base-100 rounded-box z-50 mt-3 w-52 p-2 shadow">
<li><a href="./index.html">{{translate_calendars}}</a></li>
<li><a href="./today.html">{{translate_todaysEvents}}</a></li>
<li><a href="./medals.html">{{translate_medalsTable}}</a></li>
<hr />
<!-- <li><a href="https://fabrice404.github.io/olympics-calendar/index.html">English</a></li>
<li><a href="https://fabrice404.github.io/olympics-calendar/ja/index.html">日本語</a></li>
<li><a href="https://fabrice404.github.io/olympics-calendar/ko/index.html">한국어</a></li>
<li><a href="https://fabrice404.github.io/olympics-calendar/ru/index.html">Русский</a></li>
<li><a href="https://fabrice404.github.io/olympics-calendar/zh/index.html">中文</a></li>
<hr /> -->
<li><a href="https://github.com/fabrice404/olympics-calendar" target="_blank">Source code</a></li>
</ul>
</div>
</div>
<div class="navbar-center">
<a class="btn btn-ghost text-xl" href="./">Paris 2024 - {{translate_calendars}}</a>
</div>
<div class="navbar-end">
</div>
</div>
<div>
{{calendars}}
</div>
<dialog id="modal" class="modal">
<div class="modal-box">
</h3>
<input type="text" class="input input-bordered w-full text-sm" id="link" readonly="readonly"></input>
<div class="modal-action">
<form method="dialog">
<button class="btn">Close</button>
</form>
</div>
</div>
</dialog>
<div class="text-sm my-10 text-center">
{{translate_disclaimer}}
</div>
</div>
<script>
const showModal = (key, language) => {
document.querySelector("#modal #link")
.setAttribute('value', `https://fabrice404.github.io/olympics-calendar/${language}/${key}.ics`);
modal.showModal();
};
</script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-0KQC1F1K4H"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', 'G-0KQC1F1K4H');
</script>
</body>

View File

@ -1,11 +0,0 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
export const saveFile = (path: string, content: string): void => {
const folder = path.split("/").slice(0, -1).join("/");
mkdirSync(folder, { recursive: true });
writeFileSync(path, content);
};
export const hasFile = (path: string) => existsSync(path);
export const readFile = (path: string) => readFileSync(path, "utf-8");

View File

@ -1,72 +0,0 @@
<!DOCTYPE html>
<html data-theme="cmyk">
<head>
<title>Paris 2024 - {{translate_medalsTable}}</title>
<link href="https://fabrice404.github.io/olympics-calendar/main.css?refresh={{refresh}}" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Paris 2024 - {{translate_medalsTable}}">
<meta name="keywords" content="Paris 2024 - {{translate_medalsTable}}">
<meta name="author" content="Fabrice LAMANT">
</head>
<body>
<div class="p-4">
<div class="navbar bg-base-100">
<div class="navbar-start">
<div class="dropdown">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7" />
</svg>
</div>
<ul tabindex="0" class="menu menu-sm dropdown-content bg-base-100 rounded-box z-50 mt-3 w-52 p-2 shadow">
<li><a href="./index.html">{{translate_calendars}}</a></li>
<li><a href="./today.html">{{translate_todaysEvents}}</a></li>
<li><a href="./medals.html">{{translate_medalsTable}}</a></li>
<hr />
<!-- <li><a href="https://fabrice404.github.io/olympics-calendar/index.html">English</a></li>
<li><a href="https://fabrice404.github.io/olympics-calendar/ja/index.html">日本語</a></li>
<li><a href="https://fabrice404.github.io/olympics-calendar/ko/index.html">한국어</a></li>
<li><a href="https://fabrice404.github.io/olympics-calendar/ru/index.html">Русский</a></li>
<li><a href="https://fabrice404.github.io/olympics-calendar/zh/index.html">中文</a></li>
<hr /> -->
<li><a href="https://github.com/fabrice404/olympics-calendar" target="_blank">Source code</a></li>
</ul>
</div>
</div>
<div class="navbar-center">
<a class="btn btn-ghost text-xl" href="./">Paris 2024 - {{translate_medalsTable}}</a>
</div>
<div class="navbar-end">
</div>
</div>
<div role="alert" class="alert my-4 hidden">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-info h-6 w-6 shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>{{translate_medalsTableError}}</span>
</div>
<div>
{{medals}}
</div>
<div class="text-sm my-10 text-center">
{{translate_disclaimer}}
</div>
</div>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-0KQC1F1K4H"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', 'G-0KQC1F1K4H');
</script>
</body>

View File

@ -1,243 +0,0 @@
import { NOC } from "./types";
const NOCS: Map<string, NOC> = new Map([
["AFG", { icon: "🇦🇫", name: "Afghanistan" }],
["ALB", { icon: "🇦🇱", name: "Albania" }],
["ALG", { icon: "🇩🇿", name: "Algeria" }],
["AIN", { icon: "🇦🇸", name: "American Samoa" }],
["AND", { icon: "🇦🇩", name: "Andorra" }],
["ANG", { icon: "🇦🇴", name: "Angola" }],
["ANT", { icon: "🇦🇬", name: "Antigua and Barbuda" }],
["ARG", { icon: "🇦🇷", name: "Argentina" }],
["ARM", { icon: "🇦🇲", name: "Armenia" }],
["ARU", { icon: "🇦🇼", name: "Aruba" }],
["ASA", { icon: "🇦🇸", name: "American Samoa" }],
["AUS", { icon: "🇦🇺", name: "Australia" }],
["AUT", { icon: "🇦🇹", name: "Austria" }],
["AZE", { icon: "🇦🇿", name: "Azerbaijan" }],
["BAH", { icon: "🇧🇸", name: "Bahamas" }],
["BAN", { icon: "🇧🇩", name: "Bangladesh" }],
["BAR", { icon: "🇧🇧", name: "Barbados" }],
["BDI", { icon: "🇧🇮", name: "Burundi" }],
["BEL", { icon: "🇧🇪", name: "Belgium" }],
["BEN", { icon: "🇧🇯", name: "Benin" }],
["BER", { icon: "🇧🇲", name: "Bermuda" }],
["BHU", { icon: "🇧🇹", name: "Bhutan" }],
["BIH", { icon: "🇧🇦", name: "Bosnia and Herzegovina" }],
["BIZ", { icon: "🇧🇿", name: "Belize" }],
["BOL", { icon: "🇧🇴", name: "Bolivia" }],
["BOT", { icon: "🇧🇼", name: "Botswana" }],
["BRA", { icon: "🇧🇷", name: "Brazil" }],
["BRN", { icon: "🇧🇭", name: "Bahrain" }],
["BRU", { icon: "🇧🇳", name: "Brunei" }],
["BUL", { icon: "🇧🇬", name: "Bulgaria" }],
["BUR", { icon: "🇧🇫", name: "Burkina Faso" }],
["CAF", { icon: "🇨🇫", name: "Central African Republic" }],
["CAM", { icon: "🇰🇭", name: "Cambodia" }],
["CAN", { icon: "🇨🇦", name: "Canada" }],
["CAY", { icon: "🇰🇾", name: "Cayman Islands" }],
["CGO", { icon: "🇨🇬", name: "Congo" }],
["CHA", { icon: "🇹🇩", name: "Chad" }],
["CHI", { icon: "🇨🇱", name: "Chile" }],
["CHN", { icon: "🇨🇳", name: "China" }],
["CIV", { icon: "🇨🇮", name: "Côte d'Ivoire" }],
["CMR", { icon: "🇨🇲", name: "Cameroon" }],
["COD", { icon: "🇨🇩", name: "Democratic Republic of the Congo" }],
["COK", { icon: "🇨🇰", name: "Cook Islands" }],
["COL", { icon: "🇨🇴", name: "Colombia" }],
["COM", { icon: "🇰🇲", name: "Comoros" }],
["CPV", { icon: "🇨🇻", name: "Cabo Verde" }],
["CRC", { icon: "🇨🇷", name: "Costa Rica" }],
["CRO", { icon: "🇭🇷", name: "Croatia" }],
["CUB", { icon: "🇨🇺", name: "Cuba" }],
["CYP", { icon: "🇨🇾", name: "Cyprus" }],
["CZE", { icon: "🇨🇿", name: "Czechia" }],
["DEN", { icon: "🇩🇰", name: "Denmark" }],
["DJI", { icon: "🇩🇯", name: "Djibouti" }],
["DMA", { icon: "🇩🇲", name: "Dominica" }],
["DOM", { icon: "🇩🇴", name: "Dominican Republic" }],
["ECU", { icon: "🇪🇨", name: "Ecuador" }],
["EGY", { icon: "🇪🇬", name: "Egypt" }],
["EOR", { icon: "🏳️", name: "Refugee Olympic Team" }],
["ERI", { icon: "🇪🇷", name: "Eritrea" }],
["ESA", { icon: "🇸🇻", name: "El Salvador" }],
["ESP", { icon: "🇪🇸", name: "Spain" }],
["EST", { icon: "🇪🇪", name: "Estonia" }],
["ETH", { icon: "🇪🇹", name: "Ethiopia" }],
["FIJ", { icon: "🇫🇯", name: "Fiji" }],
["FIN", { icon: "🇫🇮", name: "Finland" }],
["FRA", { icon: "🇫🇷", name: "France" }],
["FSM", { icon: "🇫🇲", name: "Federated States of Micronesia" }],
["GAB", { icon: "🇬🇦", name: "Gabon" }],
["GAM", { icon: "🇬🇲", name: "Gambia" }],
["GBR", { icon: "🇬🇧", name: "Great Britain" }],
["GBS", { icon: "🇬🇼", name: "Guinea-Bissau" }],
["GEO", { icon: "🇬🇪", name: "Georgia" }],
["GEQ", { icon: "🇬🇶", name: "Equatorial Guinea" }],
["GER", { icon: "🇩🇪", name: "Germany" }],
["GHA", { icon: "🇬🇭", name: "Ghana" }],
["GRE", { icon: "🇬🇷", name: "Greece" }],
["GRN", { icon: "🇬🇩", name: "Grenada" }],
["GUA", { icon: "🇬🇹", name: "Guatemala" }],
["GUI", { icon: "🇬🇳", name: "Guinea" }],
["GUM", { icon: "🇬🇺", name: "Guam" }],
["GUY", { icon: "🇬🇾", name: "Guyana" }],
["HAI", { icon: "🇭🇹", name: "Haiti" }],
["HKG", { icon: "🇭🇰", name: "Hong Kong" }],
["HON", { icon: "🇭🇳", name: "Honduras" }],
["HUN", { icon: "🇭🇺", name: "Hungary" }],
["INA", { icon: "🇮🇩", name: "Indonesia" }],
["IND", { icon: "🇮🇳", name: "India" }],
["IRI", { icon: "🇮🇷", name: "Iran" }],
["IRL", { icon: "🇮🇪", name: "Ireland" }],
["IRQ", { icon: "🇮🇶", name: "Iraq" }],
["ISL", { icon: "🇮🇸", name: "Iceland" }],
["ISR", { icon: "🇮🇱", name: "Israel" }],
["ISV", { icon: "🇻🇮", name: "U.S. Virgin Islands" }],
["ITA", { icon: "🇮🇹", name: "Italy" }],
["IVB", { icon: "🇻🇬", name: "British Virgin Islands" }],
["JAM", { icon: "🇯🇲", name: "Jamaica" }],
["JOR", { icon: "🇯🇴", name: "Jordan" }],
["JPN", { icon: "🇯🇵", name: "Japan" }],
["KAZ", { icon: "🇰🇿", name: "Kazakhstan" }],
["KEN", { icon: "🇰🇪", name: "Kenya" }],
["KGZ", { icon: "🇰🇬", name: "Kyrgyzstan" }],
["KIR", { icon: "🇰🇮", name: "Kiribati" }],
["KOR", { icon: "🇰🇷", name: "Korea" }],
["KOS", { icon: "🇽🇰", name: "Kosovo" }],
["KSA", { icon: "🇸🇦", name: "Saudi Arabia" }],
["KUW", { icon: "🇰🇼", name: "Kuwait" }],
["LAO", { icon: "🇱🇦", name: "Laos" }],
["LAT", { icon: "🇱🇻", name: "Latvia" }],
["LBA", { icon: "🇱🇾", name: "Libya" }],
["LBN", { icon: "🇱🇧", name: "Lebanon" }],
["LBR", { icon: "🇱🇷", name: "Liberia" }],
["LCA", { icon: "🇱🇨", name: "Saint Lucia" }],
["LES", { icon: "🇱🇸", name: "Lesotho" }],
["LIE", { icon: "🇱🇮", name: "Liechtenstein" }],
["LTU", { icon: "🇱🇹", name: "Lithuania" }],
["LUX", { icon: "🇱🇺", name: "Luxembourg" }],
["MAD", { icon: "🇲🇬", name: "Madagascar" }],
["MAR", { icon: "🇲🇦", name: "Morocco" }],
["MAS", { icon: "🇲🇾", name: "Malaysia" }],
["MAW", { icon: "🇲🇼", name: "Malawi" }],
["MDA", { icon: "🇲🇩", name: "Moldova" }],
["MDV", { icon: "🇲🇻", name: "Maldives" }],
["MEX", { icon: "🇲🇽", name: "Mexico" }],
["MGL", { icon: "🇲🇳", name: "Mongolia" }],
["MHL", { icon: "🇲🇭", name: "Marshall Islands" }],
["MKD", { icon: "🇲🇰", name: "North Macedonia" }],
["MLI", { icon: "🇲🇱", name: "Mali" }],
["MLT", { icon: "🇲🇹", name: "Malta" }],
["MNE", { icon: "🇲🇪", name: "Montenegro" }],
["MON", { icon: "🇲🇨", name: "Monaco" }],
["MOZ", { icon: "🇲🇿", name: "Mozambique" }],
["MRI", { icon: "🇲🇺", name: "Mauritius" }],
["MTN", { icon: "🇲🇷", name: "Mauritania" }],
["MYA", { icon: "🇲🇲", name: "Myanmar" }],
["NAM", { icon: "🇳🇦", name: "Namibia" }],
["NCA", { icon: "🇳🇮", name: "Nicaragua" }],
["NED", { icon: "🇳🇱", name: "Netherlands" }],
["NEP", { icon: "🇳🇵", name: "Nepal" }],
["NGR", { icon: "🇳🇬", name: "Nigeria" }],
["NIG", { icon: "🇳🇪", name: "Niger" }],
["NOR", { icon: "🇳🇴", name: "Norway" }],
["NRU", { icon: "🇳🇷", name: "Nauru" }],
["NZL", { icon: "🇳🇿", name: "New Zealand" }],
["OMA", { icon: "🇴🇲", name: "Oman" }],
["PAK", { icon: "🇵🇰", name: "Pakistan" }],
["PAN", { icon: "🇵🇦", name: "Panama" }],
["PAR", { icon: "🇵🇾", name: "Paraguay" }],
["PER", { icon: "🇵🇪", name: "Peru" }],
["PHI", { icon: "🇵🇭", name: "Philippines" }],
["PLE", { icon: "🇵🇸", name: "Palestine" }],
["PLW", { icon: "🇵🇼", name: "Palau" }],
["PNG", { icon: "🇵🇬", name: "Papua New Guinea" }],
["POL", { icon: "🇵🇱", name: "Poland" }],
["POR", { icon: "🇵🇹", name: "Portugal" }],
["PRK", { icon: "🇰🇵", name: "North Korea" }],
["PUR", { icon: "🇵🇷", name: "Puerto Rico" }],
["QAT", { icon: "🇶🇦", name: "Qatar" }],
["ROU", { icon: "🇷🇴", name: "Romania" }],
["RSA", { icon: "🇿🇦", name: "South Africa" }],
["RWA", { icon: "🇷🇼", name: "Rwanda" }],
["SAM", { icon: "🇼🇸", name: "Samoa" }],
["SEN", { icon: "🇸🇳", name: "Senegal" }],
["SEY", { icon: "🇸🇨", name: "Seychelles" }],
["SGP", { icon: "🇸🇬", name: "Singapore" }],
["SKN", { icon: "🇰🇳", name: "Saint Kitts and Nevis" }],
["SLE", { icon: "🇸🇱", name: "Sierra Leone" }],
["SLO", { icon: "🇸🇮", name: "Slovenia" }],
["SMR", { icon: "🇸🇲", name: "San Marino" }],
["SOL", { icon: "🇸🇧", name: "Solomon Islands" }],
["SOM", { icon: "🇸🇴", name: "Somalia" }],
["SRB", { icon: "🇷🇸", name: "Serbia" }],
["SRI", { icon: "🇱🇰", name: "Sri Lanka" }],
["SSD", { icon: "🇸🇸", name: "South Sudan" }],
["STP", { icon: "🇸🇹", name: "Sao Tome and Principe" }],
["SUD", { icon: "🇸🇩", name: "Sudan" }],
["SUI", { icon: "🇨🇭", name: "Switzerland" }],
["SUR", { icon: "🇸🇷", name: "Suriname" }],
["SVK", { icon: "🇸🇰", name: "Slovakia" }],
["SWE", { icon: "🇸🇪", name: "Sweden" }],
["SWZ", { icon: "🇸🇿", name: "Eswatini" }],
["SYR", { icon: "🇸🇾", name: "Syria" }],
["TAN", { icon: "🇹🇿", name: "Tanzania" }],
["TGA", { icon: "🇹🇴", name: "Tonga" }],
["THA", { icon: "🇹🇭", name: "Thailand" }],
["TJK", { icon: "🇹🇯", name: "Tajikistan" }],
["TKM", { icon: "🇹🇲", name: "Turkmenistan" }],
["TLS", { icon: "🇹🇱", name: "Timor-Leste" }],
["TOG", { icon: "🇹🇬", name: "Togo" }],
["TPE", { icon: "🇹🇼", name: "Chinese Taipei" }],
["TTO", { icon: "🇹🇹", name: "Trinidad and Tobago" }],
["TUN", { icon: "🇹🇳", name: "Tunisia" }],
["TUR", { icon: "🇹🇷", name: "Türkiye" }],
["TUV", { icon: "🇹🇻", name: "Tuvalu" }],
["UAE", { icon: "🇦🇪", name: "United Arab Emirates" }],
["UGA", { icon: "🇺🇬", name: "Uganda" }],
["UKR", { icon: "🇺🇦", name: "Ukraine" }],
["URU", { icon: "🇺🇾", name: "Uruguay" }],
["USA", { icon: "🇺🇸", name: "United States" }],
["UZB", { icon: "🇺🇿", name: "Uzbekistan" }],
["VAN", { icon: "🇻🇺", name: "Vanuatu" }],
["VEN", { icon: "🇻🇪", name: "Venezuela" }],
["VIE", { icon: "🇻🇳", name: "Vietnam" }],
["VIN", { icon: "🇻🇨", name: "Saint Vincent and the Grenadines" }],
["YEM", { icon: "🇾🇪", name: "Yemen" }],
["ZAM", { icon: "🇿🇲", name: "Zambia" }],
["ZIM", { icon: "🇿🇼", name: "Zimbabwe" }],
]);
/**
* isValidNOC checks if the NOC code is in the NOCS list
* @param {string} noc National Olympic Committee code
* @returns {boolean}
*/
export const isValidNOC = (noc: string) => NOCS.has(noc);
/**
* getNOC returns the NOC name and icon from the NOC code
* @param {string} noc National Olympic Committee code
* @returns {object}
*/
export const getNOC = (noc: string): NOC => {
if (isValidNOC(noc)) {
return NOCS.get(noc)!;
}
throw new Error(`NOC code ${noc} not found`);
};
/**
* getNOCFlag returns the NOC icon from the NOC code
* @param {string} noc National Olympic Committee code
* @returns {string}
*/
export const getNOCFlag = (noc: string) => getNOC(noc).icon;
/**
* getNOCName returns the NOC name from the NOC code
* @param {string} noc National Olympic Committee code
* @returns
*/
export const getNOCName = (noc: string) => getNOC(noc).name;

View File

@ -1,35 +0,0 @@
const SPORTS: Map<string, string> = new Map([
["blind-football", ""],
["boccia", ""],
["goalball", ""],
["para-archery", ""],
["para-athletics", ""],
["para-badminton", ""],
["para-canoe", ""],
["para-cycling-road", ""],
["para-cycling-track", ""],
["para-equestrian", ""],
["para-judo", ""],
["para-powerlifting", ""],
["para-rowing", ""],
["para-swimming", ""],
["para-table-tennis", ""],
["para-taekwondo", ""],
["para-triathlon", ""],
["shooting-para-sport", ""],
["sitting-volleyball", ""],
["wheelchair-basketball", ""],
["wheelchair-fencing", ""],
["wheelchair-rugby", ""],
["wheelchair-tennis", ""],
]);
export const getSportIcon = (sport: string): string => {
if (SPORTS.has(sport)) {
return SPORTS.get(sport)!;
}
console.error(`No icon set for ${sport}`);
return "";
};
export const getAllSportsKeys = (): string[] => [...SPORTS.keys()];

View File

@ -1,125 +0,0 @@
<!DOCTYPE html>
<html data-theme="cmyk">
<head>
<title>Paris 2024 - {{translate_todaysEvents}}</title>
<link href="https://fabrice404.github.io/olympics-calendar/main.css?refresh={{refresh}}" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Paris 2024 - {{translate_todaysEvents}}">
<meta name="keywords" content="Paris 2024 - {{translate_todaysEvents}}">
<meta name="author" content="Fabrice LAMANT">
<meta http-equiv="refresh" content="300">
<script src="https://cdn.jsdelivr.net/npm/luxon@3.4.4/build/global/luxon.min.js"></script>
</head>
<body>
<div class="p-4">
<div class="navbar bg-base-100">
<div class="navbar-start">
<div class="dropdown">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7" />
</svg>
</div>
<ul tabindex="0" class="menu menu-sm dropdown-content bg-base-100 rounded-box z-50 mt-3 w-52 p-2 shadow">
<li><a href="./index.html">{{translate_calendars}}</a></li>
<li><a href="./today.html">{{translate_todaysEvents}}</a></li>
<li><a href="./medals.html">{{translate_medalsTable}}</a></li>
<hr />
<!-- <li><a href="https://fabrice404.github.io/olympics-calendar/today.html">English</a></li>
<li><a href="https://fabrice404.github.io/olympics-calendar/ja/today.html">日本語</a></li>
<li><a href="https://fabrice404.github.io/olympics-calendar/ko/today.html">한국어</a></li>
<li><a href="https://fabrice404.github.io/olympics-calendar/ru/today.html">Русский</a></li>
<li><a href="https://fabrice404.github.io/olympics-calendar/zh/today.html">中文</a></li>
<hr /> -->
<li><a href="https://github.com/fabrice404/olympics-calendar" target="_blank">Source code</a></li>
</ul>
</div>
</div>
<div class="navbar-center">
<a class="btn btn-ghost text-xl" href="./">Paris 2024 - {{translate_todaysEvents}}</a>
</div>
<div class="navbar-end">
</div>
</div>
<div>
{{events}}
</div>
<div class="no-event my-10 text-center text-2xl hidden">
{{translate_noEventToday}}
</div>
<div class="text-sm my-10 text-center">
{{translate_disclaimer}}
</div>
</div>
<script type="text/javascript">
const DateTime = luxon.DateTime;
const now = DateTime.now();
const noc = new URLSearchParams(window.location.search).get('noc');
let color = 0;
const cycleColor = () => {
color++
color = color % 5
switch (color) {
case 0: return "blue";
case 1: return "yellow";
case 2: return "black";
case 3: return "green";
case 4: return "red";
}
};
document.querySelectorAll('.event').forEach((element) => {
const start = DateTime.fromISO(element.getAttribute('data-start'));
const end = DateTime.fromISO(element.getAttribute('data-end'));
const fifteenMinuteAgo = now.minus({ minutes: 15 });
const nocs = element.getAttribute('data-noc').split(",");
if (!noc || nocs.includes(noc)) {
if (now.day === start.day) {
element.querySelector(".time-start").textContent = start.toLocaleString(DateTime.TIME_24_SIMPLE);
element.querySelector(".time-end").textContent = end.toLocaleString(DateTime.TIME_24_SIMPLE);
if (end < now) {
if (end > fifteenMinuteAgo) {
element.classList.add(cycleColor());
element.classList.add('opacity-30');
} else {
element.remove();
}
} else {
element.classList.add(cycleColor());
if (now < start) {
element.classList.add('opacity-30');
}
}
} else {
element.remove();
}
} else {
element.remove();
}
});
document.querySelectorAll(`.competitor:not(.${noc})`).forEach((element) => {
element.remove();
});
if (document.querySelectorAll('.event').length === 0) {
document.querySelector('.no-event').classList.remove('hidden');
}
</script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-0KQC1F1K4H"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', 'G-0KQC1F1K4H');
</script>
</body>

View File

@ -1,120 +0,0 @@
const allSports = new Map<string, string>([
["en", "All sports"],
["ja", "すべてのスポーツ"],
["ko", "모든 스포츠"],
["ru", "Все виды спорта"],
["zh", "所有运动"],
]);
const calendars = new Map<string, string>([
["en", "Calendars"],
["ja", "カレンダー"],
["ko", "달력"],
["ru", "Календари"],
["zh", "日历"],
]);
const closingCeremony = new Map<string, string>([
["en", "Closing Ceremony"],
["ja", "閉会式"],
["ko", "폐막식"],
["ru", "Церемония закрытия"],
["zh", "闭幕式"],
]);
const disclaimer = new Map<string, string>([
["en", "This webiste is not affiliated with the International Olympic Committee. All trademarks, logos and brand names are the property of their respective owners."],
["ja", "このウェブサイトは国際オリンピック委員会とは関係ありません。すべての商標、ロゴ、およびブランド名はそれぞれの所有者の財産です。"],
["ko", "이 웹 사이트는 국제 올림픽 위원회와 관련이 없습니다. 모든 상표, 로고 및 상표는 각 소유자의 소유입니다."],
["ru", "Этот сайт не связан с Международным олимпийским комитетом. Все товарные знаки, логотипы и бренды являются собственностью их соответствующих владельцев."],
["zh", "本网站与国际奥林匹克委员会无关。所有商标、标志和品牌名称均为其各自所有者的财产。"],
]);
const fullSchedule = new Map<string, string>([
["en", "Full schedule"],
["ja", "フルスケジュール"],
["ko", "전체 일정"],
["ru", "Полное расписание"],
["zh", "完整时间表"],
]);
const genderMen = new Map<string, string>([
["en", "M"],
["ja", "男性"],
["ko", "남성"],
["ru", "М"],
["zh", "男"],
]);
const genderWomen = new Map<string, string>([
["en", "W"],
["ja", "女性"],
["ko", "여성"],
["ru", "Ж"],
["zh", "女"],
]);
const medalEvents = new Map<string, string>([
["en", "Medal events"],
["ja", "メダルイベント"],
["ko", "메달 이벤트"],
["ru", "Медальные события"],
["zh", "奖牌赛事"],
]);
const medalsTable = new Map<string, string>([
["en", "Medals table"],
["ja", "メダル表"],
["ko", "메달 테이블"],
["ru", "Таблица медалей"],
["zh", "奖牌榜"],
]);
const noEventToday = new Map<string, string>([
["en", "No event today, come back tomorrow! :)"],
["ja", "今日のイベントはありません。明日また来てください! :)"],
["ko", "오늘 이벤트가 없습니다. 내일 다시 오세요! :)"],
["ru", "Сегодня нет событий, вернитесь завтра! :)"],
["zh", "今天没有活动,请明天再来! :)"],
]);
const openingCeremony = new Map<string, string>([
["en", "Opening Ceremony"],
["ja", "開会式"],
["ko", "개막식"],
["ru", "Церемония открытия"],
["zh", "开幕式"],
]);
const todaysEvents = new Map<string, string>([
["en", "Today's events"],
["ja", "今日のイベント"],
["ko", "오늘의 이벤트"],
["ru", "События сегодня"],
["zh", "今天的活动"],
]);
const medalsTableError = new Map<string, string>([
["en", "Due to a recent update on the official website, the information on this page may no longer be accurate."],
["ja", "公式ウェブサイトの最新情報により、このページの情報が正確でない可能性があります。"],
["ko", "공식 웹 사이트의 최신 정보로 인해이 페이지의 정보가 더 이상 정확하지 않을 수 있습니다."],
["ru", "Из-за недавнего обновления на официальном сайте информация на этой странице может быть недействительной."],
["zh", "由于官方网站的最新更新,此页面上的信息可能不再准确。"],
]);
export const translate = (text: string, language: string) => text
.replace(/\{\{translate_allSports}}/gi, allSports.get(language)!)
.replace(/\{\{translate_calendars}}/gi, calendars.get(language)!)
.replace(/\{\{translate_closingCeremony}}/gi, closingCeremony.get(language)!)
.replace(/\{\{translate_disclaimer}}/gi, disclaimer.get(language)!)
.replace(/\{\{translate_fullSchedule}}/gi, fullSchedule.get(language)!)
.replace(/\{\{translate_genderMen}}/gi, genderMen.get(language)!)
.replace(/\{\{translate_genderWomen}}/gi, genderWomen.get(language)!)
.replace(/\{\{translate_medalEvents}}/gi, medalEvents.get(language)!)
.replace(/\{\{translate_medalsTable}}/gi, medalsTable.get(language)!)
.replace(/\{\{translate_medalsTableError}}/gi, medalsTableError.get(language)!)
.replace(/\{\{translate_noEventToday}}/gi, noEventToday.get(language)!)
.replace(/\{\{translate_openingCeremony}}/gi, openingCeremony.get(language)!)
.replace(/\{\{translate_todaysEvents}}/gi, todaysEvents.get(language)!)
.replace(/\{\{refresh}}/gi, "20240824T2035")
;

41
src/types.d.ts vendored
View File

@ -1,41 +0,0 @@
export interface Competitor {
noc: string;
name: string;
}
export interface Event {
UID: string;
DTSTAMP: string;
DTSTART: string;
DTEND: string;
SUMMARY: string;
DESCRIPTION: string;
LOCATION: string;
_COMPETITORS: Competitor[];
_GENDER: string;
_MEDAL: boolean;
_NOCS: string[];
_SPORT: string;
_UNITNAME: string;
}
export interface Sport {
key: string;
name: string;
NOCS: string[];
}
export interface NOC {
icon: string;
name: string;
}
export interface Medal {
color: "gold" | "silver" | "bronze";
name: string;
noc: string;
sport: string;
unit: string;
date: string;
}

View File

@ -1,15 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./docs/*.html"],
theme: {
extend: {
},
},
plugins: [
require("daisyui"),
],
daisyui: {
themes: ["cmyk"],
},
};

View File

@ -1,65 +0,0 @@
import * as fs from "fs";
import { afterEach, beforeEach, describe } from "node:test";
import { expect, it, vi, MockInstance } from "vitest";
import { generateICS } from "../src/ics";
import { Event } from "../src/types";
describe("ics", () => {
let mkdirMock: MockInstance;
let writeFileMock: MockInstance;
beforeEach(() => {
vi.mock("fs");
});
afterEach(() => {
vi.resetAllMocks();
});
describe("generateICS", () => {
mkdirMock = vi.spyOn(fs, "mkdirSync").mockImplementation(() => "");
writeFileMock = vi.spyOn(fs, "writeFileSync").mockImplementation(() => null);
it("should generate empty ICS file", () => {
generateICS("title", "sport/key", []);
expect(mkdirMock).toHaveBeenCalledWith(
expect.stringMatching("docs/sport"),
{ recursive: true },
);
expect(writeFileMock).toHaveBeenCalledWith(
expect.stringMatching("docs/sport/key.ics"),
expect.any(String),
);
});
it("should generate ICS file with events", () => {
const events: Event[] = [{
UID: "123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890",
DTSTAMP: "20240801T000000Z",
DTSTART: "20240801T000000Z",
DTEND: "20240801T200000Z",
SUMMARY: "Event 1",
DESCRIPTION: "Description that's is very long, longer than 75 characters, to test if it's gonna be split appropriately",
LOCATION: "Location 1",
_COMPETITORS: [],
_GENDER: "",
_MEDAL: false,
_NOCS: [],
_SPORT: "sport",
_UNITNAME: "unit",
}];
generateICS("title", "sport/key", events);
expect(mkdirMock).toHaveBeenCalledWith(
expect.stringMatching("docs/sport"),
{ recursive: true },
);
expect(writeFileMock).toHaveBeenCalledWith(
expect.stringMatching("docs/sport/key.ics"),
expect.any(String),
);
});
});
});

View File

View File

View File

@ -1,17 +0,0 @@
import { describe } from "node:test";
import { expect, it } from "vitest";
import { getSportIcon } from "../src/sports";
describe("sports", () => {
describe("getSportIcon", () => {
it("should return basketball icon", () => {
const icon = getSportIcon("basketball");
expect(icon).toBe("🏀");
});
it("should log an error and return an empty string if sport is not found", () => {
const icon = getSportIcon("ice-hockey");
expect(icon).toBe("");
});
});
});

View File

@ -1,108 +0,0 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dist", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

View File

@ -1,14 +0,0 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
coverage: {
provider: "istanbul",
exclude: [
"**/dist/**",
"**/test/**",
"**/tailwind.config.js",
],
},
},
});