diff --git a/src/app/projects/page.tsx b/src/app/projects/page.tsx new file mode 100644 index 0000000..fe1c2ce --- /dev/null +++ b/src/app/projects/page.tsx @@ -0,0 +1,32 @@ +/** + * @copyright nhcarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ +import { Project } from "../../components/project"; +import { Projects } from "../../config/Projects"; +import type { JSX } from "react"; + +/** + * Renders the /projects page. + * @returns A React Component. + */ +const ProjectPage = (): JSX.Element => { + return ( +
+

{`Our Projects`}

+

{`These are all of the projects we are currently maintaining.`}

+
+ {Projects.toSorted((a, b) => { + return a.name.localeCompare(b.name); + }).map((project) => { + return ( + + ); + })} +
+
+ ); +}; + +export default ProjectPage; diff --git a/src/components/navigation.tsx b/src/components/navigation.tsx index 529da1f..5bce7af 100644 --- a/src/components/navigation.tsx +++ b/src/components/navigation.tsx @@ -84,7 +84,8 @@ export const Navigation = (): JSX.Element | null => { {isOpen - ?
+ ?
{NavItems.map((item, index) => { return ( { + const { name, url, source, description, type } = properties; + return ( +
+

{name}

+ + {type} + +

+ {description} +

+
+ {`See Project`} + + + {source === undefined + ? null + : + {`View Source`} + + + } +
+ ); +}; diff --git a/src/config/NavItems.ts b/src/config/NavItems.ts index 502b56d..9a20c06 100644 --- a/src/config/NavItems.ts +++ b/src/config/NavItems.ts @@ -9,17 +9,17 @@ * on main navbar. */ export const NavItems = [ - { href: "/about", text: "About" }, + { href: "/about", text: "About Naomi" }, { href: "/manual", text: "User Manual" }, - { href: "/work", text: "Our Work" }, - { href: "/contact", text: "Contact" }, + { href: "/work", text: "Employment History" }, + { href: "/contact", text: "Contact Us" }, { href: "/certs", text: "Certifications" }, { href: "/reviews", text: "Reviews" }, - { href: "/games", text: "Games" }, - { href: "/team", text: "Our Team" }, + { href: "/games", text: "Game Screenshots" }, + { href: "/team", text: "The NHCarrigan Team" }, { href: "/polycule", text: "Polycule" }, { href: "/activity", text: "Activity" }, - { href: "/art", text: "Art" }, + { href: "/art", text: "Art of Naomi" }, { href: "/manifesto", text: "Transfemme Manifesto" }, { href: "/ask", text: "Ask Me Anything!" }, { href: "/play", text: "Play with Naomi" }, @@ -27,6 +27,7 @@ export const NavItems = [ { href: "/contribute", text: "Contribute to our Projects" }, { href: "/koikatsu", text: "Koikatsu Scenes" }, { href: "/ref", text: "Reference Sheet" }, + { href: "/projects", text: "Our Projects" }, ].sort((a, b) => { return a.text.localeCompare(b.text); }); diff --git a/src/config/Projects.ts b/src/config/Projects.ts new file mode 100644 index 0000000..c19c940 --- /dev/null +++ b/src/config/Projects.ts @@ -0,0 +1,232 @@ +/** + * @copyright nhcarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ + +/** + * List of projects to render. + */ +export const Projects: Array<{ + name: string; + url: string; + source?: string; + description: string; + type: "Website" | "Bot" | "API" | "Game"; +}> = [ + { + description: "A tool to monitor the email complaints of the freeCodeCamp newsletter.", + name: "Email Monitor", + source: "https://github.com/freecodecamp/email-complaint-monitoring", + type: "API", + url: "https://complaint.freecodecamp.org/", + }, + { + description: "Our self-hosted LibreTranslate instance.", + name: "Translation API", + source: "https://github.com/LibreTranslate/LibreTranslate", + type: "API", + url: "https://trans.nhcarrigan.com/", + }, + { + description: "API to authenticate our Slack translation bot.", + name: "Translation Slack Auth", + type: "API", + url: "https://trans-slack.nhcarrigan.com/slack/install", + }, + { + description: "AI bot in our Discord which generates alt text.", + name: "AltGenerator", + source: "https://codeberg.org/nhcarrigan/alt-generator", + type: "Bot", + url: "https://alt.nhcarrigan.com/", + }, + { + description: "A bot and API to allow submitting anonymous questions for Naomi to answer.", + name: "Anon Bot", + source: "https://codeberg.org/nhcarrigan/anon-bot", + type: "Bot", + url: "https://anon.nhcarrigan.com/", + }, + { + description: "A bot to track art requests and latest news updates for the Art 4 Palestine charity.", + name: "Art4Palestine Bot", + source: "https://codeberg.org/nhcarrigan/a4p-bot", + type: "Bot", + url: "https://afp.nhcarrigan.com/", + }, + { + description: "A bot to remove special booster colour roles when someone stops boosting.", + name: "Boost Monitor", + source: "https://codeberg.org/nhcarrigan/boost-monitor", + type: "Bot", + url: "https://oogie.nhcarrigan.com/", + }, + { + description: "Custom moderation utility for freeCodeCamp.", + name: "CamperChan", + source: "github.com/freeCodeCamp/camperchan/", + type: "Bot", + url: "https://camperchan.nhcarrigan.com/", + }, + { + description: "AI-powered bot in our Discord community which can evaluate code snippets.", + name: "Code Evaluator", + source: "https://codeberg.org/nhcarrigan/code-evaluator", + type: "Bot", + url: "https://eval.nhcarrigan.com/", + }, + { + description: "Community syndication and automation tool that bridges Github discussions and Discord threads into an internal Slack channel.", + name: "Deepgram Bot", + type: "Bot", + url: "https://deepgram-discord-bot.fly.dev/", + }, + { + description: "AI bot designed to answer queries by providing actual sources.", + name: "Librarian", + source: "https://codeberg.org/nhcarrigan/librarian", + type: "Bot", + url: "https://lib.nhcarrigan.com/", + }, + { + description: "Python bot to detect links in the promote-your-stream channel for Streamcord.", + name: "Link Detector", + source: "https://codeberg.org/nhcarrigan/link-detector", + type: "Bot", + url: "https://linkdetector.nhcarrigan.com/health", + }, + { + description: "Our paid general-purpose moderation bot for Discord.", + name: "Moderation Bot", + source: "https://codeberg.org/nhcarrigan/mod-bot", + type: "Bot", + url: "https://hooks.nhcarrigan.com/", + }, + { + description: "A general-purpose AI bot for our Discord community.", + name: "NaomiAI", + source: "https://codeberg.org/nhcarrigan/anthropic-bot", + type: "Bot", + url: "https://naomiai.nhcarrigan.com/", + }, + { + description: "A bot that bridges discussions between Discord, Slack, Matrix, and IRC.", + name: "Social Media Bridge", + source: "https://codeberg.org/nhcarrigan/social-media-bridge", + type: "Bot", + url: "https://bridge.nhcarrigan.com/", + }, + { + description: "A kanban-style task management bot for Discord, installed directly to your user account.", + name: "Task Bot", + source: "https://codeberg.org/nhcarrigan/user-task-bot", + type: "Bot", + url: "https://tasks.nhcarrigan.com/", + }, + { + description: "A Zelda RP bot for my friend Ruu.", + name: "Tingle Bot", + source: "https://codeberg.org/nhcarrigan/tingle-bot", + type: "Bot", + url: "https://ruubot.nhcarrigan.com/", + }, + { + description: "A bot for Slack and Discord to translate user messages.", + name: "Translation Bot", + source: "https://codeberg.org/nhcarrigan/translation-bot", + type: "Bot", + url: "https://trans-bot.nhcarrigan.com/", + }, + { + description: "Our first game, an introduction to our original characters Becca and Rosalia", + name: "Beccalia: Prologue", + type: "Game", + url: "https://beccalia.nhcarrigan.com/prologue", + }, + { + description: "A cancelled game that explores Becca and Rosalia's origin stories.", + name: "Beccalia: Origins", + type: "Game", + url: "https://beccalia.nhcarrigan.com/origins", + }, + { + description: "A short game built for a weekend game jam our friend hosted.", + name: "Ruu's Goblin Quest", + type: "Game", + url: "https://goblin.nhcarrigan.com/", + }, + { + description: "Our self-hosted Plausible analytics.", + name: "Analytics", + source: "https://github.com/plausible/analytics/", + type: "Website", + url: "https://analytics.nhcarrigan.com/", + }, + { + description: "A quick landing page for our Beccalia games.", + name: "Beccalia Landing", + type: "Website", + url: "https://beccalia.nhcarrigan.com/", + }, + { + description: "A manual username service to provide free custom handles to Bluesky users.", + name: "BlueSky Username Service", + type: "Website", + url: "https://naomi.party/", + }, + { + description: "Community marketing site for Deepgram.", + name: "Deepgram Community", + type: "Website", + url: "https://community.deepgram.com/", + }, + { + description: "Personal site for my sister.", + name: "Denna", + source: "https://codeberg.org/nhcarrigan/denna", + type: "Website", + url: "https://denna.nhcarrigan.com/", + }, + { + description: "Our primary documentation platform.", + name: "Documentation", + source: "https://codeberg.org/nhcarrigan/docs", + type: "Website", + url: "https://docs.nhcarrigan.com/", + }, + { + description: "The core freeCodeCamp curriculum.", + name: "freeCodeCamp", + source: "https://github.com/freecodecamp/freecodecamp", + type: "Website", + url: "https://www.freecodecamp.org/", + }, + { + description: "A small server to handle link redirections.", + name: "Link Redirection Service", + type: "Website", + url: "https://nhcarrigan.link/", + }, + { + description: "Personal website for my partner Kaitlyn.", + name: "Kaitlyn", + source: "https://codeberg.org/nhcarrigan/kaitlyn", + type: "Website", + url: "https://kaitlyn.nhcarrigan.com/", + }, + { + description: "This website you're looking at right now!", + name: "Portfolio", + source: "https://codeberg.org/nhcarrigan/portfolio", + type: "Website", + url: "https://nhcarrigan.com/", + }, + { + description: "Portfolio site for my friend Starfazers.", + name: "Starfazers", + source: "https://codeberg.org/nhcarrigan/starfazers", + type: "Website", + url: "https://starfazers.nhcarrigan.com/", + }, +]; diff --git a/test/config/Projects.spec.ts b/test/config/Projects.spec.ts new file mode 100644 index 0000000..1fce66f --- /dev/null +++ b/test/config/Projects.spec.ts @@ -0,0 +1,29 @@ +/** + * @copyright nhcarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ +import { describe, it, expect } from "vitest"; +import { Projects } from "../../src/config/Projects"; + +describe("project objects", () => { + it("should have unique names", () => { + expect.assertions(1); + const set = new Set( + Projects.map((a) => { + return a.name; + }), + ); + expect(set, "are not unique").toHaveLength(Projects.length); + }); + + it("should have unique URLs", () => { + expect.assertions(1); + const set = new Set( + Projects.map((a) => { + return a.url; + }), + ); + expect(set, "are not unique").toHaveLength(Projects.length); + }); +});