From e5b77b882145e4a89e0ecdf219710c90f6d41577 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Fri, 4 Jul 2025 19:58:18 -0700 Subject: [PATCH] feat: build out product directory --- client/src/app/app.routes.ts | 2 + client/src/app/config/products.ts | 473 ++++++++++++++++++++++++++ client/src/app/products/products.css | 85 +++++ client/src/app/products/products.html | 77 +++++ client/src/app/products/products.ts | 78 +++++ client/src/styles.css | 3 + 6 files changed, 718 insertions(+) create mode 100644 client/src/app/config/products.ts create mode 100644 client/src/app/products/products.css create mode 100644 client/src/app/products/products.html create mode 100644 client/src/app/products/products.ts diff --git a/client/src/app/app.routes.ts b/client/src/app/app.routes.ts index cd7b836..fd3e26b 100644 --- a/client/src/app/app.routes.ts +++ b/client/src/app/app.routes.ts @@ -6,9 +6,11 @@ import { Routes } from "@angular/router"; import { Home } from "./home/home.js"; +import { Products } from "./products/products.js"; import { Soon } from "./soon/soon.js"; export const routes: Routes = [ { component: Home, path: "", pathMatch: "full" }, + { component: Products, path: "products" }, { component: Soon, path: "**" }, ]; diff --git a/client/src/app/config/products.ts b/client/src/app/config/products.ts new file mode 100644 index 0000000..6f288a9 --- /dev/null +++ b/client/src/app/config/products.ts @@ -0,0 +1,473 @@ +/** + * @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 Discourse instance for our community.", + name: "Fourm", + premium: false, + url: "https://forum.nhcarrigan.com", + wip: false, + }, + { + 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: null, + wip: true, + }, + { + 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, + }, +]; diff --git a/client/src/app/products/products.css b/client/src/app/products/products.css new file mode 100644 index 0000000..3c78adc --- /dev/null +++ b/client/src/app/products/products.css @@ -0,0 +1,85 @@ +a.product { + text-decoration: none; +} + +a.product:hover { + background-color: var(--background); + color: var(--foreground); +} + +.product:not(a) { + cursor: default; + border: 2px dashed grey; +} + +.btn { + display: inline-block; + padding: 10px 20px; + background-color: var(--foreground); + color: var(--background); + text-decoration: none; + border-radius: 50px; + border: 2px solid white; + font-family: 'OpenDyslexic', monospace; +} + +.btn:disabled { + background-color: var(--background); + color: var(--foreground); +} + +.btn:hover { + background-color: var(--background); + color: var(--foreground); + transition: background-color 0.3s, color 0.3s; +} + +.product { + display: grid; + grid-template-areas: "logo title icon" "logo description icon"; + grid-template-columns: 100px 1fr auto; + background-color: var(--foreground); + color: var(--background); + border: 2px solid white; + border-radius: 50px; + margin-left: 10px; + margin-right: 10px; + margin-bottom: 10px; + padding-right: 20px; + align-items: center; +} + +.icons { + grid-area: icon; + font-size: 2rem; + display: grid; + grid-template-columns: repeat(2, auto); + gap: 10px; +} + +.title { + grid-area: title; + font-size: 1.5rem; + font-weight: bold; +} + +.description { + grid-area: description; + font-size: 1rem; + margin-top: 10px; +} + +.logo { + grid-area: logo; + width: 100px; + height: 100px; + border-radius: 50%; +} + +.row { + display: flex; + flex-direction: row; + justify-content: space-evenly; + align-items: center; + flex-wrap: wrap; +} diff --git a/client/src/app/products/products.html b/client/src/app/products/products.html new file mode 100644 index 0000000..31cab98 --- /dev/null +++ b/client/src/app/products/products.html @@ -0,0 +1,77 @@ +

Products

+Hikari +

Excellent! What sort of product are you looking for?

+
+ + + + +
+

And would you like to apply a filter?

+
+ + + + +
+
+

+ Oh dear, it appears there are no products in this category yet! Please check + back later. +

+
+
+ + +

{{ product.name }}

+ +

{{ product.description }}

+
+ + + + +
+
+ + +
+

{{ product.name }}

+ +

{{ product.description }}

+
+ + + + +
+
+
+
+
+I want something custom... diff --git a/client/src/app/products/products.ts b/client/src/app/products/products.ts new file mode 100644 index 0000000..d23e86e --- /dev/null +++ b/client/src/app/products/products.ts @@ -0,0 +1,78 @@ +/** + * @copyright nhcarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ + +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { products } from "../config/products.js"; + +@Component({ + imports: [ CommonModule ], + selector: "app-products", + styleUrl: "./products.css", + templateUrl: "./products.html", +}) +export class Products { + public view: (typeof products)[number]["category"] | "all" + = "all"; + public products: typeof products = []; + public readonly filters: { + wip: boolean; + prod: boolean; + paid: boolean; + free: boolean; + } = { + free: true, + paid: true, + prod: true, + wip: true, + }; + + public constructor() { + this.selectCategory("all"); + } + + public selectCategory( + category: (typeof products)[number]["category"] | "all", + ): void { + this.view = category; + const sortedProducts = products.sort((a, b) => { + return a.name.localeCompare(b.name); + }); + if (this.view === "all") { + this.products = sortedProducts; + return; + } + this.products = sortedProducts.filter((product) => { + return product.category === this.view; + }); + } + + public toggleFilter( + filter: "wip" | "prod" | "paid" | "free", + ): void { + this.filters[filter] = !this.filters[filter]; + this.applyFilters(); + } + + private applyFilters(): void { + this.selectCategory(this.view); + this.products = this.products.filter((product) => { + if (!this.filters.wip && product.wip) { + return false; + } + if (!this.filters.prod && !product.wip) { + return false; + } + if (!this.filters.paid && product.premium) { + return false; + } + if (!this.filters.free && !product.premium) { + return false; + } + return true; + }); + } +} diff --git a/client/src/styles.css b/client/src/styles.css index 880d432..9bdbeee 100644 --- a/client/src/styles.css +++ b/client/src/styles.css @@ -77,6 +77,9 @@ a { color: unset; cursor: url('https://cdn.nhcarrigan.com/cursors/pointer.cur'), pointer; } +.btn:not(:disabled) { + cursor: url('https://cdn.nhcarrigan.com/cursors/pointer.cur'), pointer; +} #tree-nation-offset-website { display: flex; align-items: center;