generated from nhcarrigan/template
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:
parent
33d0d594c2
commit
0660afe142
@ -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;
|
@ -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>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="w-[95%] text-center
|
||||
max-w-4xl m-auto mt-16 mb-16 rounded-lg">
|
||||
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>
|
||||
<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>
|
||||
);
|
||||
|
||||
/*
|
||||
*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;
|
||||
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
@ -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",
|
||||
},
|
||||
];
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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-]+$/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user