mirror of
https://github.com/clarencechaan/ufc-cal.git
synced 2026-10-19 00:38:30 -07:00
Convert to TypeScript
This commit is contained in:
1 parent
a1163ca642
commit
44c2594121
13 files changed
+868
-317
No files matched your search
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="66px" height="66px" viewBox="0 0 66 66" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 49 (51002) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Shape</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="AsDestination-Desktop" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(7, 0)">
|
||||
<g id="10-Sport-Instruction" transform="translate(-412.000000, -388.000000)" fill="#000000" fill-rule="nonzero">
|
||||
<g id="Group-6" transform="translate(202.000000, 165.000000)">
|
||||
<g id="Group" transform="translate(20.000000, 123.000000)">
|
||||
<g id="Group-3" transform="translate(150.000000, 81.000000)">
|
||||
<g id="Apple">
|
||||
<g id="apple" transform="translate(40.000000, 19.000000)">
|
||||
<path d="M35.0516469,10.0307023 C37.5110687,7.37202743 39.1745975,3.68080684 38.7192569,0 C35.1760752,0.127681595 30.8818395,2.10018113 28.3385686,4.75862967 C26.0587271,7.11213652 24.0627168,10.8735367 24.5954047,14.4821265 C28.5490879,14.7522047 32.5881895,12.6850759 35.0516469,10.0307023 Z M42.5973863,35.4504327 C42.5218325,27.1984436 49.067437,23.2351073 49.3573214,23.0435849 C45.6773806,17.439857 39.9462775,16.6730883 37.905204,16.5866089 C33.025146,16.0693174 28.3899093,19.5708266 25.9123276,19.5708266 C23.4448347,19.5708266 19.6281348,16.6545247 15.580738,16.7328542 C10.2599122,16.821371 5.35721052,19.9500229 2.62449464,24.9085512 C-2.90281499,34.883789 1.20780487,49.6606698 6.5938715,57.751925 C9.22682049,61.7129974 12.3617426,66.1564981 16.4784158,65.9957641 C20.4522765,65.8350302 21.9503492,63.3269018 26.7447646,63.3269018 C31.5411977,63.3269018 32.8841272,65.9957641 37.0803897,65.9153972 C41.3501882,65.8350302 44.0524135,61.8780327 46.6629429,57.904509 C49.6835255,53.3106882 50.9287059,48.8631126 51,48.6385379 C50.9083041,48.5848845 42.6850467,45.3183637 42.5973863,35.4504327 Z" id="Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 161 B |
@@ -0,0 +1 @@
|
||||
<svg width="76" height="76" version="1.1" viewBox="0 0 76 76" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-186,-38)"><path d="M244 56h-40v40h40V56z" fill="#fff"/><path d="M212.205 87.03c-1.495-1.01-2.53-2.485-3.095-4.435l3.47-1.43c.315 1.2.865 2.13 1.65 2.79.78.66 1.73.985 2.84.985 1.135 0 2.11-.345 2.925-1.035s1.225-1.57 1.225-2.635c0-1.09-.43-1.98-1.29-2.67-.86-.69-1.94-1.035-3.23-1.035h-2.005V74.13h1.8c1.11 0 2.045-.3 2.805-.9.76-.6 1.14-1.42 1.14-2.465 0 -.93-.34-1.67-1.02-2.225-.68-.555-1.54-.835-2.585-.835-1.02 0 -1.83.27-2.43.815a4.784 4.784 0 0 0 -1.31 2.005l-3.435-1.43c.455-1.29 1.29-2.43 2.515-3.415 1.225-.985 2.79-1.48 4.69-1.48 1.405 0 2.67.27 3.79.815 1.12.545 2 1.3 2.635 2.26.635.965.95 2.045.95 3.245 0 1.225-.295 2.26-.885 3.11-.59.85-1.315 1.5-2.175 1.955v.205a6.605 6.605 0 0 1 2.79 2.175c.725.975 1.09 2.14 1.09 3.5 0 1.36-.345 2.575-1.035 3.64s-1.645 1.905-2.855 2.515c-1.215.61-2.58.92-4.095.92-1.755.005-3.375-.5-4.87-1.51zM233.52 69.81l-3.81 2.755-1.905-2.89 6.835-4.93h2.62V88h-3.74V69.81z" fill="#4285F4"/><path d="M244 96h-40v18h40V96z" fill="#34A853"/><path d="M244 38h-52c-3.315 0 -6 2.685-6 6v52h18V56h40V38z" fill="#4285F4"/><path d="M186 96v12c0 3.315 2.685 6 6 6h12V96h-18z" fill="#188038"/><path d="M262 56h-18v40h18V56z" fill="#FBBC04"/><path d="M262 56V44c0-3.315-2.685-6-6-6h-12v18h18z" fill="#1967D2"/><path d="M244 114l18-18h-18v18z" fill="#EA4335"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 193 KiB |
@@ -0,0 +1,97 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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="./assets/favicon.ico" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
background-color: rgb(243, 243, 243);
|
||||
font-family: "Inter", sans-serif;
|
||||
}
|
||||
.links {
|
||||
margin: 40px 20px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
}
|
||||
.sub-link {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
row-gap: 16px;
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0px 4px 12px -3px rgba(0, 0, 0, 0.2);
|
||||
padding: 16px 32px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: 0.3s;
|
||||
}
|
||||
.sub-link:hover {
|
||||
box-shadow: 0px 4px 12px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.sub-link img {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
.sub-manual {
|
||||
margin-bottom: 40px;
|
||||
max-width: 100%;
|
||||
}
|
||||
.code-wrapper {
|
||||
background-color: white;
|
||||
padding: 2px 6px;
|
||||
border: 1px solid lightgray;
|
||||
border-radius: 4px;
|
||||
}
|
||||
code {
|
||||
overflow-wrap: break-word;
|
||||
max-width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Subscribe to this calendar to keep track of UFC events</h1>
|
||||
<div class="links">
|
||||
<a
|
||||
class="sub-link"
|
||||
href="webcal://raw.githubusercontent.com/clarencechaan/ufc-cal/ics/UFC.ics"
|
||||
><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="./assets/google.svg" alt="google" />
|
||||
<div>Google</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="sub-manual">
|
||||
<p>Or subscribe manually from your calendar app:</p>
|
||||
<div class="code-wrapper">
|
||||
<code
|
||||
>webcal://raw.githubusercontent.com/clarencechaan/ufc-cal/ics/UFC.ics</code
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<p>Maintained by Clarence Chan</p>
|
||||
<p>
|
||||
<a href="https://github.com/clarencechaan/ufc-cal"
|
||||
>Source code on GitHub</a
|
||||
>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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
@@ -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 };
|
||||
@@ -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;
|
||||
};
|
||||
Reference in new issue
Block a user