From c8bd129c0f4a4289217ca79d0ae1b4ba05ef4edc Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Sat, 27 Sep 2025 14:52:24 -0700 Subject: [PATCH] feat: use data api for products --- client/src/app/config/products.ts | 572 -------------------------- client/src/app/products.ts | 34 ++ client/src/app/products/products.html | 3 +- client/src/app/products/products.ts | 21 +- client/src/styles.css | 8 +- 5 files changed, 53 insertions(+), 585 deletions(-) delete mode 100644 client/src/app/config/products.ts create mode 100644 client/src/app/products.ts diff --git a/client/src/app/config/products.ts b/client/src/app/config/products.ts deleted file mode 100644 index 90809af..0000000 --- a/client/src/app/config/products.ts +++ /dev/null @@ -1,572 +0,0 @@ -/** - * @copyright nhcarrigan - * @license Naomi's Public License - * @author Naomi Carrigan - */ -/* eslint-disable stylistic/max-len -- we are going to have long descriptions here. */ -/* eslint-disable max-lines -- Big ol' config!*/ - -export const products: Array<{ - name: string; - description: string; - url: string | null; - wip: boolean; - category: "community" | "websites" | "apps"; - premium: boolean; - avatar: string | null; -}> = [ - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/rosalia.png", - category: "websites", - description: - "Our global logging server, which pipes logs from all of our apps into a Discord webhook and our email inbox.", - name: "Rosalia Nightsong", - premium: false, - url: "https://rosalia.nhcarrigan.com", - wip: false, - }, - { - avatar: null, - category: "websites", - description: - "Our self-hosted LibreTranslate instance, which powers some of our apps and is available for subscribers.", - name: "Translation Service", - premium: true, - url: "https://trans.nhcarrigan.com", - wip: false, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/aria.png", - category: "community", - description: - "A user-installable bot that allows you to translate any message into your preferred language.", - name: "Aria Iuvo", - premium: true, - url: "https://aria.nhcarrigan.com/", - wip: false, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/becca.png", - category: "community", - description: - "A user-installable Discord app that facilitates a solo Dungeons and Dragons experience in private messages.", - name: "Becca Lyria", - premium: true, - url: "https://becca.nhcarrigan.com", - wip: false, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/cordelia.png", - category: "community", - description: - "A user-installable Discord app that allows you to ask questions, generate alt text for images, evaluate code, and more.", - name: "Cordelia Taryne", - premium: true, - url: "https://cordelia.nhcarrigan.com/", - wip: false, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/gwen.png", - category: "community", - description: "A ticketing system for Discord servers.", - name: "Gwen Abalise", - premium: true, - url: "https://gwen.nhcarrigan.com/", - wip: false, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/maylin.png", - category: "community", - description: "A helpful and supportive Discord bot that allows you to have conversations with a virtual friend in private messages.", - name: "Maylin Taryne", - premium: true, - url: "https://maylin.nhcarrigan.com/", - wip: false, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/melody.png", - category: "community", - description: "A user-installable task management application for Discord.", - name: "Melody Iuvo", - premium: true, - url: "https://melody.nhcarrigan.com/", - wip: false, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/beccalia.png", - category: "apps", - description: "Originally planned as the story of Becca and Rosalia growing up, this game was only released as a demo.", - name: "Beccalia: Origins", - premium: false, - url: "https://beccalia.nhcarrigan.com/origins", - wip: false, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/beccalia.png", - category: "apps", - description: "An introductory story that sets the stage for the Beccalia universe, featuring Becca and Rosalia.", - name: "Beccalia: Prologue", - premium: false, - url: "https://beccalia.nhcarrigan.com/prologue", - wip: false, - }, - { - avatar: "https://cdn.nhcarrigan.com/profile.png", - category: "apps", - description: "A quick game that introduces who Naomi is, and provides a glimpse into her life.", - name: "Life of a Naomi", - premium: false, - url: "https://loan.nhcarrigan.com", - wip: false, - }, - { - avatar: null, - category: "apps", - description: "A game developed for our friend Ruu's game jam.", - name: "Ruu's Goblin Quest", - premium: false, - url: "https://goblin.nhcarrigan.com", - wip: false, - }, - { - avatar: "https://cdn.nhcarrigan.com/profile.png", - category: "websites", - description: "The personal musings of our founder, Naomi Carrigan.", - name: "Naomi's Blog", - premium: false, - url: "https://blog.nhcarrigan.com", - wip: false, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/nymira.png", - category: "websites", - description: "A service that allows you to claim a custom .naomi.party username for Bluesky.", - name: "Nymira", - premium: true, - url: "https://naomi.party", - wip: true, - }, - { - avatar: null, - category: "websites", - description: "A website outlining our policies, legal agreements, community rules, and product information.", - name: "NHCarrigan Documentation", - premium: false, - url: "https://docs.nhcarrigan.com", - wip: true, - }, - { - avatar: null, - category: "websites", - description: "A self-hosted Gitea instance to hold all of our source code.", - name: "Gitea", - premium: false, - url: "https://git.nhcarrigan.com", - wip: false, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/hikari.png", - category: "websites", - description: "This dashboard!", - name: "Hikari", - premium: false, - url: "https://hikari.nhcarrigan.com", - wip: true, - }, - { - avatar: null, - category: "community", - description: "A Discord, Slack, and Bluesky bot that provides you motherly love and encouragement.", - name: "Mommy Bot", - premium: false, - url: "https://mommy-bot.nhcarrigan.com", - wip: false, - }, - { - avatar: null, - category: "websites", - description: "A quick web app that provides you motherly love and encouragements.", - name: "Mommy", - premium: false, - url: "https://mommy.nhcarrigan.com", - wip: false, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/lucinda.png", - category: "websites", - description: "A kanban-style task management site.", - name: "Lucinda", - premium: false, - url: "https://lucinda.nhcarrigan.com", - wip: false, - }, - { - avatar: null, - category: "websites", - description: "Our homepage and marketing landing.", - name: "NHCarrigan", - premium: false, - url: "https://nhcarrigan.com", - wip: false, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/vitalia.png", - category: "websites", - description: "A full-featured nutrition tracker with community-driven nutrient data.", - name: "Vitalia", - premium: true, - url: "https://vitalia.nhcarrigan.com", - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/octavia.png", - category: "apps", - description: "Linux-native music player application with a focus on handling large libraries with minimal memory.", - name: "Octavia", - premium: true, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/maribelle.png", - category: "community", - description: "A Discord bot that allows you to configure daily progress huddle reminders for your server members.", - name: "Maribelle", - premium: true, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/sorielle.png", - category: "community", - description: "A Discord bot that allows servers to specify a venting channel for automatic deletion.", - name: "Sorielle", - premium: true, - url: "https://sorielle.nhcarrigan.com", - wip: false, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/verena.png", - category: "community", - description: "A Discord bot that allows identity and age verification.", - name: "Verena", - premium: true, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/thalassa.png", - category: "apps", - description: "A rich presence application for Linux.", - name: "Thalassa", - premium: true, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/aeris.png", - category: "websites", - description: "An authentication service featuring magic links and support for multiple social media platforms", - name: "Aeris", - premium: true, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/liora.png", - category: "community", - description: "A Discord bot that allows your server members to specify 'highlight' words, which they'll get pinged on if a message contains that word.", - name: "Liora", - premium: true, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/thessalia.png", - category: "community", - description: "An RPG game on Discord", - name: "Thessalia", - premium: true, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/callista.png", - category: "community", - description: "A user-installable Discord bot that allows you to bookmark messages and save a link and copy in your DMs.", - name: "Callista", - premium: true, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/isolda.png", - category: "apps", - description: "Modern, sleek email client for the web or desktop", - name: "Isolda", - premium: true, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/meliora.png", - category: "websites", - description: "Embeddable chat widget, comment section, and full support flow utility.", - name: "Meliora", - premium: true, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/aurelia.png", - category: "websites", - description: "Blogging platform with markdown editor", - name: "Aurelia", - premium: true, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/eirene.png", - category: "community", - description: "Website and Discord activity that allows you to participate in code challenges competitively or collaboratively", - name: "Eirene", - premium: true, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/amirei.png", - category: "websites", - description: "A quick social link aggregator for 'link in bio' pages.", - name: "Amirei", - premium: true, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/zephra.png", - category: "websites", - description: "Microblogging social media platform.", - name: "Zephra", - premium: true, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/oriana.png", - category: "websites", - description: "Uptime monitoring tool with status pages", - name: "Oriana", - premium: true, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/lyra.png", - category: "websites", - description: "A web-based API mocking tool, allowing you to create temporary endpoints for a front-end to hit, test webhook payloads, and more!", - name: "Lyra", - premium: true, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/selene.png", - category: "apps", - description: "A local-only privacy-focused REST API client.", - name: "Selene", - premium: true, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/sybil.png", - category: "community", - description: "A Discord bot that syndicates forum threads to an indexable website and generates help articles based on resolved conversations.", - name: "Sybil", - premium: true, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/calenelle.png", - category: "websites", - description: "A group coordination app with event scheduling and such.", - name: "Calenelle", - premium: true, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/rowena.png", - category: "websites", - description: "Web app that allows you to create and share forms, and track responses in a user friendly table.", - name: "Rowena", - premium: true, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/alouette.png", - category: "websites", - description: "A web server that allows you to set up arbitrary webhooks and format them to post on Discord.", - name: "Alouette", - premium: true, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/clarion.png", - category: "community", - description: "A Discord bot with dashboard that allows server mangers to post and edit announcements, rules, and similar.", - name: "Clarion", - premium: true, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/elowyn.png", - category: "websites", - description: "A quick website that helps you format text.", - name: "Elowyn", - premium: false, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/evangeline.png", - category: "community", - description: "A Discord bot that allows you to configure canned replies, retrieve them anywhere on discord, and easily copy + paste them into chat.", - name: "Evangeline", - premium: true, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/theodora.png", - category: "community", - description: "A Discord bot that generates 100 days of code reminders.", - name: "Theodora", - premium: true, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/vivienne.png", - category: "websites", - description: "An RSS feed reader/management site.", - name: "Vivienne", - premium: true, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/veluna.png", - category: "community", - description: "Discord bot that allows you to receive and answer anonymous questions.", - name: "Veluna", - premium: true, - url: null, - wip: true, - }, - { - avatar: null, - category: "apps", - description: "Idle RPG in the browser.", - name: "Elysium", - premium: true, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/chibika.png", - category: "community", - description: "A Discord bot that generates ascii anime girls.", - name: "Chibika", - premium: true, - url: "https://chibika.nhcarrigan.com", - wip: false, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/elaria.png", - category: "websites", - description: "Meeting schedule coordination tool.", - name: "Elaria", - premium: true, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/elunara.png", - category: "community", - description: "Discord bot that allows users to proxy messages so they correctly appear as composed by an alter.", - name: "Elunara", - premium: true, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/aureline.png", - category: "websites", - description: "Web app that allows you to create/upload digital badges and certifications and grant them to users", - name: "Aureline", - premium: true, - url: null, - wip: true, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/lynira.png", - category: "apps", - description: "Link shortener managed via a Discord bot.", - name: "Lynira", - premium: true, - url: "https://lynira.link", - wip: false, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/altaria.png", - category: "community", - description: "A Discord bot that reminds you to provide alt-text for images.", - name: "Altaria", - premium: false, - url: "https://altaria.nhcarrigan.com", - wip: false, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/pavelle.png", - category: "community", - description: "Discord bot that allows you to throw things (like cake) at your fellow server members.", - name: "Pavelle", - premium: true, - url: "https://pavelle.nhcarrigan.com", - wip: false, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/amari.png", - category: "community", - description: "Naomi's virtual personal assistant who helps out with automation around our Discord community.", - name: "Amari", - premium: false, - url: "https://amari.nhcarrigan.com", - wip: false, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/serenya.png", - category: "community", - description: "Discord bot that allows you to force yourself to take a break.", - name: "Serenya", - premium: false, - url: "https://serenya.nhcarrigan.com", - wip: false, - }, - { - avatar: "https://cdn.nhcarrigan.com/new-avatars/caelia.png", - category: "community", - description: "Discord bot that gently reminds you to use inclusive language.", - name: "Caelia", - premium: false, - url: "https://caelia.nhcarrigan.com", - wip: false, - }, -]; diff --git a/client/src/app/products.ts b/client/src/app/products.ts new file mode 100644 index 0000000..b9401a6 --- /dev/null +++ b/client/src/app/products.ts @@ -0,0 +1,34 @@ +/** + * @copyright NHCarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ +import { Injectable } from "@angular/core"; + +export type Product = Array<{ + name: string; + description: string; + url: string | null; + wip: boolean; + category: "community" | "websites" | "apps"; + premium: boolean; + avatar: string | null; +}>; + +@Injectable({ + providedIn: "root", +}) +export class Products { + private products: Product = []; + public constructor() { } + + public async getProducts(): Promise { + if (this.products.length > 0) { + return this.products; + } + const request = await fetch("https://data.nhcarrigan.com/projects.json"); + const data = await request.json() as Product; + this.products = data; + return data; + } +} diff --git a/client/src/app/products/products.html b/client/src/app/products/products.html index bdb4e9e..6dfa7b0 100644 --- a/client/src/app/products/products.html +++ b/client/src/app/products/products.html @@ -56,8 +56,7 @@

- Oh dear, it appears there are no products in this category yet! Please check - back later. + Products are loading, please wait a moment...

diff --git a/client/src/app/products/products.ts b/client/src/app/products/products.ts index d23e86e..4f77a76 100644 --- a/client/src/app/products/products.ts +++ b/client/src/app/products/products.ts @@ -6,7 +6,7 @@ import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; -import { products } from "../config/products.js"; +import { Products as ProductService, type Product } from "../products.js"; @Component({ imports: [ CommonModule ], @@ -15,9 +15,9 @@ import { products } from "../config/products.js"; templateUrl: "./products.html", }) export class Products { - public view: (typeof products)[number]["category"] | "all" + public view: Product[number]["category"] | "all" = "all"; - public products: typeof products = []; + public products: Product = []; public readonly filters: { wip: boolean; prod: boolean; @@ -29,16 +29,17 @@ export class Products { prod: true, wip: true, }; + private allProducts: Product = []; public constructor() { - this.selectCategory("all"); + void this.fetchProducts(); } public selectCategory( - category: (typeof products)[number]["category"] | "all", + category: Product[number]["category"] | "all", ): void { this.view = category; - const sortedProducts = products.sort((a, b) => { + const sortedProducts = [ ...this.allProducts ].sort((a, b) => { return a.name.localeCompare(b.name); }); if (this.view === "all") { @@ -59,7 +60,7 @@ export class Products { private applyFilters(): void { this.selectCategory(this.view); - this.products = this.products.filter((product) => { + this.products = [ ...this.allProducts ].filter((product) => { if (!this.filters.wip && product.wip) { return false; } @@ -75,4 +76,10 @@ export class Products { return true; }); } + + private async fetchProducts(): Promise { + const productService = new ProductService(); + this.allProducts = await productService.getProducts(); + this.selectCategory("all"); + } } diff --git a/client/src/styles.css b/client/src/styles.css index 9bdbeee..7880aeb 100644 --- a/client/src/styles.css +++ b/client/src/styles.css @@ -4,8 +4,8 @@ } :root { - --foreground: #2a0a18; - --background: #ffb6c1bb; + --foreground: #8F2447; + --background: #E1F6F9DC; } * { @@ -85,8 +85,8 @@ a { align-items: center; } .is-dark { - --foreground: #ffb6c1; - --background: #2a0a18bb; + --foreground: #E1F6F9; + --background: #8F2447bb; } @media screen and (max-width: 625px) { #tree-nation-offset-website {