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
|
* @author Naomi Carrigan
|
||||||
*/
|
*/
|
||||||
"use client";
|
"use client";
|
||||||
import { type JSX, useEffect, useState } from "react";
|
import type { JSX } from "react";
|
||||||
import { Issue } from "../../components/issue";
|
|
||||||
import { Rule } from "../../components/rule";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the /contribute page.
|
* Renders the /contribute page.
|
||||||
* @returns A React Component.
|
* @returns A React Component.
|
||||||
*/
|
*/
|
||||||
const ContributeComponent = (): JSX.Element => {
|
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 (
|
return (
|
||||||
<main className="w-[95%] text-center
|
<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>
|
<h1 className="text-5xl">{`Open for Contribution~!`}</h1>
|
||||||
<section>
|
<section>
|
||||||
<p className="mb-2">{`Heya! This page lists issues across all of our projects that are currently open for contribution.
|
<p className="mb-2">{`Our issue tracker is currently unavailable while we work with the Codeberg team to address rate limit issues.`}</p>
|
||||||
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>
|
</section>
|
||||||
</main>
|
</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;
|
export default ContributeComponent;
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
* @license Naomi's Public License
|
* @license Naomi's Public License
|
||||||
* @author Naomi Carrigan
|
* @author Naomi Carrigan
|
||||||
*/
|
*/
|
||||||
|
import { faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import React, { type JSX } from "react";
|
import React, { type JSX } from "react";
|
||||||
import { NavItems } from "../config/NavItems";
|
import { NavItems } from "../config/NavItems";
|
||||||
@ -32,31 +34,13 @@ const getLuminance = (hexColor: string): number => {
|
|||||||
return rl + gl + bl;
|
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 generateColorPair = (): { background: string; color: string } => {
|
||||||
const backgroundColor = generateRandomColor();
|
const backgroundColor = generateRandomColor();
|
||||||
const backgroundLuminance = getLuminance(backgroundColor);
|
const backgroundLuminance = getLuminance(backgroundColor);
|
||||||
const textColor
|
const textColor
|
||||||
= backgroundLuminance > 0.5
|
= backgroundLuminance > 0.5
|
||||||
? adjustColorLuminosity(backgroundColor, 0)
|
? "#00000099"
|
||||||
: adjustColorLuminosity(backgroundColor, 5);
|
: "#FFFFFF99";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
background: backgroundColor,
|
background: backgroundColor,
|
||||||
@ -88,15 +72,26 @@ const Home = (): JSX.Element => {
|
|||||||
items-center border-solid border-2 rounded-3xl h-14 p-8 my-4"
|
items-center border-solid border-2 rounded-3xl h-14 p-8 my-4"
|
||||||
href={item.href}
|
href={item.href}
|
||||||
key={item.href}
|
key={item.href}
|
||||||
|
rel={item.href.startsWith("http")
|
||||||
|
? "noreferrer"
|
||||||
|
: ""}
|
||||||
style={{
|
style={{
|
||||||
background: background,
|
background: background,
|
||||||
borderColor: color,
|
borderColor: color,
|
||||||
color: color,
|
color: color,
|
||||||
}}
|
}}
|
||||||
|
target={item.href.startsWith("http")
|
||||||
|
? "_blank"
|
||||||
|
: "_self"}
|
||||||
>
|
>
|
||||||
{index % 2 === 1
|
{index % 2 === 1
|
||||||
? "🩷"
|
? "🩷"
|
||||||
: "🩵"} {item.text}
|
: "🩵"} {item.text}{" "}
|
||||||
|
{item.href.startsWith("http")
|
||||||
|
? <FontAwesomeIcon
|
||||||
|
aria-label="External link"
|
||||||
|
icon={faUpRightFromSquare} />
|
||||||
|
: null}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
import {
|
import {
|
||||||
faBars,
|
faBars,
|
||||||
faTimes,
|
faTimes,
|
||||||
|
faUpRightFromSquare,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
@ -60,10 +61,21 @@ export const Navigation = (): JSX.Element | null => {
|
|||||||
href={item.href}
|
href={item.href}
|
||||||
key={item.href}
|
key={item.href}
|
||||||
onClick={toggleMenu}
|
onClick={toggleMenu}
|
||||||
|
rel={item.href.startsWith("http")
|
||||||
|
? "noreferrer"
|
||||||
|
: ""}
|
||||||
|
target={item.href.startsWith("http")
|
||||||
|
? "_blank"
|
||||||
|
: "_self"}
|
||||||
>
|
>
|
||||||
{index % 2 === 1
|
{index % 2 === 1
|
||||||
? "🩷"
|
? "🩷"
|
||||||
: "🩵"} {item.text}
|
: "🩵"} {item.text}
|
||||||
|
{item.href.startsWith("http")
|
||||||
|
? <FontAwesomeIcon
|
||||||
|
aria-label="External link"
|
||||||
|
icon={faUpRightFromSquare} />
|
||||||
|
: null}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -187,4 +187,10 @@ export const Games: Array<{
|
|||||||
name: "Angel Legion",
|
name: "Angel Legion",
|
||||||
url: "https://store.steampowered.com/app/1333350/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: "/games", text: "Game Screenshots" },
|
||||||
{ href: "/team", text: "The NHCarrigan Team" },
|
{ href: "/team", text: "The NHCarrigan Team" },
|
||||||
{ href: "/polycule", text: "Polycule" },
|
{ href: "/polycule", text: "Polycule" },
|
||||||
{ href: "/activity", text: "Activity" },
|
|
||||||
{ href: "/art", text: "Art of Naomi" },
|
{ href: "/art", text: "Art of Naomi" },
|
||||||
{ href: "/manifesto", text: "Transfemme Manifesto" },
|
{ href: "/manifesto", text: "Transfemme Manifesto" },
|
||||||
{ href: "/ask", text: "Ask Me Anything!" },
|
{ href: "/ask", text: "Ask Me Anything!" },
|
||||||
@ -31,6 +30,10 @@ export const NavItems = [
|
|||||||
{ href: "/sales", text: "Sales Inquiries" },
|
{ href: "/sales", text: "Sales Inquiries" },
|
||||||
{ href: "/newsletter", text: "Newsletter" },
|
{ href: "/newsletter", text: "Newsletter" },
|
||||||
{ href: "/appeal", text: "Sanction Appeals" },
|
{ 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) => {
|
].sort((a, b) => {
|
||||||
return a.text.localeCompare(b.text);
|
return a.text.localeCompare(b.text);
|
||||||
});
|
});
|
||||||
|
@ -20,11 +20,4 @@ describe("nav items", () => {
|
|||||||
expect(href, "links are not unique").toHaveLength(NavItems.length);
|
expect(href, "links are not unique").toHaveLength(NavItems.length);
|
||||||
expect(text, "names 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