Compare commits

..

19 Commits

Author SHA1 Message Date
4d73218d45 fix: include products in script
Some checks failed
Code Analysis / SonarQube (push) Failing after 13s
2025-06-27 16:27:42 -07:00
96eb46a6ea feat: add products page
Some checks failed
Code Analysis / SonarQube (push) Failing after 12s
2025-06-27 16:27:00 -07:00
94b481b9a2 feat: mention wildcard domain
Some checks failed
Code Analysis / SonarQube (push) Failing after 13s
2025-06-27 14:34:08 -07:00
73618f9181 chore: update book and song lists
Some checks failed
Code Analysis / SonarQube (push) Failing after 13s
2025-06-25 12:53:27 -07:00
5b2a7f6eea chore: update form
Some checks failed
Code Analysis / SonarQube (push) Failing after 46s
2025-05-27 15:51:39 -07:00
415c122a36 chore: more music
Some checks failed
Code Analysis / SonarQube (push) Failing after 46s
2025-04-14 18:37:40 -07:00
b0031e35e0 chore: new songs
Some checks failed
Code Analysis / SonarQube (push) Failing after 44s
2025-04-11 00:58:37 -07:00
4f82486799 feat: remove resume
Some checks failed
Code Analysis / SonarQube (push) Failing after 48s
We're moving it to its own repo.
2025-04-08 13:40:00 -07:00
4cc0240cf6 feat: nightcore
Some checks failed
Code Analysis / SonarQube (push) Failing after 46s
2025-04-04 02:12:40 -07:00
9bd5956146 feat: pronouns
Some checks failed
Code Analysis / SonarQube (push) Failing after 50s
2025-04-03 14:05:16 -07:00
06b817cd55 feat: add ruutuli bot dev
Some checks failed
Code Analysis / SonarQube (push) Failing after 47s
2025-04-03 14:01:26 -07:00
f870bd28d8 feat: make resume print nicer
Some checks failed
Code Analysis / SonarQube (push) Failing after 47s
2025-04-03 13:58:58 -07:00
031406c95b feat: update music library, fix table
Some checks failed
Code Analysis / SonarQube (push) Failing after 48s
2025-04-03 13:01:44 -07:00
aff99e119a chore: forgot to remove matrix
Some checks failed
Code Analysis / SonarQube (push) Failing after 48s
2025-03-31 09:40:32 -07:00
166755a6e0 fix: clean up sitemap
Some checks failed
Code Analysis / SonarQube (push) Failing after 48s
2025-03-31 09:32:15 -07:00
b191f14a0b fix: use mid2v3 to handle special characters
Some checks failed
Code Analysis / SonarQube (push) Failing after 46s
2025-03-25 01:01:16 -07:00
00dc40ba47 feat: add books too
Some checks failed
Code Analysis / SonarQube (push) Failing after 50s
2025-03-24 20:07:24 -07:00
e33df16e43 feat: dynamic button to reset filters
Some checks failed
Code Analysis / SonarQube (push) Failing after 47s
2025-03-24 15:36:17 -07:00
e6f00559a9 feat: show filtered count when filter is applied 2025-03-24 15:32:35 -07:00
14 changed files with 83843 additions and 11502 deletions

View File

@ -1,24 +1,14 @@
# New Repository Template
# Static Pages
This template contains all of our basic files for a new GitHub repository. There is also a handy workflow that will create an issue on a new repository made from this template, with a checklist for the steps we usually take in setting up a new repository.
If you're starting a Node.JS project with TypeScript, we have a [specific template](https://github.com/naomi-lgbt/nodejs-typescript-template) for that purpose.
## Readme
Delete all of the above text (including this line), and uncomment the below text to use our standard readme template.
<!-- # Project Name
Project Description
This repository holds the files for all of our basic static site pages.
## Live Version
This page is currently deployed. [View the live website.]
This page is currently deployed.
## Feedback and Bugs
If you have feedback or a bug report, please feel free to open a GitHub issue!
If you have feedback or a bug report, please feel free to open an issue!
## Contributing
@ -36,4 +26,4 @@ Copyright held by Naomi Carrigan.
## Contact
We may be contacted through our [Chat Server](http://chat.nhcarrigan.com) or via email at `contact@nhcarrigan.com`. -->
We may be contacted through our [Chat Server](http://chat.nhcarrigan.com) or via email at `contact@nhcarrigan.com`.

36
books.sh Executable file
View File

@ -0,0 +1,36 @@
IFS=$'\n'
# Initialize an empty string to hold the list of books in JSON-like format
books=""
filecount=$(find /home/naomi/cloud/Books -type f | wc -l)
echo "Found $filecount files."
current=0
# Loop over each file found by find
for file in $(find /home/naomi/cloud/Books -type f -print0 | tr '\0' '\n'); do
current=$((current + 1))
echo -ne "Processing $current/$filecount\r"
title=$(exiftool "$file" | grep "^Title\s*:" | cut -d ":" -f 2 | sed -e 's/^[[:space:]]*//')
author=$(exiftool "$file" | grep "^Creator\s*:" | cut -d ":" -f 2 | sed -e 's/^[[:space:]]*//')
if [ -z "$title" ]; then
# remove .mp3 from the title
title=$(basename "$file" | sed -e 's/\.*//g')
fi
if [ -z "$author" ]; then
author=$(exiftool "$file" | grep "^Author\s*:" | cut -d ":" -f 2 | sed -e 's/^[[:space:]]*//')
fi
if [ -z "$author" ]; then
author="Unknown Author"
fi
# use jq to add the book to the list
books="$books$(jq -n --arg title "$title" --arg author "$author" '{title: $title, author: $author}'),"
done
# Remove trailing comma and add square brackets to complete the list
books="[${books%,}]"
# Write to ./books/books.json
echo "$books" > ./books/books.json
echo -ne "Done!\r"

1210
books/books.json Normal file

File diff suppressed because it is too large Load Diff

117
books/index.html Normal file
View File

@ -0,0 +1,117 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Naomi's Book Library</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="An interactive explorer for the books Naomi reads." />
<script src="https://cdn.nhcarrigan.com/headers/index.js" async defer></script>
</head>
<body>
<main>
<h1>Naomi's Book Library</h1>
<section>
<p>An interactive explorer for the books Naomi reads.</p>
<p id="count">Loading library...</p>
</section>
<div style="display: none;">
<span>Search Authors: </span>
<input type="text" id="author" />
</div>
<div style="display: none;">
<span>Search Titles: </span>
<input type="text" id="title" />
</div>
<div style="display: none;">
<button type="button" id="clear">Clear Filters</button>
</div>
<table id="books">
</table>
</main>
</body>
<script>
const authorQuery = document.getElementById('author');
const titleQuery = document.getElementById('title');
const resetButton = document.getElementById('clear');
const bookTable = document.getElementById('books');
const filterBooks = (author, title) => {
let result = [...bookList];
if(author) {
result = result.filter(book => book.author.toLowerCase().includes(author.toLowerCase()));
}
if(title) {
result = result.filter(book => book.title.toLowerCase().includes(title.toLowerCase()));
}
resetButton.parentElement.style.display = author || title ? "block" : "none";
document.getElementById('count').innerText = author || title ? `Filtered to ${result.length} books from ${bookList.length}.` : `Naomi currently has ${bookList.length} books.`;
updateTable(result);
}
const loadBooks = (books) => {
bookList.push(...books);
authorQuery.value = "";
titleQuery.value = "";
authorQuery.parentElement.style.display = "block";
titleQuery.parentElement.style.display = "block";
document.getElementById('count').innerText = `Naomi currently has ${books.length} books.`;
updateTable(books);
}
const updateTable = (books) => {
books = books.sort((a, b) => a.title.localeCompare(b.title));
bookTable.innerHTML = "";
const header = document.createElement('tr');
const authorHeader = document.createElement('th');
authorHeader.innerText = "Author";
const titleHeader = document.createElement('th');
titleHeader.innerText = "Title";
header.appendChild(titleHeader);
header.appendChild(authorHeader);
bookTable.appendChild(header);
books.forEach(book => {
const row = document.createElement('tr');
const author = document.createElement('td');
author.innerText = book.author;
const title = document.createElement('td');
title.innerText = book.title;
row.appendChild(title);
row.appendChild(author);
bookTable.appendChild(row);
});
}
const bookList = [];
fetch("./books.json").then(res => res.json()).then(data => loadBooks(data))
authorQuery?.addEventListener("input", (e) => filterBooks(e.target.value, titleQuery.value));
titleQuery?.addEventListener("input", (e) => filterBooks(authorQuery.value, e.target.value));
resetButton?.addEventListener("click", () => {
authorQuery.value = "";
titleQuery.value = "";
filterBooks("", "");
});
</script>
<style>
table {
width: 100%;
border-collapse: collapse;
}
tr:nth-of-type(even) {
background-color: #db7093dd;
color: #ffefef;
}
input {
background:var(--foreground);
color:var(--background);
border:1px solid white;
border-radius:10px;
padding:.25rem
}
button {
background:var(--foreground);
color:var(--background);
border:1px solid white;
border-radius:10px;
padding:.25rem;
cursor:url('https://cdn.nhcarrigan.com/cursors/pointer.cur'), pointer;
}
</style>
</html>

View File

@ -22,6 +22,9 @@
<span>Search Titles: </span>
<input type="text" id="title" />
</div>
<div style="display: none;">
<button type="button" id="clear">Clear Filters</button>
</div>
<table id="songs">
</table>
@ -30,6 +33,7 @@
<script>
const artistQuery = document.getElementById('artist');
const titleQuery = document.getElementById('title');
const resetButton = document.getElementById('clear');
const songTable = document.getElementById('songs');
const filterSongs = (artist, title) => {
let result = [...songList];
@ -39,10 +43,14 @@
if(title) {
result = result.filter(song => song.title.toLowerCase().includes(title.toLowerCase()));
}
resetButton.parentElement.style.display = artist || title ? "block" : "none";
document.getElementById('count').innerText = artist || title ? `Filtered to ${result.length} songs from ${songList.length}.` : `Naomi currently has ${songList.length} songs.`;
updateTable(result);
}
const loadSongs = (songs) => {
songList.push(...songs);
artistQuery.value = "";
titleQuery.value = "";
artistQuery.parentElement.style.display = "block";
titleQuery.parentElement.style.display = "block";
document.getElementById('count').innerText = `Naomi currently has ${songs.length} songs.`;
@ -75,15 +83,22 @@
artistQuery?.addEventListener("input", (e) => filterSongs(e.target.value, titleQuery.value));
titleQuery?.addEventListener("input", (e) => filterSongs(artistQuery.value, e.target.value));
resetButton?.addEventListener("click", () => {
artistQuery.value = "";
titleQuery.value = "";
filterSongs("", "");
});
</script>
<style>
table {
width: 100%;
max-width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
tr:nth-of-type(even) {
background-color: #db7093dd;
color: #ffefef;
background-color: var(--foreground);
color: var(--background);
}
input {
background:var(--foreground);
@ -92,5 +107,20 @@
border-radius:10px;
padding:.25rem
}
button {
background:var(--foreground);
color:var(--background);
border:1px solid white;
border-radius:10px;
padding:.25rem;
cursor:url('https://cdn.nhcarrigan.com/cursors/pointer.cur'), pointer;
}
tr {
max-width: 100%;
}
td {
max-width: 50%;
word-wrap: break-word;
}
</style>
</html>

File diff suppressed because it is too large Load Diff

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>

View File

@ -1,364 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Naomi Carrigan</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="This is Naomi's full work history!" />
<script
src="https://cdn.nhcarrigan.com/headers/index.js"
async
defer
></script>
<style>
hr {
border: 1px solid var(--background);
}
.title {
font-size: 1.3rem;
}
.subtitle {
font-size: 1.15rem;
}
.company {
text-decoration: underline;
}
.type {
font-style: italic;
}
.type::before {
content: " - ";
}
.date {
font-size: 0.8rem;
}
.info {
font-size: 0.8rem;
}
@media screen {
.card {
background: var(--foreground);
color: var(--background);
width: 80%;
max-width: 500px;
margin: auto;
border-radius: 10px;
box-shadow: 4px 4px 4px rgba(0, 0, 0, 0.7);
margin-bottom: 10px;
}
}
@media print {
:root {
font-size: 12px;
}
* {
color: black;
font-family: "Times New Roman", serif;
}
video,
footer,
hr {
display: none !important;
}
}
</style>
</head>
<body>
<main>
<h1>Naomi Carrigan</h1>
<p class="info">
Washington, USA | contact@nhcarrigan.com | https://nhcarrigan.com
</p>
<p>
Passionate technologist dedicated to building inclusive tech communities
and empowering individuals to break into the field. With a rich
background in community management, software engineering, and developer
experience, I strive to create accessible pathways for diverse talent.
</p>
<p>
Interested in hiring me? <a href="https://testimonials.nhcarrigan.com" target="_blank">See what past clients have to say</a>.
</p>
<section>
<h2>Employment</h2>
<!-- MARK: Current
-->
<div class="card">
<p class="title">Developer Experience Consultant</p>
<div>
<span class="company">Deepgram</span>
<span class="type">Contract</span>
</div>
<span class="date">June 2024 - present</span>
<hr />
<p class="subtitle">Community Bot Engineer</p>
<p class="date">July 2023 - June 2024</p>
</div>
<div class="card">
<p class="title">Educational Web Developer and Community Manager</p>
<div>
<span class="company">freeCodeCamp</span>
<span class="type">Contract</span>
</div>
<span class="date">Dec 2020 - present</span>
</div>
<div class="card">
<p class="title">Technomancer</p>
<div>
<span class="company">nhcarrigan</span>
<span class="type">Founder</span>
</div>
<span class="date">Dec 2020 - present</span>
</div>
<!-- MARK: Prior
-->
<div class="card">
<p class="title">Community Manager and Infrastructure Engineer</p>
<div>
<span class="company">Streamcord</span>
<span class="type">Contract</span>
</div>
<span class="date">Aug 2021 - Dec 2024</span>
</div>
<div class="card">
<p class="title">Senior Integrations Engineer</p>
<div>
<span class="company">Rythm</span>
<span class="type">Contract</span>
</div>
<span class="date">Apr 2022 - Oct 2024</span>
</div>
<div class="card">
<p class="title">Twitch Integration Engineer</p>
<div>
<span class="company">BigBadBeaver TV</span>
<span class="type">Freelance</span>
</div>
<span class="date">Oct 2022 - Jan 2024</span>
</div>
<div class="card">
<p class="title">Community Manager</p>
<div>
<span class="company">Tweetshift</span>
<span class="type">Contract</span>
</div>
<span class="date">Jan 2022 - May 2023</span>
</div>
<div class="card">
<p class="title">Community Manager</p>
<div>
<span class="company">4C</span>
<span class="type">Contract</span>
</div>
<span class="date">May 2022 - Nov 2022</span>
</div>
<div class="card">
<p class="title">Community Manager and Open Source Engineer</p>
<div>
<span class="company">Sema</span>
<span class="type">Contract</span>
</div>
<span class="date">May 2022 - Sep 2022</span>
</div>
<div class="card">
<p class="title">Safeway</p>
<div>
<span class="company">Service Operations Manager</span>
<span class="type">Full-time</span>
</div>
<span class="date">Nov 2016 - Apr 2020</span>
<hr />
<p class="subtitle">Person-in-Charge</p>
<p class="date">Aug 2013 - Nov 2016</p>
<hr />
<p class="subtitle">Produce Clerk</p>
<p class="date">Feb 2010 - Aug 2013</p>
<hr />
<p class="subtitle">Courtesy Clerk</p>
<p class="date">Aug 2009 - Feb 2010</p>
</div>
</section>
<section>
<h2>Volunteer</h2>
<!-- MARK: Volun. Current
-->
<div class="card">
<p class="title">Discord Moderator</p>
<div>
<span class="company">Virtual Insanity</span>
</div>
<span class="date">May 2024 - present</span>
</div>
<div class="card">
<p class="title">Discord Moderator</p>
<div>
<span class="company">FruitPursuits</span>
</div>
<span class="date">Mar 2024 - present</span>
</div>
<div class="card">
<p class="title">Development Lead</p>
<div>
<span class="company">Artists for Palestine</span>
</div>
<span class="date">Nov 2023 - present</span>
</div>
<div class="card">
<p class="title">Discord Moderator</p>
<div>
<span class="company">Angel Rose</span>
</div>
<span class="date">Sep 2023 - present</span>
</div>
<div class="card">
<p class="title">
Discord Moderator and Platform Engineering Manager
</p>
<div>
<span class="company">Caylus Crew</span>
</div>
<span class="date">Jun 2021 - present</span>
</div>
<div class="card">
<p class="title">
Discord Administrator and Lead Integrations Engineer
</p>
<div>
<span class="company">Commit Your Code</span>
</div>
<span class="date">Dec 2020 - present</span>
</div>
</section>
<!-- MARK: Volun. Prior
-->
<div class="card">
<p class="title">Hacktoberfest Community Moderator</p>
<div>
<span class="company">DigitalOcean</span>
</div>
<span class="date">Apr 2021 - Oct 2024</span>
</div>
<div class="card">
<p class="title">Discord Administrator and Integrations Engineer</p>
<div>
<span class="company">Azuliah</span>
</div>
<span class="date">Dec 2023 - Apr 2024</span>
</div>
<div class="card">
<p class="title">Discord Moderator</p>
<div>
<span class="company">Rion Kuroko</span>
</div>
<span class="date">Nov 2023 - Jan 2024</span>
</div>
<div class="card">
<p class="title">Senior Discord Moderator</p>
<div>
<span class="company">Rythm</span>
</div>
<span class="date">Feb 2022 - Jul 2022</span>
</div>
<div class="card">
<p class="title">Technical Support Staff</p>
<div>
<span class="company">TweetShift</span>
</div>
<span class="date">Sep 2021 - Feb 2022</span>
</div>
<div class="card">
<p class="title">Discord Moderator</p>
<div>
<span class="company">Rythm</span>
</div>
<span class="date">Sep 2021 - Feb 2022</span>
</div>
<div class="card">
<p class="title">Community Moderator</p>
<div>
<span class="company">Battlesnake</span>
</div>
<span class="date">Jun 2021 - Nov 2022</span>
</div>
<div class="card">
<p class="title">Integrations Engineer</p>
<div>
<span class="company">XCentric Collective</span>
</div>
<span class="date">Apr 2021 - Jul 2023</span>
</div>
<div class="card">
<p class="title">Technical Support Staff</p>
<div>
<span class="company">Streamcord</span>
</div>
<span class="date">Mar 2021 - Aug 2021</span>
</div>
<div class="card">
<p class="title">Discord Administrator</p>
<div>
<span class="company">EddieHub</span>
</div>
<span class="date">Jan 2021 - May 2023</span>
</div>
<div class="card">
<p class="title">Community Moderator</p>
<div>
<span class="company">freeCodeCamp</span>
</div>
<span class="date">Jun 2020 - Dec 2020</span>
</div>
<div class="card">
<p class="title">Shop Steward</p>
<div>
<span class="company">United Food and Commercial Workers</span>
</div>
<span class="date">Sep 2013 - Mar 2016</span>
</div>
<div class="card">
<p class="title">Instructional Assistant</p>
<div>
<span class="company">Vancouver Public Schools</span>
</div>
<span class="date">Sep 2010 - Jun 2014</span>
</div>
</main>
</body>
<script>
const dates = document.querySelectorAll(".date");
const today = new Date();
for (const date of dates) {
const start = new Date("5" + date.textContent.split(" - ")[0]);
const end =
date.textContent.split(" - ")[1] === "present"
? "present"
: new Date("5" + date.textContent.split(" - ")[1]);
const diff =
(end === "present" ? today.getTime() : end.getTime()) - start.getTime();
const diffYears = Math.floor(diff / (1000 * 60 * 60 * 24 * 365.25));
const diffMonths = Math.floor(
(diff % (1000 * 60 * 60 * 24 * 365.25)) / (1000 * 60 * 60 * 24 * 30.44)
);
const diffString = [];
if (diffYears > 0) {
diffString.push(`${diffYears} year${diffYears === 1 ? "" : "s"}`);
}
if (diffMonths > 0) {
diffString.push(`${diffMonths} month${diffMonths === 1 ? "" : "s"}`);
}
if (end === "present") {
date.textContent = `${start.toLocaleDateString("en-GB", {
year: "numeric",
month: "long",
})} - present (${diffString.join(", ")})`;
continue;
}
date.textContent = `${start.toLocaleDateString("en-GB", {
year: "numeric",
month: "long",
})} - ${end.toLocaleDateString("en-GB", {
year: "numeric",
month: "long",
})} (${diffString.join(", ")})`;
}
</script>
</html>

View File

@ -18,6 +18,7 @@
<section>
<p>A complete listing of all of our various pages.</p>
<p>All subdomains listed here are under our top level <code>nhcarrigan.com</code> domain.</p>
<p>Additionally, every one of these apps is available under our <code>naomi.lgbt</code> domain thanks to a wildcard DNS redirect.</p>
</section>
<section>
<h2>APIs</h2>
@ -58,28 +59,22 @@
<ul>
<li><a href="https://analytics.nhcarrigan.com" target="_blank"><code>analytics</code> - Analytics</a></li>
<li><a href="https://blog.nhcarrigan.com" target="_blank"><code>blog</code> - Our Blog</a></li>
<li><a href="https://chat.nhcarrigan.com" target="_blank"><code>chat</code> - Social Media Landing Page</a></li>
<li><a href="https://books.nhcarrigan.com" target="_blank"><code>books</code> - Naomi's E-Book Library</a></li>
<li><a href="https://cloud.nhcarrigan.com" target="_blank"><code>cloud</code> - NextCloud</a></li>
<li><a href="https://deepgram.nhcarrigan.com" target="_blank"><code>deepgram</code> - Deepgram Weekly Standup</a></li>
<li><a href="https://docs.nhcarrigan.com" target="_blank"><code>docs</code> - Documentation</a></li>
<li><a href="https://fcc.nhcarrigan.com" target="_blank"><code>fcc</code> - freeCodeCamp Weekly Standup</a></li>
<li><a href="https://fedi.nhcarrigan.com" target="_blank"><code>fedi</code> - Sharkey Instance</a></li>
<li><a href="https://forms.nhcarrigan.com" target="_blank"><code>forms</code> - Web Form Collection</a></li>
<li><a href="https://forum.nhcarrigan.com" target="_blank"><code>forum</code> - Discourse Forum</a></li>
<li><a href="https://git.nhcarrigan.com" target="_blank"><code>git</code> - Gitea Instance</a></li>
<li><a href="https://incidents.nhcarrigan.com" target="_blank"><code>incidents</code> - Uptime Kuma Instance</a></li>
<li><a href="https://irc-admin.nhcarrigan.com" target="_blank"><code>irc-admin</code> - UninspIRCd Admin Panel</a></li>
<li><a href="https://irc.nhcarrigan.com" target="_blank"><code>irc</code> - IRC Server and public web client</a></li>
<li><a href="https://irc-private.nhcarrigan.com" target="_blank"><code>irc-private</code> - Private IRC client for Naomi</a></li>
<li><a href="https://manual.nhcarrigan.com" target="_blank"><code>manual</code> - User Manual</a></li>
<li><a href="https://matrix-admin.nhcarrigan.com" target="_blank"><code>matrix-admin</code> - Synapse Admin Panel</a></li>
<li><a href="https://moderation.nhcarrigan.com" target="_blank"><code>moderation</code> - Mod Logs</a></li>
<li><a href="https://music.nhcarrigan.com" target="_blank"><code>music</code> - Music Library</a></li>
<li><a href="https://nails.nhcarrigan.com" target="_blank"><code>nails</code> - Nail Polish Tracker</a></li>
<li><a href="https://notes.nhcarrigan.com" target="_blank"><code>notes</code> - Private Notes for Sponsors</a></li>
<li><a href="https://quality.nhcarrigan.com" target="_blank"><code>quality</code> - SonarQube Instance</a></li>
<li><a href="https://resume.nhcarrigan.com" target="_blank"><code>resume</code> - Web-based Resume</a></li>
<li><a href="https://oogie.nhcarrigan.com" target="_blank"><code>analytics</code> - Analytics</a></li>
<li><a href="https://security.nhcarrigan.com" target="_blank"><code>security</code> - Automated Code Scanning Results</a></li>
<li><a href="https://sitemap.nhcarrigan.com" target="_blank"><code>sitemap</code> - This page!</a></li>
<li><a href="https://testimonials.nhcarrigan.com" target="_blank"><code>testimonials</code> - Client Reviews</a></li>
@ -91,9 +86,9 @@
<h2>Redirects</h2>
<ul>
<li><a href="https://announcements.nhcarrigan.com" target="_blank"><code>announcements</code> - Redirects to our forum</a></li>
<li><a href="https://chat.nhcarrigan.com" target="_blank"><code>chat</code> - Redirects to our forum</a></li>
<li><a href="https://contact.nhcarrigan.com" target="_blank"><code>contact</code> - Redirects to our docs</a></li>
<li><a href="https://donate.nhcarrigan.com" target="_blank"><code>donate</code> - Redirects to our docs</a></li>
<li><a href="https://matrix.nhcarrigan.com" target="_blank"><code>matrix</code> - Redirects to matrix.to page</a></li>
</ul>
</section>
<section>

View File

@ -10,15 +10,8 @@ current=0
for file in $(find /home/naomi/music -type f -print0 | tr '\0' '\n'); do
current=$((current + 1))
echo -ne "Processing $current/$filecount\r"
title=$(id3v2 -l "$file" | grep "TIT2" | cut -d ":" -f 2 | sed -e 's/^[[:space:]]*//')
artist=$(id3v2 -l "$file" | grep "TPE1" | cut -d ":" -f 2 | sed -e 's/^[[:space:]]*//')
if [ -z "$title" ]; then
title=$(id3v2 -l "$file" | grep "TT2" | cut -d ":" -f 2 | sed -e 's/^[[:space:]]*//')
fi
if [ -z "$artist" ]; then
artist=$(id3v2 -l "$file" | grep "TP1" | cut -d ":" -f 2 | sed -e 's/^[[:space:]]*//')
fi
title=$(mid3v2 -l "$file" | grep "TIT2\|TT2" | cut -d "=" -f 2)
artist=$(mid3v2 -l "$file" | grep "TPE1\|TP1" | cut -d "=" -f 2)
if [ -z "$title" ]; then
# remove .mp3 from the title
title=$(basename "$file" | sed -e 's/\.mp3//g')
@ -29,8 +22,6 @@ for file in $(find /home/naomi/music -type f -print0 | tr '\0' '\n'); do
# use jq to add the song to the list
songs="$songs$(jq -n --arg title "$title" --arg artist "$artist" '{title: $title, artist: $artist}'),"
# songs="$songs{\"title\":\"$(echo "$title" | sed -e 's/[\\"\/]/\\&/g' | sed -e 's/[\x01-\x1f\x7f]//g')\",\"artist\":\"$(echo "$artist" | sed -e 's/[\\"\/]/\\&/g' | sed -e 's/[\x01-\x1f\x7f]//g')\"},"
done
# Remove trailing comma and add square brackets to complete the list

View File

@ -1,6 +1,6 @@
#! /usr/bin/bash
dirs=("bsky" "chat" "games" "link-redirector" "resume" "testimonials" "manual" "sitemap" "music");
dirs=("bsky" "chat" "games" "link-redirector" "testimonials" "manual" "sitemap" "music" "books" "products");
for dir in "${dirs[@]}"; do
rsync -av $dir prod:/home/nhcarrigan

View File

@ -54,7 +54,7 @@
<main>
<h1>Testimonials</h1>
<p>See what our past clients have to say about our work!</p>
<p>Want to submit your own? <a href="https://forms.nhcarrigan.com/testimonial">Use our web form</a>.</p>
<p>Want to submit your own? <a href="https://forms.nhcarrigan.com/form/M_GrmqASymmO744axMOmu2LaMAaT5F0LmdVcU2c8-gQ">Use our web form</a>.</p>
<section>
<div class="card">
<p class="title">Alexis Madsen</p>