official website update

This commit is contained in:
Fabrice LAMANT
2026-02-04 09:28:16 +01:00
parent d48d904f95
commit b7e4dc2ae4
11 changed files with 469 additions and 359 deletions

View File

@ -5,7 +5,7 @@ export class Cache {
private debug = Debug("olympics-calendar:cache");
private cachePath = (key: string): string => {
return `./cache/${key}.cached`;
return `./cache/${key}.json`;
};
public get(key: string): string | null {

View File

@ -13,6 +13,23 @@ export class ICSGenerator {
this.calendar = calendar;
}
private cleanLine(line: string): string {
if (line.length <= 75) {
return line;
}
const chunks: string[] = [];
let index = 0;
while (index < line.length) {
let chunk = line.slice(index, index + 75);
if (index > 0) {
chunk = " " + chunk.trim();
}
chunks.push(chunk);
index += 75;
}
return chunks.join("\n");
}
private generateICSFile(
sportKey: string | null,
nocKey: string | null,
@ -22,27 +39,28 @@ export class ICSGenerator {
sportKey || "all-sports",
nocKey || "all-nocs",
);
this.calendar.languages.forEach((lang) => {
for (const lang of this.calendar.languages) {
const pathSportKey = sportKey ? sportKey : "all-sports";
const pathNocKey = nocKey ? nocKey : "calendar";
const filepath = `./output/${lang.code.toLowerCase()}/${pathSportKey.toLowerCase()}/${pathNocKey.toLowerCase()}.ics`;
mkdirSync(filepath.split("/").slice(0, -1).join("/"), { recursive: true });
const titleComponents = [];
const titleComponents: string[] = [];
if (nocKey) {
titleComponents.push(
`${this.calendar.nocs.find((n) => n.key === nocKey)!.name[lang.code]}`,
);
}
if (sportKey) {
titleComponents.push(this.calendar.sports.find((s) => s.key === sportKey)!.name[lang.code]);
titleComponents.push(this.calendar.sports.find((s) => s.key === sportKey)!.name[lang.code] || "");
}
titleComponents.push("Milano Cortina 2026");
const title = titleComponents.join(" - ");
const lines = [];
const lines: string[] = [];
lines.push("BEGIN:VCALENDAR");
lines.push("VERSION:2.0");
@ -54,17 +72,11 @@ export class ICSGenerator {
this.calendar.events
.filter((event) => {
if (sportKey && event.sport !== sportKey) return false;
if (nocKey) {
if (event.match) {
const team1Key = event.match.team1.key;
const team2Key = event.match.team2.key;
if (team1Key !== nocKey && team2Key !== nocKey) {
return false;
}
} else {
return false;
}
if ((sportKey && event.sport !== sportKey) || event.sport === "CER") {
return false;
}
if (nocKey && !event.nocs.includes(nocKey)) {
return false;
}
return true;
})
@ -79,27 +91,54 @@ export class ICSGenerator {
const sport = this.calendar.sports.find(
(s) => s.key === event.sport,
)!;
lines.push(`DESCRIPTION:${sport.name[lang.code]} - ${event.name[lang.code] || ""}`);
const summary = `SUMMARY:${event.name[lang.code] || ""}`;
let description = `DESCRIPTION:${sport.name[lang.code]} - ${event.name[lang.code] || ""}`;
let summary = `SUMMARY:${event.name[lang.code] || ""}`;
if (event.match) {
const team1Name = event.match.team1.name[lang.code] || event.match.team1.key;
const team1Flag = getFlag(event.match.team1.key);
const team2Name = event.match.team2.name[lang.code] || event.match.team2.key;
const team2Flag = getFlag(event.match.team2.key);
if (team1Name && team2Name) {
lines.push(`SUMMARY:${team1Flag} ${team1Name} - ${team2Name} ${team2Flag}`);
if (event.competitors?.length === 2) {
const competitor1 = this.getCompetitor(event.competitors[0]!, lang.code);
const competitor2 = this.getCompetitor(event.competitors[1]!, lang.code);
if (competitor1 && competitor2) {
summary = `SUMMARY:${competitor1?.flag} ${competitor1.name} - ${competitor2?.name} ${competitor2.flag}`;
}
} else if (event.competitors?.length > 0) {
const competitors = event.competitors
.map((competitorId) => this.getCompetitor(competitorId, lang.code))
.map((competitor) => `\\n${competitor.flag} ${competitor.name}`).join("");
description += `${competitors}`;
}
lines.push(summary);
lines.push(this.cleanLine(description));
lines.push("END:VEVENT");
});
lines.push("END:VCALENDAR");
writeFileSync(filepath, lines.join("\n"));
});
if (lines.length <= 10) {
this.debug("Skipping empty ICS file:", filepath);
} else {
writeFileSync(filepath, lines.join("\n"));
}
}
}
private getCompetitor(competitorId: string, lang: string) {
if (competitorId.startsWith("team:")) {
const team = this.calendar.nocs.find(noc => noc.key === competitorId.replace("team:", ""));
return {
noc: team?.key,
name: team?.name[lang] || team?.key || competitorId,
flag: getFlag(team?.key || ""),
};
}
const competitor = this.calendar.competitors.find(comp => comp.code === competitorId)!;
return {
noc: competitor.noc,
name: competitor.name,
flag: getFlag(competitor.noc),
};
}
public generate(): void {

View File

@ -1,11 +1,18 @@
import { removeSync } from "fs-extra/esm";
import nodeCron from "node-cron";
import { Scraper } from "./scraper";
const main = async () => {
nodeCron.schedule("* * * * *", async () => {
const main = () => {
nodeCron.schedule("*/1 * * * *", () => {
removeSync("./cache/schedules");
const scraper = new Scraper();
await scraper.scrape();
scraper.scrape();
});
nodeCron.schedule("0 0 * * *", () => {
removeSync("./cache/disciplinesevents");
removeSync("./cache/nocs");
});
};

View File

@ -10,6 +10,7 @@
"dependencies": {
"axios": "^1.13.4",
"debug": "^4.4.3",
"fs-extra": "^11.3.3",
"node-cron": "^4.2.1",
"nodemon": "^3.1.11",
"ts-node": "^10.9.2"
@ -1268,6 +1269,20 @@
"node": ">= 6"
}
},
"node_modules/fs-extra": {
"version": "11.3.3",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
"integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=14.14"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -1366,6 +1381,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@ -1541,6 +1562,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -2135,6 +2168,15 @@
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"license": "MIT"
},
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",

View File

@ -13,6 +13,7 @@
"dependencies": {
"axios": "^1.13.4",
"debug": "^4.4.3",
"fs-extra": "^11.3.3",
"node-cron": "^4.2.1",
"nodemon": "^3.1.11",
"ts-node": "^10.9.2"

View File

@ -1,129 +1,123 @@
import { get } from "axios";
import Debug from "debug";
import { writeFileSync } from "fs";
import { Cache } from "./cache";
import { ICSGenerator } from "./ics-generator";
import { Calendar, Event, Language, PageData, Sport, Team } from "./types";
import { Calendar, Event, Language, Sport, NOC, Competitor } from "./types";
const BASE_URL = "https://www.olympics.com";
const BASE_SCHEDULE_PATH = "milano-cortina-2026/schedule/overview";
export class Scraper {
private cache = new Cache();
private debug = Debug("olympics-calendar:scraper");
private readonly cache = new Cache();
private events: Event[] = [];
private languages: Language[] = [];
private nocs: Team[] = [];
private sports: Sport[] = [];
private readonly competitors: Competitor[] = [];
private async getPageData(path: string): Promise<PageData> {
this.debug(`getPageData: path=${path}`);
if (!this.cache.has(path)) {
const url = `${BASE_URL}${path}`;
this.debug(url);
const response = await get(url, {
headers: {
private readonly debug = Debug("olympics-calendar:scraper");
private readonly events: Event[] = [];
private readonly languages: Language[] = [
{ code: "en", name: "English", code3: "ENG" },
{ code: "it", name: "Italiano", code3: "ITA" },
{ code: "fr", name: "Français", code3: "FRA" },
{ code: "de", name: "Deutsch", code3: "DEU" },
{ code: "pt", name: "Português", code3: "POR" },
{ code: "es", name: "Español", code3: "SPA" },
{ code: "ja", name: "日本語", code3: "JPN" },
{ code: "zh", name: "中文", code3: "CHI" },
{ code: "hi", name: "हिन्दी", code3: "HIN" },
{ code: "ko", name: "한국어", code3: "KOR" },
{ code: "ru", name: "Русский", code3: "RUS" },
];
private readonly nocs: NOC[] = [];
private readonly sports: Sport[] = [];
private async getJSONData(url: string, cacheKey: string): Promise<any> {
this.debug(`getJSONData: url=${url}`);
if (!this.cache.has(cacheKey)) {
const response = await fetch(url, {
"headers": {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36"
"accept-language": "en-US,en;q=0.9,fr-FR;q=0.8,fr;q=0.7",
"cache-control": "max-age=0",
"priority": "u=0, i",
"sec-ch-ua": "\"Not(A:Brand\";v=\"8\", \"Chromium\";v=\"144\", \"Google Chrome\";v=\"144\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"macOS\"",
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "none",
"sec-fetch-user": "?1",
"upgrade-insecure-requests": "1",
},
"body": null,
"method": "GET"
});
const page = await response.data;
const dataMatch = page.match(
/<script id="__NEXT_DATA__" type="application\/json">([\s\S]*?)<\/script>/,
);
if (!dataMatch) {
throw new Error(
`Could not find __NEXT_DATA__ script tag for URL: ${url}`,
);
}
const data = dataMatch[1];
if (data) {
this.cache.set(path, JSON.stringify(JSON.parse(data), null, 2));
}
const result = await response.json();
this.cache.set(cacheKey, JSON.stringify(result, null, 2));
}
return JSON.parse(this.cache.get(path)!);
return JSON.parse(this.cache.get(cacheKey)!);
}
private saveCalendar(): void {
this.debug("saveCalendar");
const calendar = this.getCalendar();
writeFileSync("./output/calendar.json", JSON.stringify(calendar));
writeFileSync("./output/calendar.json", JSON.stringify(calendar, null, 2));
}
private async scrapeEvents(): Promise<void> {
this.debug("scrapeEvents");
for (const sport of this.sports) {
for (const lang of this.languages) {
const data = await this.getPageData(
`/${lang.code}/milano-cortina-2026/schedule/${sport.key}`,
);
const scheduleList = data.props.pageProps.page.items
.find(
(item) => item.type === "module" && item.name === "scheduleList",
)!
.data.schedules.map((schedule) => schedule.units)
.flat();
for (const scheduleElement of scheduleList) {
if (
this.events.find((e) => e.key === scheduleElement.unitCode) == null
) {
for (const lang of this.languages) {
this.debug(`Scraping events: ${lang.code}`);
for (let i = 3; i <= 23; i++) {
const url = `https://www.olympics.com/wmr-owg2026/schedules/api/${lang.code3}/schedule/lite/day/2026-02-${i.toString().padStart(2, "0")}`;
const data = await this.getJSONData(url, `schedules/day/2026-02-${i.toString().padStart(2, "0")}/${lang.code3}`);
for (const event of data.units) {
const { id: key } = event;
if (!this.events.some((e) => e.key === key)) {
this.events.push({
key: scheduleElement.unitCode,
sport: sport.key,
start: scheduleElement.startDateTimeUtc,
end: scheduleElement.endDateTimeUtc,
isTraining: scheduleElement.isTraining,
medal: scheduleElement.medal,
key,
sport: event.disciplineCode,
start: event.startDate,
end: event.endDate,
medal: event.medalFlag.toString(),
name: {},
location: {},
nocs: [],
competitors: [],
});
}
const event = this.events.find(
(e) => e.key === scheduleElement.unitCode,
)!;
event.name[lang.code] = scheduleElement.description;
event.location[lang.code] = scheduleElement.venue?.description || "";
if (scheduleElement.match) {
if (event.match == null) {
event.match = {
team1: {
key: scheduleElement.match.team1.teamCode.replace(
/[^A-Z]/gi,
"",
),
name: {},
},
team2: {
key: scheduleElement.match.team2.teamCode.replace(
/[^A-Z]/gi,
"",
),
name: {},
},
};
}
event.match.team1.name[lang.code] = (
scheduleElement.match.team1.description || ""
).replace(/,/gi, "");
event.match.team2.name[lang.code] = (
scheduleElement.match.team2.description || ""
).replace(/,/gi, "");
const calendarEvent = this.events.find((e) => e.key === key)!;
calendarEvent.name[lang.code] = event.eventUnitName;
calendarEvent.location[lang.code] = event.venueDescription;
for (const team of [
scheduleElement.match.team1,
scheduleElement.match.team2,
]) {
const nocKey = team.teamCode.replace(/[^A-Z]/gi, "");
if (this.nocs.find((n) => n.key === nocKey) == null) {
this.nocs.push({ key: nocKey, name: {} });
if (event.competitors) {
for (const competitor of event.competitors) {
const { code, name, noc, competitorType } = competitor;
if (!calendarEvent.nocs.some((n) => n === noc)) {
calendarEvent.nocs.push(noc);
}
if (competitorType) {
if (!calendarEvent.competitors.some((c) => c === code)) {
calendarEvent.competitors.push(code);
}
this.setCompetitor(code, noc, name);
this.setNoc(noc, "", lang.code);
} else {
const key = `team:${noc}`;
if (!calendarEvent.competitors.some((c) => c === key)) {
calendarEvent.competitors.push(key);
}
this.setNoc(noc, name, lang.code);
}
const noc = this.nocs.find((n) => n.key === nocKey)!;
noc.name[lang.code] = (team.description || "").replace(/,/gi, "");
}
}
}
@ -131,47 +125,59 @@ export class Scraper {
}
}
private async scrapeLanguages(): Promise<void> {
this.debug("scrapeLanguages");
const pageData = await this.getPageData(`/en/${BASE_SCHEDULE_PATH}`);
const languagesData =
pageData.props.pageProps.page.template.properties.header.mainNav
.languages;
private async scrapeNOCs(): Promise<void> {
this.debug("scrapeNOCs");
for (const lang of this.languages) {
this.debug(`Scraping NOCs: ${lang.code}`);
const url = `https://www.olympics.com/wmr-owg2026/info/api/${lang.code3}/nocs`;
const data = await this.getJSONData(url, `nocs/${lang.code3}`);
this.languages = languagesData
.filter((lang) =>
lang.link.match(/\/milano-cortina-2026\/schedule\/overview$/),
)
.map((lang) => ({
code: lang.lang,
name: lang.label,
}));
for (const noc of this.nocs) {
const found = data.nocs.find((n) => n.id === noc.key);
this.setNoc(found.id, found.name, lang.code);
}
}
}
private async scrapeSports(): Promise<void> {
this.debug("scrapeSports");
for (const lang of this.languages) {
this.debug(`Scraping language: ${lang.code}`);
const pageData = await this.getPageData(
`/${lang.code}/${BASE_SCHEDULE_PATH}`,
);
this.debug(`Scraping sports: ${lang.code}`);
const disciplines = pageData.props.pageProps.page.items.find(
(item) => item.type === "module" && item.name === "scheduleGrid",
)!.data.disciplines;
for (const discipline of disciplines.filter(
(d) => d.disciplineCode.toLowerCase() !== "cer",
)) {
const key = discipline.disciplineCode.toLowerCase();
if (this.sports.find((s) => s.key === key) == null) {
this.sports.push({ key, name: {}, order: -1 });
const url = `https://www.olympics.com/wmr-owg2026/info/api/${lang.code3}/disciplinesevents`;
const data = await this.getJSONData(url, `disciplinesevents/${lang.code3}`);
for (const discipline of data.disciplines) {
const { id, name } = discipline;
if (!this.sports.some((s) => s.key === id)) {
this.sports.push({ key: id, name: {}, order: 0 });
}
const sport = this.sports.find((s) => s.key === key)!;
sport.name[lang.code] = discipline.description;
sport.order = discipline.order;
const sport = this.sports.find((s) => s.key === id)!;
sport.name[lang.code] = name;
}
}
this.sports
.toSorted((a, b) => (a.order < b.order ? -1 : 1))
.forEach((sport, index) => {
sport.order = index + 1;
});
}
private setCompetitor(code: string, noc: string, name: string): void {
if (!this.competitors.some((c) => c.code === code)) {
this.competitors.push({ code, noc, name });
}
}
private setNoc(key: string, name: string, langCode: string): void {
if (!key)
return;
if (!this.nocs.some((n) => n.key === key)) {
this.nocs.push({ key, name: {} });
}
const noc = this.nocs.find((n) => n.key === key)!;
noc.name[langCode] = name;
}
public getCalendar(): Calendar {
@ -179,15 +185,17 @@ export class Scraper {
languages: this.languages,
sports: this.sports,
nocs: this.nocs,
competitors: this.competitors,
events: this.events,
};
}
public async scrape(): Promise<void> {
this.debug("scrape");
await this.scrapeLanguages();
await this.scrapeSports();
await this.scrapeEvents();
await this.scrapeNOCs();
this.saveCalendar();
new ICSGenerator(this.getCalendar()).generate();

View File

@ -22,6 +22,7 @@
"exactOptionalPropertyTypes": true,
// Style Options
"noImplicitAny": false,
// "noImplicitReturns": true,
// "noImplicitOverride": true,
// "noUnusedLocals": true,
@ -36,6 +37,6 @@
"isolatedModules": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"skipLibCheck": true,
"skipLibCheck": true
}
}

73
scraper/types.d.ts vendored
View File

@ -5,6 +5,7 @@ export interface MultilingualString {
export interface Language {
code: string;
name: string;
code3: string;
}
export interface Sport {
@ -13,14 +14,15 @@ export interface Sport {
order: number;
}
export interface Team {
export interface NOC {
key: string;
name: MultilingualString;
}
export interface Match {
team1: Team;
team2: Team;
export interface Competitor {
noc: string;
code: string;
name: string;
}
export interface Event {
@ -28,72 +30,17 @@ export interface Event {
start: string;
end: string;
sport: string;
isTraining: boolean;
medal: "0" | "1" | "3";
name: MultilingualString;
location: MultilingualString;
match?: Match;
nocs: string[];
competitors: string[];
}
export interface Calendar {
languages: Language[];
sports: Sport[];
nocs: NOC[];
competitors: Competitor[];
events: Event[];
nocs: Team[];
}
export interface PageData {
props: {
pageProps: {
page: {
template: {
properties: {
header: {
mainNav: {
languages: {
link: string;
lang: string;
label: string;
}[];
};
};
};
};
items: {
type: string;
name: string;
data: {
disciplines: {
disciplineCode: string;
order: number;
description: string;
}[];
schedules: {
units: {
unitCode: string;
startDateTimeUtc: string;
endDateTimeUtc: string;
isTraining: boolean;
medal: "0" | "1" | "3";
description: string;
venue: {
description: string;
};
match?: {
team1: {
teamCode: string;
description: string;
};
team2: {
teamCode: string;
description: string;
};
};
}[];
}[];
};
}[];
};
};
};
}

View File

@ -262,6 +262,7 @@ export default function Flag({ iso3, name }: { iso3: string; name: string }) {
return <img
src={`https://gstatic.olympics.com/s3/noc/oly/3x2/${iso3.toUpperCase()}.png`}
height="24"
alt={`${iso3} - ${iso2}`}
alt={name}
title={name}
className="inline-block mx-2 h-5 border-1 border-gray-300" />
}

View File

@ -3,50 +3,55 @@
import { loadSchedule } from "../lib/data";
import { useEffect, useState } from "react";
import Flag from "./flag";
import { COPY, COPY_SUCCESS, FILTER_BY_COUNTRY, FILTER_BY_SPORT, MADE_BY_FABRICE, NOT_AFFILIATED } from "../lib/text";
import { COPY, COPY_SUCCESS, FILTER_BY_COUNTRY, FILTER_BY_SPORT, MADE_BY_FABRICE, NOT_AFFILIATED, NO_EVENT_FOR_FILTERS } from "../lib/text";
import useLocalStorage from "@/lib/local-storage";
import { GoogleAnalytics } from "@next/third-parties/google";
interface MultilingualString {
export interface MultilingualString {
[key: string]: string;
}
interface Language {
export interface Language {
code: string;
name: string;
code3: string;
}
export interface Sport {
key: string;
name: MultilingualString;
order: number;
}
export interface NOC {
key: string;
name: MultilingualString;
}
export interface Competitor {
noc: string;
code: string;
name: string;
}
interface Sport {
key: string;
name: MultilingualString
}
interface Team {
key: string;
name: MultilingualString;
}
interface Match {
team1: Team;
team2: Team;
}
interface Event {
export interface Event {
key: string;
start: string;
end: string;
sport: string;
isTraining: boolean;
medal: '0' | '1' | '3';
medal: "0" | "1" | "3";
name: MultilingualString;
match?: Match;
location: MultilingualString;
nocs: string[];
competitors: string[];
}
interface Calendar {
export interface Calendar {
languages: Language[];
sports: Sport[];
nocs: NOC[];
competitors: Competitor[];
events: Event[];
nocs: Team[];
}
const COLORS = ['azzurro', 'giallo', 'rosa', 'rosso', 'verde', 'viola'];
@ -123,15 +128,12 @@ export default function Home() {
const noc = qs.get('noc');
if (noc) {
if (event.match) {
if (event.match.team1.key !== noc && event.match.team2.key !== noc) {
visible = false;
}
} else {
if (!event.nocs.includes(noc)) {
visible = false;
}
}
return visible;
}
@ -143,7 +145,6 @@ export default function Home() {
button.classList.add('text-success');
button.classList.add('font-bold');
setTimeout(() => {
// document.getElementById('copy_toast')?.classList.remove('toast-open');
button.textContent = translate(COPY);
button.classList.remove('text-success');
button.classList.remove('font-bold');
@ -159,6 +160,165 @@ export default function Home() {
if (data.languages.find(lang => lang.code === language) === undefined) {
setLanguage('en')
}
const events = data.events.filter(event => filter(event));
let main = (
<div>
<div className="text-center pt-10 mb-100">
{translate(NO_EVENT_FOR_FILTERS)}
</div>
</div>
)
if (events.length) {
main = (
<div>
<div className="text-center pt-6">
<span className="input w-1/3">
<input type="text" placeholder={calendarLink} readOnly={true} />
<button id="copy_button" className="label cursor-pointer" onClick={() => copyToClipboard(calendarLink)}>{translate(COPY)}</button>
</span>
<a className="inline-block" href={calendarLink.replace("https://", "webcal://")} target="_blank">
<img src="/img/icon-apple.svg" alt="Apple Calendar" className="inline-block size-6 ml-4 mr-2" />
</a>
<a className="inline-block" href={`https://calendar.google.com/calendar/u/0/r?cid=${encodeURIComponent(calendarLink)}`} target="_blank">
<img src="/img/icon-google.svg" alt="Google Calendar" className="inline-block size-5 ml-4 mr-2" />
</a>
<a className="inline-block" href={`https://outlook.office.com/calendar/0/deeplink/subscribe?url=${encodeURIComponent(calendarLink)}`} target="_blank">
<img src="/img/icon-office365.svg" alt="Office 365 Calendar" className="inline-block size-5 ml-4 mr-2" />
</a>
<a className="inline-block" href={`https://outlook.live.com/calendar/0/deeplink/subscribe?url=${encodeURIComponent(calendarLink)}`} target="_blank">
<img src="/img/icon-outlookcom.svg" alt="Outlook Calendar" className="inline-block size-5 ml-4 mr-2" />
</a>
<a className="inline-block" href={`https://calendar.yahoo.com/?ics=${encodeURIComponent(calendarLink)}`} target="_blank">
<img src="/img/icon-yahoo.svg" alt="Yahoo Calendar" className="inline-block size-5 ml-4 mr-2" />
</a>
</div>
{
events
.sort((a, b) => a.start.localeCompare(b.start))
.map((event, i) => {
const startDate = new Date(event.start);
const endDate = new Date(event.end);
const startHours = startDate.getHours().toString().padStart(2, '0');
const startMinutes = startDate.getMinutes().toString().padStart(2, '0');
const endHours = endDate.getHours().toString().padStart(2, '0');
const endMinutes = endDate.getMinutes().toString().padStart(2, '0');
let titleColor = "fg-main";
if (event.medal === '1') {
titleColor = "bg-gold";
} else if (event.medal === '3') {
titleColor = "bg-bronze";
}
const day = event.start.split('T')[0];
let dayHeader = <></>;
if (lastDay !== day) {
dayHeader = (
<div className="day-header text-center my-8">
<h2 className="text-3xl font-light fg-main">
{new Date(day).toLocaleDateString(language, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
</h2>
</div>
);
}
// eslint-disable-next-line react-hooks/immutability
lastDay = day;
const getCompetitor = (competitorId: string) => {
if (competitorId.startsWith("team:")) {
const team = data.nocs.find(noc => noc.key === competitorId.replace("team:", ""));
return { noc: team?.key, name: translate(team?.name || {}) };
}
const competitor = data.competitors.find(comp => comp.code === competitorId);
return competitor ? { noc: competitor.noc, name: competitor.name } : null;
};
let competitors = <></>;
if (event.competitors.length > 0) {
if (event.competitors.length === 2) {
const competitor1 = getCompetitor(event.competitors[0]);
const competitor2 = getCompetitor(event.competitors[1]);
competitors = (
<div className="competitors min-w-md max-w-md px-2 font-light">
<div className="w-1/3 inline-block">
{competitor1?.name}
</div>
<div className="w-1/9 inline-block">
<Flag iso3={competitor1?.noc} name={competitor1?.name} />
</div>
<div className="w-1/9 inline-block text-center">-</div>
<div className="w-1/9 inline-block text-right">
<Flag iso3={competitor2?.noc} name={competitor2?.name} />
</div>
<div className="w-1/3 inline-block text-right">
{competitor2?.name}
</div>
</div>
)
} else {
competitors = (
<ul>
{
event.competitors
.map((competitorId) => {
const competitor = getCompetitor(competitorId);
if (!competitor) return null;
const noc = data.nocs.find(noc => noc.key === competitor.noc);
if (qs.get('noc') === competitor.noc) {
return (
<li key={competitorId}>
<Flag iso3={competitor.noc} name={translate(noc!.name)} /> {competitor.name}
</li>
);
}
})
}
</ul>
);
}
}
return (
<div key={event.key}>
{dayHeader}
<div className="py-4 mx-auto my-4 bg-white w-3/4 rounded-lg">
<div className={`fg-${getColor(i)} w-1/4 align-top text-right inline-block text-5xl tabular-nums pr-2 border-r border-slate-900/10`}>
<span className="time-start">{startHours}:{startMinutes}</span>
<div className="time-end text-xs">{endHours}:{endMinutes}</div>
</div>
<div className="w-3/5 align-top inline-block text-black pl-2">
<div className="px-2">
{translate(data.sports.find(sport => sport.key === event.sport)?.name || {}).toUpperCase()}
</div>
<div className={`font-bold inline-block px-2 ${titleColor}`}>{translate(event.name)}</div>
{competitors}
</div>
</div>
</div>
)
})
}
</div>
)
}
return (
<div>
<div className="navbar bg-main">
@ -241,115 +401,7 @@ export default function Home() {
</ul>
</div>
</div>
<div>
<div className="text-center pt-6">
<span className="input w-1/3">
<input type="text" placeholder={calendarLink} readOnly={true} />
<button id="copy_button" className="label cursor-pointer" onClick={() => copyToClipboard(calendarLink)}>{translate(COPY)}</button>
</span>
<a className="inline-block" href={calendarLink.replace("https://", "webcal://")} target="_blank">
<img src="/img/icon-apple.svg" alt="Apple Calendar" className="inline-block size-6 ml-4 mr-2" />
</a>
<a className="inline-block" href={`https://calendar.google.com/calendar/u/0/r?cid=${encodeURIComponent(calendarLink)}`} target="_blank">
<img src="/img/icon-google.svg" alt="Google Calendar" className="inline-block size-5 ml-4 mr-2" />
</a>
<a className="inline-block" href={`https://outlook.office.com/calendar/0/deeplink/subscribe?url=${encodeURIComponent(calendarLink)}`} target="_blank">
<img src="/img/icon-office365.svg" alt="Office 365 Calendar" className="inline-block size-5 ml-4 mr-2" />
</a>
<a className="inline-block" href={`https://outlook.live.com/calendar/0/deeplink/subscribe?url=${encodeURIComponent(calendarLink)}`} target="_blank">
<img src="/img/icon-outlookcom.svg" alt="Outlook Calendar" className="inline-block size-5 ml-4 mr-2" />
</a>
<a className="inline-block" href={`https://calendar.yahoo.com/?ics=${encodeURIComponent(calendarLink)}`} target="_blank">
<img src="/img/icon-yahoo.svg" alt="Yahoo Calendar" className="inline-block size-5 ml-4 mr-2" />
</a>
</div>
{
data.events
.filter(event => filter(event))
.sort((a, b) => a.start.localeCompare(b.start))
.map((event, i) => {
const startDate = new Date(event.start);
const endDate = new Date(event.end);
const startHours = startDate.getHours().toString().padStart(2, '0');
const startMinutes = startDate.getMinutes().toString().padStart(2, '0');
const endHours = endDate.getHours().toString().padStart(2, '0');
const endMinutes = endDate.getMinutes().toString().padStart(2, '0');
const participants = [];
let titleColor = "fg-main";
if (event.medal === '1') {
titleColor = "bg-gold";
} else if (event.medal === '3') {
titleColor = "bg-bronze";
}
if (event.match) {
participants.push(event.match.team1.key);
participants.push(event.match.team2.key);
}
const day = event.start.split('T')[0];
let dayHeader = <></>;
if (lastDay !== day) {
dayHeader = (
<div className="day-header text-center my-8">
<h2 className="text-3xl font-light fg-main">
{new Date(day).toLocaleDateString(language, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
</h2>
</div>
);
}
lastDay = day;
return (
<div key={event.key}>
{dayHeader}
<div className="py-4 mx-auto my-4 bg-white w-3/4 rounded-lg">
<div className={`fg-${getColor(i)} w-1/4 align-top text-right inline-block text-5xl tabular-nums pr-2 border-r border-slate-900/10`}>
<span className="time-start">{startHours}:{startMinutes}</span>
<div className="time-end text-xs">{endHours}:{endMinutes}</div>
</div>
<div className="w-3/5 align-top inline-block text-black pl-2">
<div className="px-2">
{translate(data.sports.find(sport => sport.key === event.sport)?.name || {}).toUpperCase()}
</div>
<div className={`font-bold inline-block px-2 ${titleColor}`}>{translate(event.name)}</div>
{event.match?.team1?.key && event.match?.team2.key && (
<div className="competitors min-w-md max-w-md px-2 font-light">
<div className="w-1/3 inline-block">
{translate(event.match.team1.name)}
</div>
<div className="w-1/9 inline-block">
<Flag iso3={event.match.team1.key} name={translate(event.match.team1.name)} />
</div>
<div className="w-1/9 inline-block text-center">-</div>
<div className="w-1/9 inline-block text-right">
<Flag iso3={event.match.team2.key} name={translate(event.match.team2.name)} />
</div>
<div className="w-1/3 inline-block text-right">
{translate(event.match.team2.name)}
</div>
</div>
)}
</div>
</div>
</div>
)
})
}
</div>
{main}
<footer className="footer footer-horizontal footer-center bg-gray-800 text-primary-content p-10">
<aside>
<p className="font-bold">

View File

@ -82,3 +82,15 @@ export const NOT_AFFILIATED = {
ja: "このウェブサイトは国際オリンピック委員会とは提携していません。すべての商標、ロゴ、ブランド名はそれぞれの所有者の財産です。",
ru: "Этот веб-сайт не аффилирован с Международным олимпийским комитетом. Все торговые марки, логотипы и названия брендов являются собственностью их соответствующих владельцев."
}
export const NO_EVENT_FOR_FILTERS = {
en: "There's no event for the selected team and/or sport, please adjust your filters.",
fr: "Il n'y a pas d'événement pour l'équipe et/ou le sport sélectionné, veuillez ajuster vos filtres.",
es: "No hay ningún evento para el equipo y/o deporte seleccionado, por favor ajuste sus filtros.",
de: "Es gibt kein Ereignis für das ausgewählte Team und/oder den ausgewählten Sport, bitte passen Sie Ihre Filter an.",
it: "Non ci sono eventi per la squadra e/o lo sport selezionato, si prega di regolare i filtri.",
pt: "Não há evento para a equipe e/ou esporte selecionado, por favor ajuste seus filtros.",
zh: "所选团队和/或运动没有事件,请调整您的过滤器。",
ja: "選択したチームおよび/またはスポーツのイベントはありません。フィルターを調整してください。",
ru: "Для выбранной команды и/или вида спорта нет событий, пожалуйста, отрегулируйте ваши фильтры."
}