Convert to TypeScript

This commit is contained in:
Clarence Chan committed 2025-01-28 17:49:26 -05:00
1 parent a1163ca642
commit 44c2594121
13 files changed
+868 -317

No files matched your search

+1 -1
View File
@@ -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
-99
View File
@@ -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<void>} - 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();
+526 -1
View File
@@ -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",
+4 -2
View File
@@ -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"
}
-211
View File
@@ -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<Array<string>>} - 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<object>} - 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<Array<object>>} - 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 };
View File
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+3 -3
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>UFC Calendar</title>
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="icon" type="image/x-icon" href="./assets/favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
@@ -69,13 +69,13 @@
<a
class="sub-link"
href="webcal://raw.githubusercontent.com/clarencechaan/ufc-cal/ics/UFC.ics"
><img src="apple.svg" alt="apple" />
><img src="./assets/apple.svg" alt="apple" />
<div>Apple</div>
</a>
<a
class="sub-link"
href="https://calendar.google.com/calendar/u/0/r?cid=webcal://raw.githubusercontent.com/clarencechaan/ufc-cal/ics/UFC.ics"
><img src="google.svg" alt="google" />
><img src="./assets/google.svg" alt="google" />
<div>Google</div>
</a>
</div>
+99
View File
@@ -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();
+223
View File
@@ -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 };
+12
View File
@@ -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;
};