From 9b34db3feda24f628b003e38ee1250f99a095fb5 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Tue, 15 Jul 2025 17:59:29 -0700 Subject: [PATCH] feat: set up service to fetch comic data --- .gitignore | 3 ++ removeBadge.js | 15 ++++++++ src/app/comic-service.ts | 75 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 removeBadge.js create mode 100644 src/app/comic-service.ts diff --git a/.gitignore b/.gitignore index cc7b141..cad6faa 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,6 @@ testem.log # System files .DS_Store Thumbs.db + +# We upload this to our CDN. +comics.json diff --git a/removeBadge.js b/removeBadge.js new file mode 100644 index 0000000..7d90933 --- /dev/null +++ b/removeBadge.js @@ -0,0 +1,15 @@ +/** + * @copyright nhcarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ + + +/** + * Run this in the Discord console to remove the bot badges from the app. + * This is how we create the appearance that the webhook is a user. + */ +const appBadges = document.querySelectorAll("[class^='botTag']") +for (const badge of appBadges) { + badge.remove(); +} diff --git a/src/app/comic-service.ts b/src/app/comic-service.ts new file mode 100644 index 0000000..d76fa82 --- /dev/null +++ b/src/app/comic-service.ts @@ -0,0 +1,75 @@ +/** + * @copyright nhcarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ + +import { Injectable } from "@angular/core"; + +const oneDay = 1000 * 60 * 60 * 24; + +@Injectable({ + providedIn: "root", +}) +export class ComicService { + // eslint-disable-next-line @typescript-eslint/class-literal-property-style -- I should turn this rule off. + private readonly comicsUrl: string = "https://cdn.yurigpt.com/comics.json"; + + private ttl = 0; + + private comics: Array<{ + number: number; + title: string; + date: string; + }> = []; + + public constructor() { + void this.loadComics(); + } + private static isObject(object: unknown): object is Record { + return typeof object === "object" && object !== null; + } + + private static validateComics( + comics: unknown, + ): comics is Array<{ number: number; title: string; date: string }> { + if (!Array.isArray(comics)) { + return false; + } + + return comics.every((comic) => { + if (!ComicService.isObject(comic)) { + return false; + } + return ( + "number" in comic + && "title" in comic + && "date" in comic + && typeof comic["number"] === "number" + && typeof comic["title"] === "string" + && typeof comic["date"] === "string" + ); + }); + } + + public async loadComics(): Promise { + if (this.ttl > Date.now()) { + return; + } + this.comics = []; + const response = await fetch(this.comicsUrl); + const data: unknown = await response.json(); + if (ComicService.validateComics(data)) { + this.comics = data.sort((a, b) => { + return a.number - b.number; + }); + this.ttl = Date.now() + oneDay; + } else { + console.error("Invalid comics data format:", data); + } + } + + public getComics(): Array<{ number: number; title: string; date: string }> { + return this.comics; + } +}