feat: add external nav links, new game, better contrast (#63)

### Explanation

_No response_

### Issue

_No response_

### Attestations

- [ ] I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/)
- [ ] I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/).
- [ ] My contribution complies with the [Contributor Covenant](https://docs.nhcarrigan.com/dev/covenant/).

### Dependencies

- [ ] I have pinned the dependencies to a specific patch version.

### Style

- [ ] I have run the linter and resolved any errors.
- [ ] My pull request uses an appropriate title, matching the conventional commit standards.
- [ ] My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request.

### Tests

- [ ] My contribution adds new code, and I have added tests to cover it.
- [ ] My contribution modifies existing code, and I have updated the tests to reflect these changes.
- [ ] All new and existing tests pass locally with my changes.
- [ ] Code coverage remains at or above the configured threshold.

### Documentation

_No response_

### Versioning

_No response_

Reviewed-on: https://codeberg.org/nhcarrigan/portfolio/pulls/63
Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit is contained in:
Naomi Carrigan 2024-12-21 01:55:19 +00:00 committed by Naomi the Technomancer
parent 33d0d594c2
commit 0660afe142
7 changed files with 102 additions and 151 deletions

View File

@ -1,69 +0,0 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
"use client";
import { type JSX, useEffect, useState } from "react";
import { Activity } from "../../components/activity";
import { Rule } from "../../components/rule";
/**
* Renders the /activity page.
* @returns A React Component.
*/
const ActivityComponent = (): JSX.Element => {
const [ activity, setActivity ] = useState<
Array<{
type: string;
date: Date;
repo: string;
repoName: string;
}>
>([]);
useEffect(() => {
void fetch("/api/activity").
then(async(data) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return (await data.json()) as Array<{
type: string;
date: Date;
repo: string;
repoName: string;
}>;
}).
then((data) => {
setActivity(data);
});
}, []);
return (
<main className="w-[95%] text-center
max-w-4xl m-auto mt-16 mb-16 rounded-lg">
<h1 className="text-5xl">{`Recent Activity`}</h1>
<section>
<p className="mb-2">{`See what Naomi has been up to lately.`}</p>
<Rule />
<ol className="relative border-s border-[--primary] w-4/5 m-auto">
{activity.map((act, index) => {
return (
<Activity
date={act.date}
heart={index % 2 === 1
? "🩷"
: "🩵"}
key={act.date.toString()}
repo={act.repo}
repoName={act.repoName}
type={act.type}
/>
);
})}
</ol>
</section>
</main>
);
};
export default ActivityComponent;

View File

@ -4,72 +4,83 @@
* @author Naomi Carrigan
*/
"use client";
import { type JSX, useEffect, useState } from "react";
import { Issue } from "../../components/issue";
import { Rule } from "../../components/rule";
import type { JSX } from "react";
/**
* Renders the /contribute page.
* @returns A React Component.
*/
const ContributeComponent = (): JSX.Element => {
const [ issues, setIssues ] = useState<
Array<{
labels: Array<string>;
number: number;
title: string;
url: string;
body: string;
}>
>([]);
useEffect(() => {
void fetch("/api/contribute").
then(async(data) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return (await data.json()) as Array<{
labels: Array<string>;
number: number;
title: string;
url: string;
body: string;
}>;
}).
then((data) => {
setIssues(data);
});
}, []);
if (issues.length === 0) {
return (
<main className="w-[95%] text-center
max-w-4xl m-auto mt-16 mb-16 rounded-lg">
<h1 className="text-5xl">{`Open for Contribution~!`}</h1>
<section>
<p className="text-3xl">{`Loading...`}</p>
<p className="mb-2">{`Our issue tracker is currently unavailable while we work with the Codeberg team to address rate limit issues.`}</p>
</section>
</main>
);
}
return (
<main className="w-[95%] text-center
max-w-4xl m-auto mt-16 mb-16 rounded-lg">
<h1 className="text-5xl">{`Open for Contribution~!`}</h1>
<section>
<p className="mb-2">{`Heya! This page lists issues across all of our projects that are currently open for contribution.
We'd love to have you work on one!`}</p>
<Rule />
<ol className="relative border-s border-[--primary] w-4/5 m-auto">
{issues.map((act) => {
return (
<Issue key={act.url} {...act} />
);
})}
</ol>
</section>
</main>
);
/*
*To be restored when rate-limit is lifted.
*const [ issues, setIssues ] = useState<
*Array<{
* labels: Array<string>;
* number: number;
* title: string;
* url: string;
* body: string;
*}>
*>([]);
*
*useEffect(() => {
*void fetch("/api/contribute").
* then(async(data) => {
* // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
* return (await data.json()) as Array<{
* labels: Array<string>;
* number: number;
* title: string;
* url: string;
* body: string;
* }>;
* }).
* then((data) => {
* setIssues(data);
* });
*}, []);
*
*if (issues.length === 0) {
*return (
* <main className="w-[95%] text-center
* max-w-4xl m-auto mt-16 mb-16 rounded-lg">
* <h1 className="text-5xl">{`Open for Contribution~!`}</h1>
* <section>
* <p className="text-3xl">{`Loading...`}</p>
* </section>
* </main>
*);
*}
*
*return (
* <main className="w-[95%] text-center
* max-w-4xl m-auto mt-16 mb-16 rounded-lg">
* <h1 className="text-5xl">{`Open for Contribution~!`}</h1>
* <section>
* <p className="mb-2">{`Heya! This page lists issues across all of our projects that are currently open for contribution.
* We'd love to have you work on one!`}</p>
* <Rule />
* <ol className="relative border-s border-[--primary] w-4/5 m-auto">
* {issues.map((act) => {
* return (
* <Issue key={act.url} {...act} />
* );
* })}
* </ol>
* </section>
* </main>
*);
*/
};
export default ContributeComponent;

View File

@ -3,6 +3,8 @@
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Image from "next/image";
import React, { type JSX } from "react";
import { NavItems } from "../config/NavItems";
@ -32,31 +34,13 @@ const getLuminance = (hexColor: string): number => {
return rl + gl + bl;
};
const adjustColorLuminosity = (
hexColor: string,
luminosityChange: number,
): string => {
let r = Number.parseInt(hexColor.slice(1, 3), 16);
let g = Number.parseInt(hexColor.slice(3, 5), 16);
let b = Number.parseInt(hexColor.slice(5, 7), 16);
r = Math.round(r * luminosityChange);
g = Math.round(g * luminosityChange);
b = Math.round(b * luminosityChange);
r = Math.min(255, Math.max(0, r));
g = Math.min(255, Math.max(0, g));
b = Math.min(255, Math.max(0, b));
return `#${r.toString(16).padStart(2, "0")}${g.
toString(16).
padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
};
const generateColorPair = (): { background: string; color: string } => {
const backgroundColor = generateRandomColor();
const backgroundLuminance = getLuminance(backgroundColor);
const textColor
= backgroundLuminance > 0.5
? adjustColorLuminosity(backgroundColor, 0)
: adjustColorLuminosity(backgroundColor, 5);
? "#00000099"
: "#FFFFFF99";
return {
background: backgroundColor,
@ -88,15 +72,26 @@ const Home = (): JSX.Element => {
items-center border-solid border-2 rounded-3xl h-14 p-8 my-4"
href={item.href}
key={item.href}
rel={item.href.startsWith("http")
? "noreferrer"
: ""}
style={{
background: background,
borderColor: color,
color: color,
}}
target={item.href.startsWith("http")
? "_blank"
: "_self"}
>
{index % 2 === 1
? "🩷"
: "🩵"} {item.text}
: "🩵"} {item.text}{" "}
{item.href.startsWith("http")
? <FontAwesomeIcon
aria-label="External link"
icon={faUpRightFromSquare} />
: null}
</a>
);
})}

View File

@ -7,6 +7,7 @@
import {
faBars,
faTimes,
faUpRightFromSquare,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Image from "next/image";
@ -60,10 +61,21 @@ export const Navigation = (): JSX.Element | null => {
href={item.href}
key={item.href}
onClick={toggleMenu}
rel={item.href.startsWith("http")
? "noreferrer"
: ""}
target={item.href.startsWith("http")
? "_blank"
: "_self"}
>
{index % 2 === 1
? "🩷"
: "🩵"} {item.text}
{item.href.startsWith("http")
? <FontAwesomeIcon
aria-label="External link"
icon={faUpRightFromSquare} />
: null}
</a>
);
})}

View File

@ -187,4 +187,10 @@ export const Games: Array<{
name: "Angel Legion",
url: "https://store.steampowered.com/app/1333350/Angel_Legion/",
},
{
alt: "A detailed anime-style illustration of an angelic warrior character against a sunset cityscape background. The character has long blonde hair and wears elaborate white and gold armor with flowing white robes accented by blue and orange fabrics. She has multiple large white wings spread dramatically behind her and wears a golden crown-like headpiece. Golden chains float decoratively around her dress and armor. She's depicted in a dynamic pose with futuristic city towers and a reddish-orange sky visible in the background. The artwork combines elements of fantasy and sci-fi, with ornate details in the character's costume and architectural elements of the setting.",
img: "idle-angels.jpg",
name: "Idle Angels",
url: "https://play.google.com/store/apps/details?id=com.mujoysg.hxbb&hl=en-US&pli=1",
},
];

View File

@ -18,7 +18,6 @@ export const NavItems = [
{ href: "/games", text: "Game Screenshots" },
{ href: "/team", text: "The NHCarrigan Team" },
{ href: "/polycule", text: "Polycule" },
{ href: "/activity", text: "Activity" },
{ href: "/art", text: "Art of Naomi" },
{ href: "/manifesto", text: "Transfemme Manifesto" },
{ href: "/ask", text: "Ask Me Anything!" },
@ -31,6 +30,10 @@ export const NavItems = [
{ href: "/sales", text: "Sales Inquiries" },
{ href: "/newsletter", text: "Newsletter" },
{ href: "/appeal", text: "Sanction Appeals" },
{ href: "https://games.nhcarrigan.com", text: "Game Dev" },
{ href: "https://merch.nhcarrigan.link", text: "Merchandise" },
{ href: "https://docs.nhcarrigan.com", text: "Documentation" },
{ href: "https://chat.nhcarrigan.com", text: "Support" },
].sort((a, b) => {
return a.text.localeCompare(b.text);
});

View File

@ -20,11 +20,4 @@ describe("nav items", () => {
expect(href, "links are not unique").toHaveLength(NavItems.length);
expect(text, "names are not unique").toHaveLength(NavItems.length);
});
it("should be internal links", () => {
expect.hasAssertions();
for (const nav of NavItems) {
expect(nav.href, `${nav.href} is not internal`).toMatch(/^\/[\da-z-]+$/);
}
});
});