feat: add mobile games (#42)

### Explanation

_No response_

### Issue

_No response_

### Attestations

- [x] I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/)
- [x] I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/).
- [x] 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

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

### Tests

- [x] 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.
- [x] All new and existing tests pass locally with my changes.
- [x] Code coverage remains at or above the configured threshold.

### Documentation

_No response_

### Versioning

Minor - My pull request introduces a new non-breaking feature.

Reviewed-on: https://codeberg.org/nhcarrigan/portfolio/pulls/42
Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit is contained in:
Naomi Carrigan 2024-11-14 05:10:48 +00:00 committed by Naomi the Technomancer
parent 792dc1d7d2
commit 25efbb1b90
5 changed files with 259 additions and 0 deletions

37
src/app/play/page.tsx Normal file
View File

@ -0,0 +1,37 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { PlayComponent } from "../../components/play";
import { Rule } from "../../components/rule";
import { Play } from "../../config/Play";
import type { JSX } from "react";
/**
* Renders the /play page.
* @returns A React Component.
*/
const PlayPage = (): JSX.Element => {
return (
<main className="w-[95%] text-center
max-w-4xl m-auto mt-16 mb-16 rounded-lg">
<h1 className="text-5xl">{`Play with Naomi`}</h1>
<section>
<p className="mb-2">
{`I play some silly li'l mobile games to keep myself from working non-stop. Here's what I'm currently playing - will you play with me?`}
</p>
<Rule />
<div className="w-full">
{Play.toSorted((a, b) => {
return a.name.localeCompare(b.name);
}).map((game) => {
return <PlayComponent key={game.name} {...game} />;
})}
</div>
</section>
</main>
);
};
export default PlayPage;

115
src/components/play.tsx Normal file
View File

@ -0,0 +1,115 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
/* eslint-disable react/jsx-no-bind */
"use client";
import { faAndroid, faAppStore } from "@fortawesome/free-brands-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useState, type JSX } from "react";
interface PlayProperties {
name: string;
userId: string;
android: string;
ios: string;
server?: string;
guild?: {
name: string;
id: string;
};
}
const copyToClipboard = (text: string): void => {
void navigator.clipboard.writeText(text);
};
/**
* Renders the view for a mobile game.
* @param properties - The game to render.
* @returns A JSX element.
*/
export const PlayComponent = (properties: PlayProperties): JSX.Element => {
const { name, android, ios, server, guild, userId } = properties;
const [ isFriendCodeCopied, setIsFriendCodeCopied ]
= useState<boolean>(false);
const [ isGuildIdCopied, setIsGuildIdCopied ]
= useState<boolean>(false);
const columns = guild
? "grid-cols-[2fr,1fr,200px]"
: "grid-cols-[2fr,200px]";
return (
<div className={`grid items-center gap-2.5 pb-10 w-full ${columns}`}>
<button
className="border-solid border-2 rounded-full p-2
text-[--background] bg-[--foreground]
hover:bg-[--background] hover:text-[--foreground]"
onClick={() => {
copyToClipboard(userId);
setIsFriendCodeCopied(true);
setTimeout(() => {
setIsFriendCodeCopied(false);
}, 2000);
}}
type="button"
>
<h2 className="text-2xl">{name}</h2>
<p>{`Friend Code: ${isFriendCodeCopied
? "Copied!"
: userId}`}</p>
{typeof server === "string"
? <p className="text-sm">{`Server: ${server}`}</p>
: null}
</button>
{guild
? <button
className="border-solid border-2 rounded-full p-2
text-[--background] bg-[--foreground]
hover:bg-[--background] hover:text-[--foreground]"
onClick={() => {
copyToClipboard(userId);
setIsGuildIdCopied(true);
setTimeout(() => {
setIsGuildIdCopied(false);
}, 2000);
}}
type="button"
>
<p className="text-2xl">{"Guild"}</p>
<p>{guild.name}</p>
<p className="text-sm">{`ID: ${isGuildIdCopied
? "Copied!"
: guild.id}`}</p>
</button>
: null}
<div className="flex">
<a
aria-label={`Play ${name} on Android`}
className="flex m-auto justify-between
items-center border-solid border-2 rounded-full p-2
bg-[#3DDC84] text-[#FFFFFF] hover:bg-[#FFFFFF] hover:text-[#3DDC84]"
href={android}
rel="noreferrer"
target="_blank"
>
<FontAwesomeIcon icon={faAndroid} size="3x" />
</a>
<a
aria-label={`Play ${name} on iOS`}
className="flex m-auto justify-between
items-center border-solid border-2 rounded-full p-2
bg-[#1ba7f8] text-[#FFFFFF] hover:bg-[#FFFFFF] hover:text-[#1ba7f8]"
href={ios}
rel="noreferrer"
target="_blank"
>
<FontAwesomeIcon icon={faAppStore} size="3x" />
</a>
</div>
</div>
);
};

View File

@ -22,6 +22,7 @@ export const NavItems = [
{ href: "/art", text: "Art" }, { href: "/art", text: "Art" },
{ href: "/manifesto", text: "Transfemme Manifesto" }, { href: "/manifesto", text: "Transfemme Manifesto" },
{ href: "/ask", text: "Ask Me Anything!" }, { href: "/ask", text: "Ask Me Anything!" },
{ href: "/play", text: "Play with Naomi" },
].sort((a, b) => { ].sort((a, b) => {
return a.text.localeCompare(b.text); return a.text.localeCompare(b.text);
}); });

87
src/config/Play.ts Normal file
View File

@ -0,0 +1,87 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export const Play: Array<{
name: string;
userId: string;
android: string;
ios: string;
server?: string;
guild?: {
name: string;
id: string;
};
}> = [
{
android: "https://play.google.com/store/apps/details?id=jp.pokemon.pokemontcgp",
ios: "https://apps.apple.com/us/app/pok%C3%A9mon-tcg-pocket/id6479970832",
name: "Pokémon TCG Pocket",
userId: "3382202283069817",
},
{
android: "https://play.google.com/store/apps/details?id=com.mujoysg.hxbb",
guild: {
id: "92 (Battle Zone 271)",
name: "NHCarrigan",
},
ios: "https://apps.apple.com/us/app/idle-angels-goddess-warfare/id1478505280",
name: "Idle Angels",
userId: "6c2c7ce4a60544a3aee8670d8dddf1ed",
},
{
android: "https://play.google.com/store/apps/details?id=com.proximabeta.nikke",
guild: {
id: "28325",
name: "NHC",
},
ios: "https://apps.apple.com/us/app/goddess-of-victory-nikke/id1585915174",
name: "Goddess of Victory: Nikke",
userId: "05362866",
},
{
android: "https://play.google.com/store/apps/details?id=com.yoozoo.jgame.us",
ios: "https://apps.apple.com/us/app/echocalypse-scarlet-covenant/id6446244975",
name: "Echocalypse: Scarlet Covenant",
server: "Aurora (3054310105)",
userId: "17754",
},
{
android: "https://play.google.com/store/apps/details?id=jp.pokemon.pokemonsleep",
ios: "https://apps.apple.com/us/app/pok%C3%A9mon-sleep/id1579464667",
name: "Pokémon Sleep",
userId: "9952-8565-4043",
},
{
android: "https://play.google.com/store/apps/details?id=com.goddessidle.global.android",
guild: {
id: "NHCarrigan",
name: "NHCarrigan",
},
ios: "https://apps.apple.com/us/app/goddess-era-2331-draws/id1626294447",
name: "Goddess Era",
server: "S971",
userId: "449",
},
{
android: "https://play.google.com/store/apps/details?id=com.nintendo.zaba",
ios: "https://apps.apple.com/us/app/fire-emblem-heroes/id1181774280",
name: "Fire Emblem: Heroes",
userId: "1305386686",
},
{
android: "https://play.google.com/store/apps/details?id=com.HoYoverse.hkrpgoversea",
ios: "https://apps.apple.com/us/app/honkai-star-rail/id1599719154",
name: "Honkai Star Rail",
server: "America",
userId: "620952550",
},
{
android: "https://play.google.com/store/apps/details?id=com.nintendo.zaca",
ios: "https://apps.apple.com/us/app/animal-crossing-pocket-camp/id1179915619",
name: "Animal Crossing: Pocket Camp",
userId: "3848 6071 011",
},
];

19
test/config/Play.spec.ts Normal file
View File

@ -0,0 +1,19 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { describe, it, expect } from "vitest";
import { Play } from "../../src/config/Play";
describe("play objects", () => {
it("should have unique names", () => {
expect.assertions(1);
const set = new Set(
Play.map((p) => {
return p.name;
}),
);
expect(set, "are not unique").toHaveLength(Play.length);
});
});