From 44c259412186c45eb1b0ab61963b748c29e68e00 Mon Sep 17 00:00:00 2001 From: Clarence Chan Date: Tue, 28 Jan 2025 17:49:26 -0500 Subject: [PATCH] Convert to TypeScript --- README.md | 2 +- index.js | 99 ---- package-lock.json | 527 +++++++++++++++++- package.json | 6 +- scrape.js | 211 ------- apple.svg => src/assets/apple.svg | 0 favicon.ico => src/assets/favicon.ico | Bin google.svg => src/assets/google.svg | 0 .../assets/ufc-cal-example.png | Bin index.html => src/index.html | 6 +- src/index.ts | 99 ++++ src/scrape.ts | 223 ++++++++ src/types.ts | 12 + 13 files changed, 868 insertions(+), 317 deletions(-) delete mode 100644 index.js delete mode 100644 scrape.js rename apple.svg => src/assets/apple.svg (100%) rename favicon.ico => src/assets/favicon.ico (100%) rename google.svg => src/assets/google.svg (100%) rename ufc-cal-example.png => src/assets/ufc-cal-example.png (100%) rename index.html => src/index.html (93%) create mode 100644 src/index.ts create mode 100644 src/scrape.ts create mode 100644 src/types.ts diff --git a/README.md b/README.md index 3261f0b1..2582ea13 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ The three biggest points that this aims to address, which I found lacking in oth ## Example event -![Example event](./ufc-cal-example.png) +![Example event](./src/assets/ufc-cal-example.png) ## Info for nerds diff --git a/index.js b/index.js deleted file mode 100644 index 790ce4a7..00000000 --- a/index.js +++ /dev/null @@ -1,99 +0,0 @@ -import { getAllDetailedEvents } from "./scrape.js"; -import { writeFileSync } from "fs"; -import { createEvents } from "ics"; - -/** - * Extracts the details of recent and near-future UFC events, then creates an - * ICS file named "UFC.ics" in the current directory containing these events - * - * @returns {Promise} - A Promise that resolves when the ICS file is created - */ -async function createICS() { - try { - let events = await getAllDetailedEvents(); - if (!events.length) throw new Error("No events retrieved"); - - // Get current date and time to communicate to the user how up-to-date - // the event details are - let currentDateTime = new Date(); - let dateTimestr = currentDateTime.toLocaleString("en-US", { - month: "short", - day: "numeric", - hour: "numeric", - minute: "numeric", - timeZone: "America/Toronto", - timeZoneName: "short", - }); - - // Convert event details in the format in accordance with the ICS generator - events = events.map((event) => { - let date = new Date(parseInt(event.date) * 1000); - let start = [ - date.getFullYear(), - date.getMonth() + 1, - date.getDate(), - date.getHours(), - date.getMinutes(), - ]; - let duration = { hours: 3 }; - let title = event.name; - let description = ""; - - // Distinguish between main card and prelims if this information has been - // announced by the UFC, otherwise list all fights without categorizing - if (event.fightCard.length) - description = `${event.fightCard.join("\n")}\n`; - if (event.mainCard.length) - description += `Main Card\n--------------------\n${event.mainCard.join( - "\n" - )}\n`; - if (event.prelims.length) { - description += "\nPrelims"; - if (event.prelimsTime) { - let prelimsTime = new Date(parseInt(event.prelimsTime) * 1000); - let hoursAgo = (date - prelimsTime) / 1000 / 60 / 60; - if (hoursAgo > 0) description += ` (${hoursAgo} hrs before Main)`; - } - description += `\n--------------------\n${event.prelims.join("\n")}\n`; - } - if (event.earlyPrelims.length) { - description += "\nEarly Prelims"; - if (event.earlyPrelimsTime) { - let earlyPrelimsTime = new Date( - parseInt(event.earlyPrelimsTime) * 1000 - ); - let hoursAgo = (date - earlyPrelimsTime) / 1000 / 60 / 60; - if (hoursAgo > 0) description += ` (${hoursAgo} hrs before Main)`; - } - description += `\n--------------------\n${event.earlyPrelims.join( - "\n" - )}\n`; - } - description += `\n${event.url}`; - description += `\n\nAccurate as of ${dateTimestr}`; - - let location = event.location; - let uid = event.url; - let calName = "UFC"; - return { - start, - duration, - title, - description, - location, - uid, - calName, - }; - }); - - console.log("\nDetailed events:"); - console.log(events); - - // Create UFC.ics - writeFileSync("UFC.ics", createEvents(events).value); - } catch (error) { - console.error(error); - } -} - -createICS(); diff --git a/package-lock.json b/package-lock.json index 17a24e47..e71a2504 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,429 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { + "@types/node": "^22.12.0", "html-entities": "^2.4.0", "ics": "^3.4.0", - "node-html-parser": "^6.1.9" + "node-html-parser": "^6.1.9", + "tsx": "^4.19.2" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.12.0.tgz", + "integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" } }, "node_modules/boolbase": { @@ -115,6 +535,74 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -196,6 +684,16 @@ "integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==", "dev": true }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/tiny-case": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", @@ -208,6 +706,26 @@ "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", "dev": true }, + "node_modules/tsx": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz", + "integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.23.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-fest": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", @@ -220,6 +738,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "node_modules/yup": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/yup/-/yup-1.2.0.tgz", diff --git a/package.json b/package.json index 95ef3914..b0d084a5 100644 --- a/package.json +++ b/package.json @@ -5,14 +5,16 @@ "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "start": "node index.js" + "start": "npx tsx src/index.ts" }, "author": "Clarence Chan", "license": "ISC", "devDependencies": { + "@types/node": "^22.12.0", "html-entities": "^2.4.0", "ics": "^3.4.0", - "node-html-parser": "^6.1.9" + "node-html-parser": "^6.1.9", + "tsx": "^4.19.2" }, "type": "module" } diff --git a/scrape.js b/scrape.js deleted file mode 100644 index e52773ed..00000000 --- a/scrape.js +++ /dev/null @@ -1,211 +0,0 @@ -import { parse } from "node-html-parser"; -import { decode } from "html-entities"; - -/** - * Returns an array of URL strings of recent and near-future UFC events - * - * @returns {Promise>} - A Promise that resolves with the array - * of URL strings of UFC events - */ -async function getEventLinks() { - // Get first two pages of event links - let urls = [ - "https://www.ufc.com/events?page=0", - "https://www.ufc.com/events?page=1", - ]; - - async function getEventLinksFromUrl(url) { - const response = await fetch(url); - const text = await response.text(); - const root = parse(text); - - // Extract the URLs of the relevant events from the DOM object - let links = root.querySelectorAll(".c-card-event--result__headline"); - links = links.map( - (html) => `https://www.ufc.com${html.firstChild.getAttribute("href")}` - ); - - return links; - } - - try { - urls = urls.map(getEventLinksFromUrl); - - let links = await Promise.all(urls); - links = links.flat(); - - console.log("\nEvent links found:"); - console.log(links); - return links; - } catch (error) { - console.error(error); - } -} - -/** - * Returns the fight card details of a UFC event given its URL - * - * @param {string} url - A URL string of a UFC event - * @returns {Promise} - A Promise that resolves with the fight card details of - * the UFC event - */ -async function getDetailsFromEventLink(url) { - try { - const response = await fetch(url); - const text = await response.text(); - const root = parse(text); - - // Extract basic details of the event from the DOM object - let headlinePrefix = root - .querySelector(".c-hero__headline-prefix") - .innerText.trim(); - let headline = root - .querySelector(".c-hero__headline") - .innerText.replace(/\s\s+/g, " ") - .trim(); - let name = `${headlinePrefix}: ${headline}`; - let date = root - .querySelector(".c-hero__headline-suffix") - .getAttribute("data-timestamp"); - let prelimsTime = root - .querySelector("#prelims-card .c-event-fight-card-broadcaster__time") - ?.getAttribute("data-timestamp"); - let earlyPrelimsTime = root - .querySelector("#early-prelims .c-event-fight-card-broadcaster__time") - ?.getAttribute("data-timestamp"); - - let location = - root - .querySelector(".field--name-venue") - ?.innerText.replaceAll("\n", "") - .replaceAll(" ", " ") || ""; - let fightCard = []; - let mainCard = root.querySelectorAll("#main-card .l-listing__item"); - let prelims = []; - let earlyPrelims = []; - - /** - * Helper function to return the string of a fight given its DOM list item - * - * @param {object} li - The DOM list item of a fight - * @returns {string} - The string of a fight - */ - function convertLiToStr(li) { - let bout = li.querySelector(".c-listing-fight__class-text")?.innerText; - - // Return only fight names without bout weightclass if formatting for - // an event is broken on the UFC event page - if (!bout) - return ( - "• " + - li - .querySelector(".field--name-node-title") - .innerText.trim() - .replace(" vs ", " vs. ") - ); - - // Convert weightclass to lbs - if (bout.includes("Strawweight")) bout = "115"; - else if (bout.includes("Flyweight")) bout = "125"; - else if (bout.includes("Bantamweight")) bout = "135"; - else if (bout.includes("Featherweight")) bout = "145"; - else if (bout.includes("Lightweight")) bout = "155"; - else if (bout.includes("Welterweight")) bout = "170"; - else if (bout.includes("Middleweight")) bout = "185"; - else if (bout.includes("Light Heavyweight")) bout = "205"; - else if (bout.includes("Heavyweight")) bout = "265"; - else if (bout.includes("Catchweight")) bout = "CW"; - - // Extract and format fighter names with weightclass - let red = li - .querySelector(".c-listing-fight__corner-name--red") - .innerText.replaceAll("\n", "") - .replace(/\s+/g, " ") - .trim(); - let blue = li - .querySelector(".c-listing-fight__corner-name--blue") - .innerText.replaceAll("\n", "") - .replace(/\s+/g, " ") - .trim(); - let ranks = li.querySelectorAll( - ".c-listing-fight__ranks-row .js-listing-fight__corner-rank span" - ); - ranks = ranks.map((rank) => rank?.innerText); - let redRankStr = ranks[0] ? ` (${ranks[0]})` : ""; - let blueRankStr = ranks[1] ? ` (${ranks[1]})` : ""; - let fightStr = `• ${red}${redRankStr} vs. ${blue}${blueRankStr} @${bout}`; - - // Deentitize HTML entities - fightStr = decode(fightStr); - - return fightStr; - } - - console.log("\nGetting details from url:", url); - - // Check if main card & prelims have been announced - if (mainCard.length) { - // Main card has been announced, extract prelims - prelims = root.querySelectorAll("#prelims-card .l-listing__item"); - earlyPrelims = root.querySelectorAll("#early-prelims .l-listing__item"); - - console.log("Main card length:", mainCard.length); - console.log("Prelims length:", prelims.length); - - mainCard = mainCard.map(convertLiToStr); - prelims = prelims.map(convertLiToStr); - earlyPrelims = earlyPrelims.map(convertLiToStr); - } else { - // Main card has not been announced, extract entire fight card - fightCard = root.querySelectorAll( - ".l-listing__group--bordered .l-listing__item" - ); - - console.log("Fight card length:", fightCard.length); - - fightCard = fightCard.map(convertLiToStr); - } - - // Deentitize HTML entities - [name, location] = [decode(name), decode(location)]; - - let details = { - name, - url, - date, - location, - fightCard, - mainCard, - prelims, - earlyPrelims, - prelimsTime, - earlyPrelimsTime, - }; - return details; - } catch (error) { - throw new Error(`Failed to retrieve event: ${url}\n${error}`); - } -} - -/** - * Returns an array of details of recent and near-future UFC events - * - * @returns {Promise>} - A Promise that resolves with the array - * of details of recent and near-future UFC events - */ -async function getAllDetailedEvents() { - let events = []; - try { - // Get the URLs of recent and near-future UFC events - events = await getEventLinks(); - - // Convert each URL to the details of the corresponding UFC event - events = events.map(getDetailsFromEventLink); - events = await Promise.all(events); - return events; - } catch (error) { - console.error(error); - } -} - -export { getAllDetailedEvents }; diff --git a/apple.svg b/src/assets/apple.svg similarity index 100% rename from apple.svg rename to src/assets/apple.svg diff --git a/favicon.ico b/src/assets/favicon.ico similarity index 100% rename from favicon.ico rename to src/assets/favicon.ico diff --git a/google.svg b/src/assets/google.svg similarity index 100% rename from google.svg rename to src/assets/google.svg diff --git a/ufc-cal-example.png b/src/assets/ufc-cal-example.png similarity index 100% rename from ufc-cal-example.png rename to src/assets/ufc-cal-example.png diff --git a/index.html b/src/index.html similarity index 93% rename from index.html rename to src/index.html index 10c9dbfa..cd29e4f7 100644 --- a/index.html +++ b/src/index.html @@ -4,7 +4,7 @@ UFC Calendar - + apple + >apple
Apple
google + >google
Google
diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..e400454f --- /dev/null +++ b/src/index.ts @@ -0,0 +1,99 @@ +import { getAllDetailedEvents } from "./scrape.js"; +import * as fs from "fs"; +import { createEvents, DateArray, EventAttributes } from "ics"; + +/** + * Extracts the details of recent and near-future UFC events, then creates an + * ICS file named "UFC.ics" in the current directory containing these events + */ +async function createICS() { + try { + const events = await getAllDetailedEvents(); + if (!events?.length) throw new Error("No events retrieved"); + + // Convert event details in the format in accordance with the ICS generator + const formattedEvents = events.map(formatEventForCalendar); + + console.log("\nDetailed events:"); + console.log(formattedEvents); + + // Create UFC.ics + const eventsData = createEvents(formattedEvents).value; + if (eventsData) fs.writeFileSync("UFC.ics", eventsData); + } catch (error) { + console.error(error); + } +} + +function formatEventForCalendar(event: UFCEvent) { + const date = new Date(parseInt(event.date) * 1000); + const start: DateArray = [ + date.getFullYear(), + date.getMonth() + 1, + date.getDate(), + date.getHours(), + date.getMinutes(), + ]; + const duration: { hours: number } = { hours: 3 }; + const title = event.name; + let description = ""; + + // Distinguish between main card and prelims if this information has been + // announced by the UFC, otherwise list all fights without categorizing + if (event.fightCard.length) description = `${event.fightCard.join("\n")}\n`; + if (event.mainCard.length) + description += `Main Card\n--------------------\n${event.mainCard.join( + "\n" + )}\n`; + if (event.prelims.length) { + description += "\nPrelims"; + if (event.prelimsTime) { + const prelimsTime = new Date(parseInt(event.prelimsTime) * 1000); + const hoursAgo = (+date - +prelimsTime) / 1000 / 60 / 60; + if (hoursAgo > 0) description += ` (${hoursAgo} hrs before Main)`; + } + description += `\n--------------------\n${event.prelims.join("\n")}\n`; + } + if (event.earlyPrelims.length) { + description += "\nEarly Prelims"; + if (event.earlyPrelimsTime) { + const earlyPrelimsTime = new Date( + parseInt(event.earlyPrelimsTime) * 1000 + ); + const hoursAgo = (+date - +earlyPrelimsTime) / 1000 / 60 / 60; + if (hoursAgo > 0) description += ` (${hoursAgo} hrs before Main)`; + } + description += `\n--------------------\n${event.earlyPrelims.join("\n")}\n`; + } + description += `\n${event.url}`; + + // Get current date and time to communicate to the user how up-to-date + // the event details are + const dateTimestr = new Date().toLocaleString("en-US", { + month: "short", + day: "numeric", + hour: "numeric", + minute: "numeric", + timeZone: "America/Toronto", + timeZoneName: "short", + }); + description += `\n\nAccurate as of ${dateTimestr}`; + + const location = event.location; + const uid = event.url.href; + const calName = "UFC"; + + const calendarEvent: EventAttributes = { + start, + duration, + title, + description, + location, + uid, + calName, + }; + + return calendarEvent; +} + +createICS(); diff --git a/src/scrape.ts b/src/scrape.ts new file mode 100644 index 00000000..9893327f --- /dev/null +++ b/src/scrape.ts @@ -0,0 +1,223 @@ +import { parse, HTMLElement } from "node-html-parser"; +import { decode } from "html-entities"; + +/** + * Returns an array of URLs of recent and upcoming UFC events + */ +async function getEventURLs() { + // Get first two pages of event urls + const pageURLs = [ + new URL("https://www.ufc.com/events?page=0"), + new URL("https://www.ufc.com/events?page=1"), + ]; + + async function getEventLinksFromUrl(url: URL) { + const response = await fetch(url); + const text = await response.text(); + const root = parse(text); + + // Extract the URLs of the relevant events from the HTML element + const eventURLElements = root.querySelectorAll( + ".c-card-event--result__headline" + ); + const eventURLs = eventURLElements.map( + (html) => + new URL( + `https://www.ufc.com${(html.firstChild as HTMLElement).getAttribute( + "href" + )}` + ) + ); + + return eventURLs; + } + + try { + const eventURLs = ( + await Promise.all(pageURLs.map(getEventLinksFromUrl)) + ).flat(); + + console.log("\nEvent URLs found:"); + console.log(eventURLs); + return eventURLs; + } catch (error) { + console.error(error); + } +} + +/** + * Helper function to return the output string of a fight given its HTML + * element list item + */ +function convertLiToStr(li: HTMLElement) { + const bout = li.querySelector(".c-listing-fight__class-text")?.textContent; + + let fightStr = ""; + + // Return only fight names without bout weightclass if formatting for + // an event is broken on the UFC event page + if (!bout) { + const textContent = li + .querySelector(".field--name-node-title") + ?.textContent?.trim() + .replace(" vs ", " vs. "); + if (!textContent) return ""; + fightStr += `• ${textContent}`; + return fightStr; + } + + let weightClass = ""; + + // Get weightclass in short form + if (bout.includes("Strawweight")) weightClass = "115"; + else if (bout.includes("Flyweight")) weightClass = "125"; + else if (bout.includes("Bantamweight")) weightClass = "135"; + else if (bout.includes("Featherweight")) weightClass = "145"; + else if (bout.includes("Lightweight")) weightClass = "155"; + else if (bout.includes("Welterweight")) weightClass = "170"; + else if (bout.includes("Middleweight")) weightClass = "185"; + else if (bout.includes("Light Heavyweight")) weightClass = "205"; + else if (bout.includes("Heavyweight")) weightClass = "265"; + else if (bout.includes("Catchweight")) weightClass = "CW"; + + // Extract and format fighter names with weightclass + const red = li + .querySelector(".c-listing-fight__corner-name--red") + ?.textContent?.replaceAll("\n", "") + .replace(/\s+/g, " ") + .trim(); + const blue = li + .querySelector(".c-listing-fight__corner-name--blue") + ?.textContent?.replaceAll("\n", "") + .replace(/\s+/g, " ") + .trim(); + const ranks = [ + ...li.querySelectorAll( + ".c-listing-fight__ranks-row .js-listing-fight__corner-rank span" + ), + ].map((rank) => rank?.textContent); + + const redRankStr = ranks[0] ? ` (${ranks[0]})` : ""; + const blueRankStr = ranks[1] ? ` (${ranks[1]})` : ""; + fightStr += `• ${red}${redRankStr} vs. ${blue}${blueRankStr} @${bout}`; + + // Deentitize HTML entities + fightStr = decode(fightStr); + + return fightStr; +} + +/** + * Returns the fight card details of a UFC event given its URL + */ +async function getDetailsFromEventLink(url: URL) { + try { + const response = await fetch(url); + const text = await response.text(); + const root = parse(text); + + // Extract basic details of the event from the HTML + const headlinePrefix = root + .querySelector(".c-hero__headline-prefix") + ?.innerText.trim(); + const headline = root + .querySelector(".c-hero__headline") + ?.innerText.replace(/\s\s+/g, " ") + .trim(); + const date = root + .querySelector(".c-hero__headline-suffix") + ?.getAttribute("data-timestamp"); + + const prelimsTime = root + .querySelector("#prelims-card .c-event-fight-card-broadcaster__time") + ?.getAttribute("data-timestamp"); + const earlyPrelimsTime = root + .querySelector("#early-prelims .c-event-fight-card-broadcaster__time") + ?.getAttribute("data-timestamp"); + + let name = `${headlinePrefix}: ${headline}`; + let location = + root + .querySelector(".field--name-venue") + ?.innerText.replaceAll("\n", "") + .replaceAll(" ", " ") || ""; + let fightCard: string[] = []; + let mainCard: string[] = []; + let prelims: string[] = []; + let earlyPrelims: string[] = []; + + console.log(`\nGetting details from url: ${url.href}`); + + const mainCardElements = root.querySelectorAll( + "#main-card .l-listing__item" + ); + // Check if main card & prelims have been announced + if (mainCardElements.length) { + // Main card has been announced, extract prelims + const prelimsElements = root.querySelectorAll( + "#prelims-card .l-listing__item" + ); + const earlyPrelimsElements = root.querySelectorAll( + "#early-prelims .l-listing__item" + ); + + mainCard = mainCardElements.map(convertLiToStr); + prelims = prelimsElements.map(convertLiToStr); + earlyPrelims = earlyPrelimsElements.map(convertLiToStr); + + console.log(`Main card length: ${mainCard.length}`); + console.log(`Prelims length: ${prelims.length}`); + } else { + // Main card has not been announced, extract entire fight card + const fightCardElements = root.querySelectorAll( + ".l-listing__group--bordered .l-listing__item" + ); + + fightCard = fightCardElements.map(convertLiToStr); + + console.log(`Fight card length: ${fightCard.length}`); + } + + // Deentitize HTML entities + [name, location] = [decode(name), decode(location)]; + + if (!name || !date) { + throw new Error("Failed to retrieve event details"); + } + + const details: UFCEvent = { + name, + url, + date, + location, + fightCard, + mainCard, + prelims, + earlyPrelims, + prelimsTime, + earlyPrelimsTime, + }; + return details; + } catch (error) { + throw new Error(`Failed to retrieve event: ${url.href}\n${error}`); + } +} + +/** + * Returns an array of details of recent and upcoming UFC events + */ +async function getAllDetailedEvents() { + try { + const eventURLs = await getEventURLs(); + + // Convert each URL to the details of the corresponding UFC event + const detailedEvents = await Promise.all( + eventURLs?.map(getDetailsFromEventLink) ?? [] + ); + return detailedEvents; + } catch (error) { + console.error(error); + } +} + +export { getAllDetailedEvents }; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..41b83536 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,12 @@ +type UFCEvent = { + name: string; + url: URL; + date: string; + location: string; + fightCard: string[]; + mainCard: string[]; + prelims: string[]; + earlyPrelims: string[]; + prelimsTime: string | undefined; + earlyPrelimsTime: string | undefined; +};