mirror of
https://github.com/fabrice404/olympics-calendar.git
synced 2025-12-13 06:39:47 +00:00
wip milano-cortina 2026
This commit is contained in:
46
README.md
46
README.md
@ -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.
|
||||
|
||||
@ -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"],
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -1,9 +0,0 @@
|
||||
{
|
||||
"verbose": false,
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
"cache/**",
|
||||
"docs/**"
|
||||
],
|
||||
"ext": "ts,json,html,css"
|
||||
}
|
||||
5201
package-lock.json
generated
5201
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
33
package.json
33
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
548
src/calendar.ts
548
src/calendar.ts
@ -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">●</span>`);
|
||||
content.push(` <span class="inline-block text-center w-1/6 flex-none silver">●</span>`);
|
||||
content.push(` <span class="inline-block text-center w-1/6 flex-none bronze">●</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}">●</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);
|
||||
});
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
60
src/ics.ts
60
src/ics.ts
@ -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"));
|
||||
};
|
||||
|
||||
11
src/index.ts
11
src/index.ts
@ -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();
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
11
src/io.ts
11
src/io.ts
@ -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");
|
||||
@ -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>
|
||||
243
src/nocs.ts
243
src/nocs.ts
@ -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;
|
||||
@ -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()];
|
||||
@ -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>
|
||||
120
src/translate.ts
120
src/translate.ts
@ -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
41
src/types.d.ts
vendored
@ -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;
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./docs/*.html"],
|
||||
theme: {
|
||||
extend: {
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require("daisyui"),
|
||||
],
|
||||
daisyui: {
|
||||
themes: ["cmyk"],
|
||||
},
|
||||
};
|
||||
|
||||
@ -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),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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("");
|
||||
});
|
||||
});
|
||||
});
|
||||
108
tsconfig.json
108
tsconfig.json
@ -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. */
|
||||
}
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
coverage: {
|
||||
provider: "istanbul",
|
||||
exclude: [
|
||||
"**/dist/**",
|
||||
"**/test/**",
|
||||
"**/tailwind.config.js",
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user