feat: add products page
Some checks failed
Code Analysis / SonarQube (push) Failing after 12s

This commit is contained in:
2025-06-27 16:27:00 -07:00
parent 94b481b9a2
commit 96eb46a6ea
3 changed files with 380 additions and 0 deletions

57
products.sh Executable file
View File

@ -0,0 +1,57 @@
#!/bin/bash
# Set your Gitea instance base URL and API token
GITEA_URL="https://git.nhcarrigan.com/api/v1"
TOKEN=$GITEA_TOKEN # Pass as CLI arg
# Output file
OUTPUT_FILE="./products/data.json"
# Organizations and their categories
declare -A orgs=(
["nhcarrigan"]="public"
["nhcarrigan-games"]="games"
["nhcarrigan-private"]="private"
["nhcarrigan-archive"]="archived"
)
# Initialize the JSON array
echo "[" > "$OUTPUT_FILE"
first=true
for org in "${!orgs[@]}"; do
category="${orgs[$org]}"
echo "Fetching repos for $org..."
# Fetch repos from the API
response=$(curl -s -H "Authorization: token $TOKEN" "$GITEA_URL/orgs/$org/repos")
# Parse each repo
echo "$response" | jq -c '.[]' | while read -r repo; do
name=$(echo "$repo" | jq -r '.name')
description=$(echo "$repo" | jq -r '.description // ""')
url=$(echo "$repo" | jq -r '.website')
# Skip ".profile" repos
if [[ "$name" == ".profile" ]]; then
continue
fi
# Add comma if not the first item
if [ "$first" = true ]; then
first=false
else
echo "," >> "$OUTPUT_FILE"
fi
# Write the repo object to the file
jq -n --arg name "$name" --arg description "$description" --arg url "$url" --arg category "$category" \
'{name: $name, description: $description, url: $url, category: $category}' >> "$OUTPUT_FILE"
done
done
# Close the JSON array
echo "]" >> "$OUTPUT_FILE"
echo "✨ Done! Your product data is in $OUTPUT_FILE 💖"

260
products/data.json Normal file
View File

@ -0,0 +1,260 @@
[
{
"name": "mod-logs",
"description": "Logs for moderation actions taken on our platforms.",
"url": "",
"category": "archived"
},
{
"name": "tingle-bot",
"description": "Bot for my friend Ruu's server",
"url": "",
"category": "archived"
},
{
"name": "announcements",
"description": "Repository for our announcements page.",
"url": "",
"category": "archived"
},
{
"name": "forms",
"description": "Client and server monorepo for our various webforms",
"url": "",
"category": "archived"
},
{
"name": "notes",
"description": "",
"url": "",
"category": "private"
},
{
"name": "status",
"description": "Status updates for our client work.",
"url": "",
"category": "private"
},
{
"name": "beaver-twitch",
"description": "Twitch bot for BigBadBeaver",
"url": "",
"category": "private"
},
{
"name": "obsidian",
"description": "",
"url": "",
"category": "private"
},
{
"name": "insomnium",
"description": "Our Insomnium requests",
"url": "",
"category": "private"
},
{
"name": "life-of-a-naomi",
"description": "A little game",
"url": "",
"category": "games"
},
{
"name": "naomis-adventure-1",
"description": "Our first full-length, paid game!",
"url": "",
"category": "games"
},
{
"name": "beccalia-origins",
"description": "The story of how Becca and Rosalia first met, and how they came to be. Never moved past a demo state.",
"url": "https://beccalia.nhcarrigan.com/origins",
"category": "games"
},
{
"name": "beccalia-prologue",
"description": "A short adventure to introduce our characters Becca and Rosalia. Our first attempt at game development!",
"url": "https://beccalia.nhcarrigan.com/prologue",
"category": "games"
},
{
"name": "ruu-goblin-quest",
"description": "A quick game we made about our friend Ruu, as part of a game jam she was hosting.",
"url": "https://goblin.nhcarrigan.com",
"category": "games"
},
{
"name": "template",
"description": "",
"url": "",
"category": "public"
},
{
"name": "a4p-bot",
"description": "Bot for the Art 4 Palestine charity initiative.",
"url": "https://a4p.nhcarrigan.com",
"category": "public"
},
{
"name": "boost-monitor",
"description": "Discord bot that monitors boost status in the Caylus Crew server.",
"url": "https://oogie.nhcarrigan.com/",
"category": "public"
},
{
"name": "docs",
"description": "Our documentation site.",
"url": "https://docs.nhcarrigan.com",
"category": "public"
},
{
"name": "eslint-config",
"description": "Our custom linter rules for our various TypeScript products.",
"url": "https://www.npmjs.com/package/@nhcarrigan/eslint-config",
"category": "public"
},
{
"name": "espanso",
"description": "Our shortcuts for Espanso.",
"url": "",
"category": "public"
},
{
"name": "celestine",
"description": "Moderation bot for Discord.",
"url": "https://hooks.nhcarrigan.com/",
"category": "public"
},
{
"name": "portfolio",
"description": "Our main homepage",
"url": "https://nhcarrigan.com",
"category": "public"
},
{
"name": "rig-task-bot",
"description": "Task bot for a friend's organisation.",
"url": "",
"category": "public"
},
{
"name": "security",
"description": "A quick tool to scan our projects for security concerns.",
"url": "https://security.nhcarrigan.com",
"category": "public"
},
{
"name": "website-headers",
"description": "Our global styling and scripts for all of our pages.",
"url": "https://cdn.nhcarrigan.com/headers/index.js",
"category": "public"
},
{
"name": "typescript-config",
"description": "Our global TypeScript configuration.",
"url": "https://www.npmjs.com/package/@nhcarrigan/typescript-config",
"category": "public"
},
{
"name": "blog",
"description": "Naomi's personal musings.",
"url": "https://blog.nhcarrigan.com",
"category": "public"
},
{
"name": "nginx-configs",
"description": "A version controlled backup of our servers' NGINX configurations.",
"url": "",
"category": "public"
},
{
"name": "vscode-themes",
"description": "Custom colour schemes for VSCode.",
"url": "https://marketplace.visualstudio.com/items?itemName=nhcarrigan.naomis-themes",
"category": "public"
},
{
"name": ".gitea",
"description": "This repository contains the files that customise our Gitea instance!",
"url": "https://git.nhcarrigan.com",
"category": "public"
},
{
"name": "aria-iuvo",
"description": "A user-installable translation application for Discord. Now you can translate messages directly on the platform!",
"url": "https://trans-bot.nhcarrigan.com/",
"category": "public"
},
{
"name": "cordelia-taryne",
"description": "AI-powered virtual assistant for Discord",
"url": "https://assistant.nhcarrigan.com/",
"category": "public"
},
{
"name": "rosalia-nightsong",
"description": "A webserver to handle alerting us to application logs and errors.",
"url": "https://alerts.nhcarrigan.com/",
"category": "public"
},
{
"name": "logger",
"description": "Our custom logging package, which pipes logs to our alerts server.",
"url": "https://www.npmjs.com/package/@nhcarrigan/logger",
"category": "public"
},
{
"name": "melody-iuvo",
"description": "Task management bot for Discord",
"url": "https://tasks.nhcarrigan.com/",
"category": "public"
},
{
"name": "static-pages",
"description": "The raw HTML pages served via our production box.",
"url": "",
"category": "public"
},
{
"name": "becca-lyria",
"description": "An AI-powered Discord bot that allows you to play an RPG without any friends!",
"url": "https://becca.nhcarrigan.com/",
"category": "public"
},
{
"name": "maylin-taryne",
"description": "An AI-powered companion to help you through the tough times.",
"url": "https://maylin.nhcarrigan.com/",
"category": "public"
},
{
"name": "gwen-abalise",
"description": "A ticket system for Discord!",
"url": "https://gwen.nhcarrigan.com/",
"category": "public"
},
{
"name": "nails",
"description": "Nail polish tracker for my sister",
"url": "",
"category": "public"
},
{
"name": "maribelle",
"description": "",
"url": "",
"category": "public"
},
{
"name": "mommy",
"description": "Mommy loves you~!",
"url": "https://mommy.nhcarrigan.com",
"category": "public"
},
{
"name": "mommy-bot",
"description": "Mommy loves you everywhere~!",
"url": "https://mommy-bot.nhcarrigan.com/",
"category": "public"
}
]

63
products/index.html Normal file
View File

@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>NHCarrigan Product Directory</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="An automated list of the products offered by NHCarrigan." />
<script src="https://cdn.nhcarrigan.com/headers/index.js" async defer></script>
</head>
<body>
<main>
<h1>NHCarrigan Product Directory</h1>
<section>
<p>An automated list of the products offered by NHCarrigan.</p>
<p>Products without a link to a public page are either not hosted or still under active development and not shipped yet. </p>
<p id="count">Loading directory...</p>
</section>
<section id="data">
</section>
</main>
</body>
<script>
const titleCase = (name) => {
// Repository names are in kebab-case, so we need to convert them to Title Case.
return name
.split("-")
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
}
const loadProducts = (products) => {
const public = products.filter(product => product.category === "public").sort((a, b) => a.name.localeCompare(b.name));
const private = products.filter(product => product.category === "private").sort((a, b) => a.name.localeCompare(b.name));
const games = products.filter(product => product.category === "games").sort((a, b) => a.name.localeCompare(b.name));
const archived = products.filter(product => product.category === "archived").sort((a, b) => a.name.localeCompare(b.name));
const html = `
<h2>Public Products</h2>
<p>These are our products which are open source and completely available to the public.</p>
${public.map(product => `<h3>${titleCase(product.name)}</h3><p>${product.description}</p>${product.url ? `<p class="italic"><a href="${product.url}" target="_blank">View Product</a></p>` : `<p class="italic">Not hosted.</p>`}`).join("")}
<h2>Public Games</h2>
<p>The games we have developed are available to the public, but due to licensing we cannot open source them.</p>
${games.map(product => `<h3>${titleCase(product.name)}</h3><p>${product.description}</p>${product.url ? `<p class="italic"><a href="${product.url}" target="_blank">Play Game</a></p>` : `<p class="italic">Not hosted.</p>`}`).join("")}
<h2>Private Products</h2>
<p>These are our products which are closed-source, either due to proprietary information or a client request. However, a hosted version may be available.</p>
${private.map(product => `<h3>${titleCase(product.name)}</h3><p>${product.description}</p>${product.url ? `<p class="italic"><a href="${product.url}" target="_blank">View Product</a></p>` : `<p class="italic">Not hosted.</p>`}`).join("")}
<h2>Archived Products</h2>
<p>These are our products which are no longer maintained, but are still available to the public.</p>
${archived.map(product => `<h3>${titleCase(product.name)}</h3><p>${product.description}</p>${product.url ? `<p class="italic"><a href="${product.url}" target="_blank">View Product</a></p>` : `<p class="italic">Not hosted.</p>`}`).join("")}
`;
const count = document.getElementById("count");
const data = document.getElementById("data");
count.innerText = `Found ${products.length} products.`;
data.innerHTML = html;
}
fetch("./data.json").then(res => res.json()).then(data => loadProducts(data.filter(el => el.name !== "template")))
</script>
<style>
.italic {
font-style: italic;
}
</style>
</html>