diff --git a/scraper/ics-generator.ts b/scraper/ics-generator.ts index d51bf21d4..a8c2c7497 100644 --- a/scraper/ics-generator.ts +++ b/scraper/ics-generator.ts @@ -40,85 +40,103 @@ export class ICSGenerator { nocKey || "all-nocs", ); - for (const lang of this.calendar.languages) { - const pathSportKey = sportKey ? sportKey : "all-sports"; - const pathNocKey = nocKey ? nocKey : "calendar"; + let types = ["all-events", "medal-events", "gold-medal-events"]; + if (nocKey) { + types = ["all-events"]; + } - const filepath = `./output/${lang.code.toLowerCase()}/${pathSportKey.toLowerCase()}/${pathNocKey.toLowerCase()}.ics`; - mkdirSync(filepath.split("/").slice(0, -1).join("/"), { recursive: true }); + for (const type of types) { + for (const lang of this.calendar.languages) { + const pathSportKey = sportKey || "all-sports"; + let pathCalendar = "calendar"; + if (type != "all-events") { + pathCalendar = type; + } else if (nocKey) { + pathCalendar = nocKey; + } - const titleComponents: string[] = []; - if (nocKey) { - titleComponents.push( - `${this.calendar.nocs.find((n) => n.key === nocKey)!.name[lang.code]}`, + const filepath = `./output/${lang.code.toLowerCase()}/${pathSportKey.toLowerCase()}/${pathCalendar.toLowerCase()}.ics`; + mkdirSync(filepath.split("/").slice(0, -1).join("/"), { recursive: true }); + + 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("Milano Cortina 2026"); + + const title = titleComponents.join(" - "); + + const lines: string[] = []; + + lines.push("BEGIN:VCALENDAR"); + lines.push("VERSION:2.0"); + lines.push( + `PRODID:-//fabrice404//olympics-calendar//${lang.code}/${pathSportKey}/${pathCalendar}`, ); - } - if (sportKey) { - titleComponents.push(this.calendar.sports.find((s) => s.key === sportKey)!.name[lang.code] || ""); - } - titleComponents.push("Milano Cortina 2026"); + lines.push(`X-WR-CALNAME:${title}`); + lines.push(`NAME:${title}`); - const title = titleComponents.join(" - "); - - const lines: string[] = []; - - lines.push("BEGIN:VCALENDAR"); - lines.push("VERSION:2.0"); - lines.push( - `PRODID:-//fabrice404//olympics-calendar//${lang.code}/${pathSportKey}/${pathNocKey}`, - ); - lines.push(`X-WR-CALNAME:${title}`); - lines.push(`NAME:${title}`); - - this.calendar.events - .filter((event) => { - if ((sportKey && event.sport !== sportKey) || event.sport === "CER") { - return false; - } - if (nocKey && !event.nocs.includes(nocKey)) { - return false; - } - return true; - }) - .forEach((event) => { - lines.push("BEGIN:VEVENT"); - lines.push(`UID:${event.key.replace(/--/g, "-")}`); - lines.push(`DTSTAMP:${event.start.replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z")}`); - lines.push(`DTSTART:${event.start.replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z")}`); - lines.push(`DTEND:${event.end.replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z")}`); - lines.push(`LOCATION:${event.location[lang.code] || ""}`); - - const sport = this.calendar.sports.find( - (s) => s.key === event.sport, - )!; - let description = `DESCRIPTION:${sport.name[lang.code]} - ${event.name[lang.code] || ""}`; - let summary = `SUMMARY:${event.name[lang.code] || ""}`; - - 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}`; + this.calendar.events + .filter((event) => { + if ((sportKey && event.sport !== sportKey) || event.sport === "CER") { + return false; } - } 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}`; - } + if (nocKey && !event.nocs.includes(nocKey)) { + return false; + } + if ( + (type === "medal-events" && event.medal === "0") || + (type === "gold-medal-events" && event.medal !== "1") + ) { + return false; + } + return true; + }) + .forEach((event) => { + lines.push("BEGIN:VEVENT"); + lines.push(`UID:${event.key.replace(/--/g, "-")}`); + lines.push(`DTSTAMP:${event.start.replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z")}`); + lines.push(`DTSTART:${event.start.replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z")}`); + lines.push(`DTEND:${event.end.replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z")}`); + lines.push(`LOCATION:${event.location[lang.code] || ""}`); - lines.push(summary); - lines.push(this.cleanLine(description)); - lines.push("END:VEVENT"); - }); + const sport = this.calendar.sports.find( + (s) => s.key === event.sport, + )!; + let description = `DESCRIPTION:${sport.name[lang.code]} - ${event.name[lang.code] || ""}`; + let summary = `SUMMARY:${event.name[lang.code] || ""}`; - lines.push("END:VCALENDAR"); + if (event.competitors?.length === 2) { + const competitor1 = this.getCompetitor(event.competitors[0]!, lang.code); + const competitor2 = this.getCompetitor(event.competitors[1]!, lang.code); - if (lines.length <= 10) { - this.debug("Skipping empty ICS file:", filepath); - } else { - writeFileSync(filepath, lines.join("\n"), "utf-8"); + 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"); + + if (lines.length <= 10) { + this.debug("Skipping empty ICS file:", filepath); + } else { + writeFileSync(filepath, lines.join("\n"), "utf-8"); + } } } } @@ -142,6 +160,7 @@ export class ICSGenerator { public generate(): void { this.debug("generate"); + this.generateICSFile(null, null); this.calendar.sports.forEach((sport) => { diff --git a/ui/app/[[...slug]]/page.tsx b/ui/app/[[...slug]]/page.tsx index 4c553a66b..903139611 100644 --- a/ui/app/[[...slug]]/page.tsx +++ b/ui/app/[[...slug]]/page.tsx @@ -4,7 +4,7 @@ import { loadSchedule } from "../../lib/data"; import { use, useEffect, useState } from "react"; import Flag from "../flag"; -import { COPY, COPY_SUCCESS, FILTER_BY_COUNTRY, FILTER_BY_SPORT, LANGUAGE, MADE_BY_FABRICE, NOT_AFFILIATED, NO_EVENT_FOR_FILTERS } from "../../lib/text"; +import { ALL_EVENTS, COPY, COPY_SUCCESS, FILTER_BY_COUNTRY, FILTER_BY_EVENT_TYPE, FILTER_BY_SPORT, GOLD_MEDAL_EVENTS, LANGUAGE, MADE_BY_FABRICE, MEDAL_EVENTS, NOT_AFFILIATED, NO_EVENT_FOR_FILTERS } from "../../lib/text"; import useLocalStorage from "@/lib/local-storage"; import { GoogleAnalytics } from "@next/third-parties/google"; @@ -72,8 +72,25 @@ export interface Calendar { const COLORS = ['azzurro', 'giallo', 'rosa', 'rosso', 'verde', 'viola']; + +const EVENT_TYPE_ALL = "all"; +const EVENT_TYPE_MEDAL = "medal-events"; +const EVENT_TYPE_GOLD_MEDAL = "gold-medal-events"; + +const EVENT_TYPES = [{ + key: EVENT_TYPE_ALL.toUpperCase(), + name: ALL_EVENTS +}, { + key: EVENT_TYPE_MEDAL.toUpperCase(), + name: MEDAL_EVENTS, +}, { + key: EVENT_TYPE_GOLD_MEDAL.toUpperCase(), + name: GOLD_MEDAL_EVENTS, +}] + const DEFAULT_NOC = "world"; const DEFAULT_SPORT = "all-sports"; +const DEFAULT_EVENT_TYPE = EVENT_TYPE_ALL; export default function Home({ params, @@ -94,17 +111,27 @@ export default function Home({ const getParams = () => ({ noc: slug?.length ? slug[0].toLowerCase() : DEFAULT_NOC, - sport: slug?.length && slug.length >= 2 ? slug[1].toLowerCase() : DEFAULT_SPORT, + sport: slug?.length >= 2 ? slug[1].toLowerCase() : DEFAULT_SPORT, + type: slug?.length >= 3 ? slug[2].toLowerCase() : DEFAULT_EVENT_TYPE, }) - const generateLink = ({ noc, sport }: { noc?: string; sport?: string }) => { - const { noc: newNOC, sport: newSport } = getParams(); + const generateLink = ({ noc, sport, type }: { noc?: string; sport?: string; type?: string }) => { + const { noc: newNOC, sport: newSport, type: newType } = getParams(); + + if (type && type !== DEFAULT_EVENT_TYPE) { + return `/${noc || newNOC}/${sport || newSport}/${type}`.toLowerCase(); + } + return `/${noc || newNOC}/${sport || newSport}`.toLowerCase(); } const generateCalendarLink = () => { const host = typeof window !== 'undefined' ? window.location.host : ''; - const { noc, sport } = getParams(); + const { noc, sport, type } = getParams(); + + if(type !== DEFAULT_EVENT_TYPE) { + return `http://${host}/api/data/${language}/${sport}/${type}.ics`; + } return `http://${host}/api/data/${language}/${sport}/${noc === DEFAULT_NOC ? "calendar" : noc}.ics`; }; @@ -114,19 +141,23 @@ export default function Home({ useEffect(() => { let selectedNOC = DEFAULT_NOC; let selectedSport = DEFAULT_SPORT; + if (qs.get('noc')) { selectedNOC = qs.get('noc')!.toLowerCase(); - } else if (slug && slug.length >= 1) { - selectedNOC = slug[0].toLowerCase(); + } else if (slug?.length >= 1) { + selectedNOC = getParams().noc.toLowerCase(); } if (qs.get('sport')) { selectedSport = qs.get('sport')!.toLowerCase(); - } else if (slug && slug.length >= 2) { - selectedSport = slug[1].toLowerCase(); + } else if (slug?.length >= 2) { + selectedSport = getParams().sport.toLowerCase(); } - const expectedUrl = `/${selectedNOC}/${selectedSport}`; + let expectedUrl = `/${selectedNOC}/${selectedSport}`; + if (getParams().type !== DEFAULT_EVENT_TYPE) { + expectedUrl = `/${selectedNOC}/${selectedSport}/${getParams().type}`; + } if (pathname !== expectedUrl) { permanentRedirect(expectedUrl); } @@ -145,20 +176,31 @@ export default function Home({ return false; } - const { noc, sport } = getParams(); + const { noc, sport, type } = getParams(); - if (noc !== DEFAULT_NOC) { - if (!event.nocs.includes(noc.toUpperCase())) { + if ( + noc !== DEFAULT_NOC && + !event.nocs.includes(noc.toUpperCase()) + ) { + visible = false; + } + + if ( + sport !== DEFAULT_SPORT && + event.sport !== sport.toUpperCase() + ) { + visible = false; + } + + if (type !== DEFAULT_EVENT_TYPE) { + if ( + (type === "medal-events" && event.medal === "0") || + (type === "gold-medal-events" && event.medal !== "1") + ) { visible = false; } } - if (sport !== DEFAULT_SPORT) { - if (event.sport !== sport.toUpperCase()) { - visible = false; - } - } - console.log({ es: event.sport, en: event.nocs, sport, noc, visible }) return visible; } @@ -188,7 +230,6 @@ export default function Home({ const events = data.events.filter(event => filter(event)); - let main = (
@@ -341,13 +382,12 @@ export default function Home({ const header = (
- Olympics Calendar + Olympics Calendar
- {/* */}
); @@ -468,6 +508,35 @@ export default function Home({
+
+ {translate(FILTER_BY_EVENT_TYPE)} + {getParams().type !== DEFAULT_EVENT_TYPE && ( +
+ + X {translate(EVENT_TYPES.find(type => type.key === getParams().type.toUpperCase())?.name || {})} + +
+ )} +
+ +
+
+
{translate(LANGUAGE)}
diff --git a/ui/lib/text.ts b/ui/lib/text.ts index 823523540..937411b35 100644 --- a/ui/lib/text.ts +++ b/ui/lib/text.ts @@ -106,3 +106,51 @@ export const LANGUAGE = { ja: "言語", ru: "Язык", } + +export const FILTER_BY_EVENT_TYPE = { + en: "Filter by event type", + fr: "Filtrer par type d'événement", + es: "Filtrar por tipo de evento", + de: "Nach Veranstaltungstyp filtern", + it: "Filtra per tipo di evento", + pt: "Filtrar por tipo de evento", + zh: "按事件类型筛选", + ja: "イベントタイプでフィルタリング", + ru: "Фильтр по типу события" +} + +export const ALL_EVENTS = { + en: "All events", + fr: "Tous les événements", + es: "Todos los eventos", + de: "Alle Veranstaltungen", + it: "Tutti gli eventi", + pt: "Todos os eventos", + zh: "所有事件", + ja: "すべてのイベント", + ru: "Все события" +}; + +export const MEDAL_EVENTS = { + en: "Medal events", + fr: "Épreuves de médailles", + es: "Eventos de medallas", + de: "Medaillenveranstaltungen", + it: "Eventi di medaglia", + pt: "Eventos de medalha", + zh: "奖牌赛事", + ja: "メダルイベント", + ru: "Медальные события" +}; + +export const GOLD_MEDAL_EVENTS = { + en: "Gold medal events", + fr: "Épreuves de médailles d'or", + es: "Eventos de medallas de oro", + de: "Goldmedaillenveranstaltungen", + it: "Eventi di medaglia d'oro", + pt: "Eventos de medalha de ouro", + zh: "金牌赛事", + ja: "金メダルイベント", + ru: "Золотые медальные события" +};