Load an Info's Website separately from the Info itself

Makes it so:
- An `Info`'s `Website` doesn't need to wait for other `Website`s of that same `Info` to load to show up
- An `Info`s `Website` not working won't prevent other `Website`s of that same `Info` from showing up
- Code is more split and organized

Furthermore, the token for the osu! API is now stored, and used for ALL osu! requests for 24 hours instead of being revoked

Overall, it's a lot of future-proofing so things on working even if I'm no longer there to maintain them
Also so `Info`s can be added, changed, and removed more easily
This commit is contained in:
Taevas 2024-05-04 19:14:18 +02:00
parent dec30acf14
commit 719672ffa0
45 changed files with 1125 additions and 759 deletions

BIN
bun.lockb

Binary file not shown.

View file

@ -32,7 +32,8 @@ export default [
"@typescript-eslint/quotes": ["error", "double"],
indent: "off",
"@typescript-eslint/indent": ["error", 2],
"@typescript-eslint/no-unsafe-assignment": "off"
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/dot-notation": "off",
},
},
{

View file

@ -1,6 +1,6 @@
import {type Handler} from "@netlify/functions";
import fetch from "node-fetch";
import {type AnilistInfo} from "../../src/components/Info/Anilist.js";
import {type AnilistInfo} from "../../src/components/Info/Anime/Anilist.js";
const handler: Handler = async () => {
const anilist = await fetch("https://graphql.anilist.co", {

View file

@ -1,6 +1,6 @@
import {type Handler} from "@netlify/functions";
import {Octokit} from "@octokit/rest";
import {type GithubInfo} from "../../src/components/Info/Git.js";
import {type GithubInfo} from "../../src/components/Info/Coding/GitHub.js";
const handler: Handler = async () => {
const octokit = new Octokit({auth: process.env.API_GITHUB});

View file

@ -1,6 +1,6 @@
import {type Handler} from "@netlify/functions";
import fetch from "node-fetch";
import {type GitlabInfo} from "../../src/components/Info/Git.js";
import {type GitlabInfo} from "../../src/components/Info/Coding/GitLab.js";
const handler: Handler = async () => {
const gitlab = await fetch("https://gitlab.com/api/v4/events?action=pushed", {

View file

@ -1,6 +1,6 @@
import {type Handler} from "@netlify/functions";
import {api} from "./shared/api.js";
import {type HacktheboxInfo} from "../../src/components/Info/Hackthebox.js";
import {type HacktheboxInfo} from "../../src/components/Info/Hacking.js";
const handler: Handler = async () => {
const hackthebox: {profile: {activity: HacktheboxInfo[]}} = await api<{

View file

@ -1,6 +1,6 @@
import {type Handler} from "@netlify/functions";
import {api} from "./shared/api.js";
import {type LastfmInfo} from "../../src/components/Info/Lastfm.js";
import {type LastfmInfo} from "../../src/components/Info/Music/Lastfm.js";
const handler: Handler = async () => {
const lastfm = await api<{

View file

@ -1,45 +0,0 @@
import {type Handler} from "@netlify/functions";
import * as osu from "osu-api-v2-js";
import {type OsuInfo} from "../../src/components/Info/Osu.js";
const handler: Handler = async () => {
const api = await osu.API.createAsync({id: 11451, secret: process.env.API_OSU!});
const profile = await Promise.all([
new Promise((resolve) => {
resolve(api.getUser(7276846, osu.Ruleset.osu));
}),
new Promise((resolve) => {
resolve(api.getUser(7276846, osu.Ruleset.taiko));
}),
new Promise((resolve) => {
resolve(api.getUser(7276846, osu.Ruleset.fruits));
}),
new Promise((resolve) => {
resolve(api.getUser(7276846, osu.Ruleset.mania));
}),
]) as osu.User.Extended[];
void api.revokeToken();
const info: OsuInfo = {
country: (profile[0]).country.name ?? "Unknown",
};
for (const ruleset of profile) {
if (ruleset.rank_history) {
const stats = ruleset.statistics;
info[ruleset.rank_history.mode] = {
global: stats.global_rank ?? 0,
country: stats.country_rank ?? 0,
};
}
}
return {
statusCode: 200,
body: JSON.stringify(info),
};
};
export {handler};

View file

@ -0,0 +1,24 @@
import {type Handler} from "@netlify/functions";
import * as osu from "osu-api-v2-js";
import {type FruitsInfo} from "../../src/components/Info/RhythmGames/OsuFruits.js";
const handler: Handler = async () => {
// eslint-disable-next-line @typescript-eslint/naming-convention
const api = new osu.API({access_token: process.env["OSU_TOKEN"]});
const profile = await api.getUser(7276846, osu.Ruleset.fruits);
const info: FruitsInfo = {
country: profile.country.name,
ranks: {
global: profile.statistics.global_rank ?? 0,
country: profile.statistics.country_rank ?? 0,
},
};
return {
statusCode: 200,
body: JSON.stringify(info),
};
};
export {handler};

View file

@ -0,0 +1,24 @@
import {type Handler} from "@netlify/functions";
import * as osu from "osu-api-v2-js";
import {type ManiaInfo} from "../../src/components/Info/RhythmGames/OsuMania.js";
const handler: Handler = async () => {
// eslint-disable-next-line @typescript-eslint/naming-convention
const api = new osu.API({access_token: process.env["OSU_TOKEN"]});
const profile = await api.getUser(7276846, osu.Ruleset.mania);
const info: ManiaInfo = {
country: profile.country.name,
ranks: {
global: profile.statistics.global_rank ?? 0,
country: profile.statistics.country_rank ?? 0,
},
};
return {
statusCode: 200,
body: JSON.stringify(info),
};
};
export {handler};

View file

@ -0,0 +1,24 @@
import {type Handler} from "@netlify/functions";
import * as osu from "osu-api-v2-js";
import {type OsuInfo} from "../../src/components/Info/RhythmGames/Osu.js";
const handler: Handler = async () => {
// eslint-disable-next-line @typescript-eslint/naming-convention
const api = new osu.API({access_token: process.env["OSU_TOKEN"]});
const profile = await api.getUser(7276846, osu.Ruleset.osu);
const info: OsuInfo = {
country: profile.country.name,
ranks: {
global: profile.statistics.global_rank ?? 0,
country: profile.statistics.country_rank ?? 0,
},
};
return {
statusCode: 200,
body: JSON.stringify(info),
};
};
export {handler};

View file

@ -0,0 +1,24 @@
import {type Handler} from "@netlify/functions";
import * as osu from "osu-api-v2-js";
import {type TaikoInfo} from "../../src/components/Info/RhythmGames/OsuTaiko.js";
const handler: Handler = async () => {
// eslint-disable-next-line @typescript-eslint/naming-convention
const api = new osu.API({access_token: process.env["OSU_TOKEN"]});
const profile = await api.getUser(7276846, osu.Ruleset.taiko);
const info: TaikoInfo = {
country: profile.country.name,
ranks: {
global: profile.statistics.global_rank ?? 0,
country: profile.statistics.country_rank ?? 0,
},
};
return {
statusCode: 200,
body: JSON.stringify(info),
};
};
export {handler};

View file

@ -0,0 +1,31 @@
import {type Handler} from "@netlify/functions";
import {API} from "osu-api-v2-js";
const handler: Handler = async () => {
const [token, expiration] = [process.env["OSU_TOKEN"], process.env["OSU_TOKEN_EXPIRATION"]];
let expired = false;
if (expiration) {
try {
expired = new Date(expiration) < new Date();
} catch {
expired = true;
}
} else {
expired = true;
}
if (!token || expired) {
console.log("Setting a new token for osu!...");
const api = await API.createAsync({id: 11451, secret: process.env.API_OSU!});
process.env["OSU_TOKEN"] = api.access_token;
process.env["OSU_TOKEN_EXPIRATION"] = api.expires.toISOString();
console.log("Successfully set a new token for osu!");
}
return {
statusCode: 200,
};
};
export {handler};

View file

@ -1,6 +1,6 @@
import {type Handler} from "@netlify/functions";
import {api} from "./shared/api.js";
import {type SpeedruncomInfo} from "../../src/components/Info/Speedruncom.js";
import {type SpeedruncomInfo} from "../../src/components/Info/Speedrunning/Speedruncom.js";
const handler: Handler = async () => {
// using the API's embedding would be stupid here, as that'd create lag due to irrelevant runs

View file

@ -1,6 +1,6 @@
import {type Handler} from "@netlify/functions";
import {api} from "./shared/api.js";
import {type WanikaniInfo} from "../../src/components/Info/Wanikani.js";
import {type WanikaniInfo} from "../../src/components/Info/Japanese/Wanikani.js";
type Subject = {
id: number;

View file

@ -19,8 +19,8 @@
"@eslint/js": "^9.1.1",
"@tailwindcss/forms": "^0.5.7",
"@types/node": "^20.10.0",
"@types/react": "^18.2.39",
"@types/react-dom": "^18.2.17",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": ">=7.0.2",
"@typescript-eslint/parser": ">=7.0.2",
"@vitejs/plugin-react": "^4.2.0",

View file

@ -1,59 +1,26 @@
import React, {useState} from "react";
import AnimateHeight, {type Height} from "react-animate-height";
import React, {Component} from "react";
export default function Info({
type,
websites,
error,
}: {
export default class Info extends Component<{
type: string;
websites: Array<{
name: string;
link: string;
elements: React.JSX.Element[];
}>;
websites: React.JSX.Element[];
error?: boolean;
}) {
const [height, setHeight] = useState<Height>(3);
const sections = websites.map((w, i) => {
setTimeout(() => { // somehow necessary to not always rerender
setHeight("auto");
}, 0);
}> {
render() {
return (
<AnimateHeight
key={w.name}
id={w.name.toLowerCase().match(/[a-z]/g)!.join().replace(/,/g, "")}
delay={150 * i}
duration={150 * (i + 1)}
height={height}
>
<a href={w.link} target="_blank" rel="noreferrer">
<h2 className="uppercase text-right font-bold pr-1 bg-white text-red-500">
{w.name}
</h2>
</a>
<div className="info p-3 m-auto">
{w.elements}
</div>
</AnimateHeight>
);
});
return (
<div className="m-5 flex w-80 border-l-3 border-r-3 border-b-3 border-white border-solid" id={type.toLowerCase()}>
<div className="m-5 flex w-80 border-l-3 border-r-3 border-b-3 border-white border-solid" id={this.props.type.toLowerCase()}>
<h2 className={`[text-orientation:upright] [writing-mode:vertical-rl]
uppercase text-start text-2xl tracking-[-.1em] font-bold pt-2
border-r-3 border-t-3 border-white border-solid
${!error ? "bg-sky-800" : "bg-purple-800"}`}>
{type}
${!this.props.error ? "bg-sky-800" : "bg-purple-800"}`}>
{this.props.type}
</h2>
{
!error ?
sections.length ?
!this.props.error ?
this.props.websites.length ?
<div className="w-80 bg-gradient-to-r from-sky-900 to-indigo-900">
{sections}
{this.props.websites.map((website) => {
return <>{website}</>;
})}
</div> :
<div className="flex w-80 bg-gradient-to-r from-sky-900 to-indigo-900 border-t-3">
<div className="animate-pulse h-min m-auto">
@ -71,4 +38,5 @@ export default function Info({
}
</div>
);
}
}

View file

@ -1,74 +0,0 @@
import React, {useState, useEffect} from "react";
import Info from "../Info.js";
import {handleError} from "./shared/handleError.js";
export type AnilistInfo = {
title: string;
episodes: {
watched: number;
total: number;
};
score: number;
startDate: string;
updateDate: string;
endDate: string;
cover: string;
url: string;
} | undefined;
export default function Anilist() {
const [anilist, setAnilist]: [AnilistInfo, React.Dispatch<React.SetStateAction<AnilistInfo>>] = useState();
const [error, setError] = useState(false);
const getAnilist = async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
setAnilist(await fetch("/.netlify/functions/anilist").then(async r => r.json()));
};
useEffect(() => {
getAnilist().catch(() => {
setError(true);
});
}, []);
if (!anilist) {
return handleError("Anime", error);
}
try {
return (
<Info
type="Anime"
websites={[{
name: "Anilist",
link: "https://anilist.co/user/Taevas/",
elements: [
<div key={"data"} className="flex mb-4">
<img className="m-auto w-16 h-22" alt="anime cover" src={anilist.cover} />
<div className="m-auto pl-2">
<p className="font-bold">{anilist.title}</p>
<p className="mt-4">Started: <strong>{anilist.startDate}</strong></p>
{
anilist.episodes.watched >= anilist.episodes.total ?
<p>Finished: <strong>{anilist.endDate}</strong></p> :
<p>Ep. {anilist.episodes.watched}: <strong>{anilist.updateDate}</strong></p>
}
</div>
</div>,
<>
{
anilist.episodes.watched >= anilist.episodes.total ?
<p>I gave it a <strong>{anilist.score}/10</strong></p> :
<p><strong>{anilist.episodes.watched}/{anilist.episodes.total}</strong> episodes watched</p>
}
</>,
<a key={"more"} className="button-link" href={anilist.url} target="_blank" rel="noreferrer">Anime Link</a>,
],
}]}
/>
);
} catch (e) {
return handleError("Anime", true, e);
}
}

View file

@ -0,0 +1,16 @@
import React from "react";
import Info from "../Info.js";
import Anilist from "./Anime/Anilist.js";
export default function Anime() {
const anilist = <Anilist key={"Anilist"}/>;
return (
<Info
type="Anime"
websites={[
anilist,
]}
/>
);
}

View file

@ -0,0 +1,73 @@
import React, {useState, useEffect} from "react";
import Website from "../../Website.js";
export type AnilistInfo = {
title: string;
episodes: {
watched: number;
total: number;
};
score: number;
startDate: string;
updateDate: string;
endDate: string;
cover: string;
url: string;
} | undefined;
export default function Anilist() {
const [anilist, setAnilist]: [AnilistInfo, React.Dispatch<React.SetStateAction<AnilistInfo>>] = useState();
const [elements, setElements] = useState([] as React.JSX.Element[]);
const [error, setError] = useState(false);
const getAnilist = async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
setAnilist(await fetch("/.netlify/functions/anilist").then(async r => r.json()));
};
useEffect(() => {
getAnilist().catch(() => {
setError(true);
});
}, []);
useEffect(() => {
if (anilist) {
try {
setElements([
<div key={"data"} className="flex mb-4">
<img className="m-auto w-16 h-22" alt="anime cover" src={anilist.cover} />
<div className="m-auto pl-2">
<p className="font-bold">{anilist.title}</p>
<p className="mt-4">Started: <strong>{anilist.startDate}</strong></p>
{
anilist.episodes.watched >= anilist.episodes.total ?
<p>Finished: <strong>{anilist.endDate}</strong></p> :
<p>Ep. {anilist.episodes.watched}: <strong>{anilist.updateDate}</strong></p>
}
</div>
</div>,
<>
{
anilist.episodes.watched >= anilist.episodes.total ?
<p>I gave it a <strong>{anilist.score}/10</strong></p> :
<p><strong>{anilist.episodes.watched}/{anilist.episodes.total}</strong> episodes watched</p>
}
</>,
<a key={"more"} className="button-link" href={anilist.url} target="_blank" rel="noreferrer">Anime Link</a>,
]);
} catch {
setError(true);
}
}
}, [anilist]);
return (
<Website
name="Anilist"
link="https://anilist.co/user/Taevas/"
elements={elements}
error={error}
/>
);
}

View file

@ -0,0 +1,19 @@
import React from "react";
import Info from "../Info.js";
import GitHub from "./Coding/GitHub.js";
import GitLab from "./Coding/GitLab.js";
export default function Coding() {
const github = <GitHub key={"github"}/>;
const gitlab = <GitLab key={"gitlab"}/>;
return (
<Info
type="Coding"
websites={[
github,
gitlab,
]}
/>
);
}

View file

@ -0,0 +1,65 @@
import React, {useState, useEffect} from "react";
import Website from "../../Website.js";
export type GithubInfo = {
public?: {
repo: string;
date: string;
};
private?: {
date: string;
};
};
export default function GitHub() {
const [github, setGithub]: [GithubInfo, React.Dispatch<React.SetStateAction<GithubInfo>>] = useState({});
const [elements, setElements] = useState([] as React.JSX.Element[]);
const [error, setError] = useState(false);
const getGithub = async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
setGithub(await fetch("/.netlify/functions/github").then(async r => r.json()));
};
useEffect(() => {
getGithub().catch(() => {
setError(true);
});
}, []);
useEffect(() => {
if (github.private ?? github.public) {
try {
const elms: React.JSX.Element[] = [];
if (github.private) {
elms.push(
<p key={"github-date-private"} className={github.public ? "mb-2" : ""}>Latest <strong>private</strong> push: <strong>{github.private.date}</strong></p>,
);
}
if (github.public) {
elms.push(
<p key={"github-date-public"}>Latest <strong>public</strong> push: <strong>{github.public.date} on {github.public.repo}</strong></p>,
);
elms.push(
<a key={"github-link"} className="button-link" href={`https://github.com/${github.public.repo}`} target="_blank" rel="noreferrer">Repo Link</a>,
);
}
setElements(elms);
} catch {
setError(true);
}
}
}, [github]);
return (
<Website
name="GitHub"
link="https://github.com/TTTaevas"
elements={elements}
error={error}
/>
);
}

View file

@ -0,0 +1,44 @@
import React, {useState, useEffect} from "react";
import Website from "../../Website.js";
export type GitlabInfo = {
date: string;
} | undefined;
export default function GitLab() {
const [gitlab, setGitlab]: [GitlabInfo, React.Dispatch<React.SetStateAction<GitlabInfo>>] = useState();
const [elements, setElements] = useState([] as React.JSX.Element[]);
const [error, setError] = useState(false);
const getGitlab = async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
setGitlab(await fetch("/.netlify/functions/gitlab").then(async r => r.json()));
};
useEffect(() => {
getGitlab().catch(() => {
setError(true);
});
}, []);
useEffect(() => {
if (gitlab) {
try {
setElements([
<p key={"gitlab-date"}>Latest push: <strong>{gitlab.date}</strong></p>,
]);
} catch {
setError(true);
}
}
}, [gitlab]);
return (
<Website
name="GitLab"
link="https://gitlab.com/TTTaevas"
elements={elements}
error={error}
/>
);
}

View file

@ -1,112 +0,0 @@
import React, {useState, useEffect} from "react";
import Info from "../Info.js";
import {handleError} from "./shared/handleError.js";
export type GithubInfo = {
public?: {
repo: string;
date: string;
};
private?: {
date: string;
};
};
export type GitlabInfo = {
date: string;
} | undefined;
export default function Git() {
const [github, setGithub]: [GithubInfo, React.Dispatch<React.SetStateAction<GithubInfo>>] = useState({});
const [gitlab, setGitlab]: [GitlabInfo, React.Dispatch<React.SetStateAction<GitlabInfo>>] = useState();
const [error, setError] = useState(false);
const getGithub = async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
setGithub(await fetch("/.netlify/functions/github").then(async r => r.json()));
};
const getGitlab = async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
setGitlab(await fetch("/.netlify/functions/gitlab").then(async r => r.json()));
};
useEffect(() => {
let errors = 0;
const promises = [
getGithub().catch(() => {
errors++;
}),
getGitlab().catch(() => {
errors++;
}),
];
void Promise.all(promises).then(() => {
if (errors >= 1) {
setError(true);
}
});
}, []);
try {
const githubElements: React.JSX.Element[] = [];
if (github.private) {
githubElements.push(
<p key={"github-date-private"} className={github.public ? "mb-2" : ""}>Latest <strong>private</strong> push: <strong>{github.private.date}</strong></p>,
);
}
if (github.public) {
githubElements.push(
<p key={"github-date-public"}>Latest <strong>public</strong> push: <strong>{github.public.date} on {github.public.repo}</strong></p>,
);
githubElements.push(
<a key={"github-link"} className="button-link" href={`https://github.com/${github.public.repo}`} target="_blank" rel="noreferrer">Repo Link</a>,
);
}
const gitlabElements: React.JSX.Element[] = [];
if (gitlab) {
gitlabElements.push(<p key={"gitlab-date"}>Latest push: <strong>{gitlab.date}</strong></p>);
}
const websites: Array<{
name: string;
link: string;
elements: React.JSX.Element[];
}> = [];
if (githubElements.length) {
websites.push({
name: "GitHub",
link: "https://github.com/TTTaevas",
elements: githubElements,
});
}
if (gitlabElements.length) {
websites.push({
name: "GitLab",
link: "https://gitlab.com/TTTaevas",
elements: gitlabElements,
});
}
if (websites.length < 2) {
return handleError("Coding", error);
}
return (
<Info
type="Coding"
websites={websites}
/>
);
} catch (e) {
return handleError("Coding", true, e);
}
}

View file

@ -0,0 +1,16 @@
import React from "react";
import Info from "../Info.js";
import Hackthebox from "./Hacking/Hackthebox.js";
export default function Hacking() {
const hackthebox = <Hackthebox key={"hackthebox"}/>;
return (
<Info
type="Hacking"
websites={[
hackthebox,
]}
/>
);
}

View file

@ -0,0 +1,65 @@
import React, {useState, useEffect} from "react";
import Website from "../../Website.js";
export type HacktheboxInfo = {
id: string;
date_diff: string;
date: string;
object_type: string;
type: string;
name: string;
machine_avatar: string;
} | undefined;
export default function Hackthebox() {
const [hackthebox, setHackthebox]: [HacktheboxInfo, React.Dispatch<React.SetStateAction<HacktheboxInfo>>] = useState();
const [elements, setElements] = useState([] as React.JSX.Element[]);
const [error, setError] = useState(false);
const getHackthebox = async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
setHackthebox(await fetch("/.netlify/functions/hackthebox").then(async r => r.json()));
};
useEffect(() => {
getHackthebox().catch(() => {
setError(true);
});
}, []);
useEffect(() => {
if (hackthebox) {
try {
setElements([
<div key={"data"} className="flex">
{
hackthebox.type === "user" ?
<img className="m-auto h-16 w-16" alt="machine thumbnail" src={hackthebox.machine_avatar}/> :
<a className="m-auto h-16 w-16" href={`https://www.hackthebox.com/achievement/machine/1063999/${hackthebox.id}`} target="_blank" rel="noreferrer">
<img alt="machine thumbnail" src={hackthebox.machine_avatar}/>
</a>
}
<div className="m-auto pl-4">
<p className="font-bold">{hackthebox.name}</p>
<p>({hackthebox.type})</p>
</div>
</div>,
<p key={"date"} className="mt-2 font-bold">{hackthebox.date}</p>,
<a key={"more"} className="button-link" href={`https://app.hackthebox.com/machines/${hackthebox.name}`}>Machine Link</a>,
]);
} catch {
setError(true);
}
}
}, [hackthebox]);
return (
<Website
name="Hacking"
link="https://app.hackthebox.com/profile/1063999"
elements={elements}
error={error}
/>
);
}

View file

@ -1,65 +0,0 @@
import React, {useState, useEffect} from "react";
import Info from "../Info.js";
import {handleError} from "./shared/handleError.js";
export type HacktheboxInfo = {
id: string;
date_diff: string;
date: string;
object_type: string;
type: string;
name: string;
machine_avatar: string;
} | undefined;
export default function Hackthebox() {
const [hackthebox, setHackthebox]: [HacktheboxInfo, React.Dispatch<React.SetStateAction<HacktheboxInfo>>] = useState();
const [error, setError] = useState(false);
const getHackthebox = async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
setHackthebox(await fetch("/.netlify/functions/hackthebox").then(async r => r.json()));
};
useEffect(() => {
getHackthebox().catch(() => {
setError(true);
});
}, []);
if (!hackthebox) {
return handleError("Hacking", error);
}
try {
return (
<Info
type="Hacking"
websites={[{
name: "HackTheBox",
link: "https://app.hackthebox.com/profile/1063999",
elements: [
<div key={"data"} className="flex">
{
hackthebox.type === "user" ?
<img className="m-auto h-16 w-16" alt="machine thumbnail" src={hackthebox.machine_avatar}/> :
<a className="m-auto h-16 w-16" href={`https://www.hackthebox.com/achievement/machine/1063999/${hackthebox.id}`} target="_blank" rel="noreferrer">
<img alt="machine thumbnail" src={hackthebox.machine_avatar}/>
</a>
}
<div className="m-auto pl-4">
<p className="font-bold">{hackthebox.name}</p>
<p>({hackthebox.type})</p>
</div>
</div>,
<p key={"date"} className="mt-2 font-bold">{hackthebox.date}</p>,
<a key={"more"} className="button-link" href={`https://app.hackthebox.com/machines/${hackthebox.name}`}>Machine Link</a>,
],
}]}
/>
);
} catch (e) {
return handleError("Hacking", true, e);
}
}

View file

@ -0,0 +1,16 @@
import React from "react";
import Info from "../Info.js";
import Wanikani from "./Japanese/Wanikani.js";
export default function Japanese() {
const wanikani = <Wanikani key={"wanikani"}/>;
return (
<Info
type="Japanese"
websites={[
wanikani,
]}
/>
);
}

View file

@ -0,0 +1,141 @@
import React, {useState, useEffect} from "react";
import Website from "../../Website.js";
export type WanikaniInfo = {
progression: {
total_count: number;
data: Array<{
data: {
level: number;
unlocked_at: undefined | string;
completed_at: undefined | string;
abandoned_at: undefined | string;
};
}>;
};
resets: Array<{
data: {
created_at: string;
original_level: number;
target_level: number;
};
}>;
lessons: Item[];
reviews: Item[];
moreThingsToReviewAt: string | undefined;
} | undefined;
type Item = {
available_at: string;
type: string;
writing: string;
meanings: Array<{
meaning: string;
}>;
url: string;
};
function Button(item: Item) {
const colour = item.type === "radical" ? "bg-sky-600" : item.type === "kanji" ? "bg-pink-500" : "bg-fuchsia-700";
const title = `(${item.type}) ${item.meanings.map((m) => m.meaning).toString().replace(/,/g, ", ")}`;
return (
<a href={item.url} target="_blank" rel="noreferrer">
<button title={title} className={`m-1 p-2 ${colour} border-solid border-white border-2 rounded-md`}>
{item.writing}
</button></a>
);
}
export default function Wanikani() {
const [wanikani, setWanikani]: [WanikaniInfo, React.Dispatch<React.SetStateAction<WanikaniInfo>>] = useState();
const [elements, setElements] = useState([] as React.JSX.Element[]);
const [error, setError] = useState(false);
const getWanikani = async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
setWanikani(await fetch("/.netlify/functions/wanikani").then(async r => r.json()));
};
useEffect(() => {
getWanikani().catch((e) => {
setError(true);
});
}, []);
useEffect(() => {
if (wanikani) {
try {
const now = new Date();
let level = <></>;
const unlockedLevels = wanikani.progression.data.filter(d => typeof d.data.unlocked_at === "string");
if (unlockedLevels.length) {
let arr = unlockedLevels.sort((a, b) => new Date(b.data.unlocked_at!).getTime() - new Date(a.data.unlocked_at!).getTime());
level = <p className="mb-4"><b>Level {arr[0].data.level}</b> reached!<br/>
<b>{new Date(arr[0].data.unlocked_at!).toISOString().substring(0, 10)}</b></p>;
}
let resets = <></>;
if (wanikani.resets.length) {
let allResets: React.JSX.Element[] = [];
for (const dataWrapper of wanikani.resets) {
const data = dataWrapper.data;
allResets.push(<p><b>{`${new Date(data.created_at).toISOString().substring(0, 10)}`}</b>{`: Reset my progress from level ${data.original_level} to level ${data.target_level}`}</p>);
}
resets = <div className="mb-4">{...allResets}</div>;
}
const lessons: React.JSX.Element[] = [];
const filteredLessons = wanikani.lessons.filter(lesson => new Date(lesson.available_at) < now);
for (const lesson of filteredLessons) {
lessons.push(Button(lesson));
}
const lessonsDiv = lessons.length ? <div className="mt-2 font-bold text-sm">
{...lessons}
</div> : <p>No lesson available for now!</p>;
const reviews: React.JSX.Element[] = [];
const filteredReviews = wanikani.reviews.filter(review => new Date(review.available_at) < now);
for (const review of filteredReviews) {
reviews.push(Button(review));
}
const reviewsDiv = reviews.length ? <div className="mt-2 font-bold text-sm">
{ ...reviews}
</div> : <p>No review available for now!</p>;
let whenNextToReview = <></>;
if (wanikani.moreThingsToReviewAt) {
const rtf = new Intl.RelativeTimeFormat("en", {style: "long", numeric: "always"});
const timeDifference = new Date(Math.abs(new Date(wanikani.moreThingsToReviewAt).getTime() - now.getTime()));
const howManyHours = (timeDifference.getUTCHours() + 1) + ((24 * (timeDifference.getUTCDate() - 1)) * (timeDifference.getUTCMonth() + 1));
whenNextToReview = <p className="mt-2">{`There will be more stuff to review ${rtf.format(howManyHours, "hour")}!`}</p>;
}
setElements([
resets,
level,
<p key={"lessons"} className="text-xl font-bold">Available lessons ({filteredLessons.length})</p>,
lessonsDiv,
<p key={"reviews"} className="mt-4 text-xl font-bold">Available reviews ({filteredReviews.length})</p>,
reviewsDiv,
whenNextToReview,
]);
} catch {
setError(true);
}
}
}, [wanikani]);
return (
<Website
name="Wanikani"
link="https://www.wanikani.com/users/Taevas"
elements={elements}
error={error}
/>
);
}

View file

@ -1,70 +0,0 @@
import React, {useState, useEffect} from "react";
import Info from "../Info.js";
import {handleError} from "./shared/handleError.js";
export type LastfmInfo = {
artist: string;
name: string;
album: string;
image: string;
listening: boolean;
url: string;
} | undefined;
export default function Lastfm() {
const [lastfm, setLastfm]: [LastfmInfo, React.Dispatch<React.SetStateAction<LastfmInfo>>] = useState();
const [error, setError] = useState(false);
const getLastfm = async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
setLastfm(await fetch("/.netlify/functions/lastfm").then(async r => r.json()));
};
const updateLastFm = () => {
getLastfm().catch(() => {
setError(true);
});
};
useEffect(() => {
updateLastFm();
const timer = setInterval(() => {
updateLastFm();
}, 2 * 60 * 1000);
return () => {
clearInterval(timer);
};
}, []);
if (!lastfm) {
return handleError("Music", error);
}
try {
return (
<Info
type="Music"
websites={[{
name: "Last.fm",
link: "https://www.last.fm/user/TTTaevas",
elements: [
<div key={"data"} className="flex">
<img alt="album thumbnail" src={lastfm.image} className="m-auto h-24 w-24" />
<div className="m-auto pl-4 w-fit">
<p className="mb-2 font-bold">{lastfm.artist}</p>
<p className="mt-2 font-bold">{lastfm.name}</p>
</div>
</div>,
<p key={"album"} className="mt-2 font-bold">{lastfm.album}</p>,
<p key={"status"} className="mt-2">{lastfm.listening ? "(Currently listening!)" : "(Last listened)"}</p>,
<a key={"more"} className="button-link" href={lastfm.url} target="_blank" rel="noreferrer">Music Details</a>,
],
}]}
/>
);
} catch (e) {
return handleError("Music", true, e);
}
}

View file

@ -0,0 +1,16 @@
import React from "react";
import Info from "../Info.js";
import Lastfm from "./Music/Lastfm.js";
export default function Anime() {
const lastfm = <Lastfm key={"Lastfm"}/>;
return (
<Info
type="Music"
websites={[
lastfm,
]}
/>
);
}

View file

@ -0,0 +1,69 @@
import React, {useState, useEffect} from "react";
import Website from "../../Website.js";
export type LastfmInfo = {
artist: string;
name: string;
album: string;
image: string;
listening: boolean;
url: string;
} | undefined;
export default function Lastfm() {
const [lastfm, setLastfm]: [LastfmInfo, React.Dispatch<React.SetStateAction<LastfmInfo>>] = useState();
const [elements, setElements] = useState([] as React.JSX.Element[]);
const [error, setError] = useState(false);
const getLastfm = async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
setLastfm(await fetch("/.netlify/functions/lastfm").then(async r => r.json()));
};
const updateLastFm = () => {
getLastfm().catch(() => {
setError(true);
});
};
useEffect(() => {
updateLastFm();
const timer = setInterval(() => {
updateLastFm();
}, 2 * 60 * 1000);
return () => {
clearInterval(timer);
};
}, []);
useEffect(() => {
if (lastfm) {
try {
setElements([
<div key={"data"} className="flex">
<img alt="album thumbnail" src={lastfm.image} className="m-auto h-24 w-24" />
<div className="m-auto pl-4 w-fit">
<p className="mb-2 font-bold">{lastfm.artist}</p>
<p className="mt-2 font-bold">{lastfm.name}</p>
</div>
</div>,
<p key={"album"} className="mt-2 font-bold">{lastfm.album}</p>,
<p key={"status"} className="mt-2">{lastfm.listening ? "(Currently listening!)" : "(Last listened)"}</p>,
<a key={"more"} className="button-link" href={lastfm.url} target="_blank" rel="noreferrer">Music Details</a>,
]);
} catch {
setError(true);
}
}
}, [lastfm]);
return (
<Website
name="Last.fm"
link="https://www.last.fm/user/TTTaevas"
elements={elements}
error={error}
/>
);
}

View file

@ -1,85 +0,0 @@
import React, {useState, useEffect} from "react";
import Info from "../Info.js";
import {handleError} from "./shared/handleError.js";
import "../../style/infos/osu.css";
export type OsuInfo = {
country: string;
osu?: {global: number; country: number};
taiko?: {global: number; country: number};
fruits?: {global: number; country: number};
mania?: {global: number; country: number};
};
export default function Osu() {
const [osu, setOsu]: [OsuInfo, React.Dispatch<React.SetStateAction<OsuInfo>>] = useState({country: "Unknown"});
const [error, setError] = useState(false);
const getOsu = async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
setOsu(await fetch("/.netlify/functions/osu").then(async r => r.json()));
};
useEffect(() => {
getOsu().catch(() => {
setError(true);
});
}, []);
const generateWebsite = (name: string, interalName: string, data: {global: number; country: number} | undefined) => {
const website = {
name,
link: `https://osu.ppy.sh/users/7276846/${interalName}`,
elements: [] as React.JSX.Element[],
};
if (data) {
website.elements.push(
<div key={interalName} className="flex">
<img className="m-auto w-16 h-16" alt={`${name} mode logo`} src={`/mode-${interalName}.png`} />
<div className="m-auto">
<p>Global: <strong>#{data.global}</strong></p>
<p>{osu.country}: <strong>#{data.country}</strong></p>
</div>
</div>,
);
}
return website;
};
try {
const osuWebsite = generateWebsite("osu!", "osu", osu.osu);
const taikoWebsite = generateWebsite("osu!taiko", "taiko", osu.taiko);
const catchWebsite = generateWebsite("osu!catch", "fruits", osu.fruits);
const maniaWebsite = generateWebsite("osu!mania", "mania", osu.mania);
const websites = [
osuWebsite,
taikoWebsite,
catchWebsite,
maniaWebsite,
];
for (const website of websites) {
if (!website.elements.length) {
return handleError("Rhythm games", error);
}
}
return (
<Info
type="Rhythm games"
websites={[
osuWebsite,
taikoWebsite,
catchWebsite,
maniaWebsite,
]}
/>
);
} catch (e) {
return handleError("Rhythm games", true, e);
}
}

View file

@ -0,0 +1,49 @@
import React, {useEffect, useState} from "react";
import Info from "../Info.js";
import Osu from "./RhythmGames/Osu.js";
import Taiko from "./RhythmGames/OsuTaiko.js";
import Fruits from "./RhythmGames/OsuFruits.js";
import Mania from "./RhythmGames/OsuMania.js";
import "../../style/infos/osu.css";
export default function RhythmGames() {
const [token, setToken] = useState(false);
const [websites, setWebsites] = useState([] as React.JSX.Element[]);
const [error, setError] = useState(false);
const getToken = async () => {
await fetch("/.netlify/functions/osu_token").then((r) => {
if (r.ok) {
setToken(true);
} else {
setError(true);
}
});
};
useEffect(() => {
getToken().catch(() => {
setError(true);
});
}, []);
useEffect(() => {
console.log("token is set?", token);
if (token) {
const osu = <Osu key={"osu"}/>;
const taiko = <Taiko key={"taiko"}/>;
const fruits = <Fruits key={"fruits"}/>;
const mania = <Mania key={"mania"}/>;
setWebsites([osu, taiko, fruits, mania]);
}
}, [token]);
return (
<Info
type="Rhythm Games"
websites={websites}
error={error}
/>
);
}

View file

@ -0,0 +1,54 @@
import React, {useState, useEffect} from "react";
import Website from "../../Website.js";
export type OsuInfo = {
country: string;
ranks: {
global: number;
country: number;
};
} | undefined;
export default function Osu() {
const [osu, setOsu]: [OsuInfo, React.Dispatch<React.SetStateAction<OsuInfo>>] = useState();
const [elements, setElements] = useState([] as React.JSX.Element[]);
const [error, setError] = useState(false);
const getOsu = async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
setOsu(await fetch("/.netlify/functions/osu_osu").then(async r => r.json()));
};
useEffect(() => {
getOsu().catch(() => {
setError(true);
});
}, []);
useEffect(() => {
if (osu) {
try {
setElements([
<div key={"osu"} className="flex">
<img className="m-auto w-16 h-16" alt="osu mode logo" src="/mode-osu.png"/>
<div className="m-auto">
<p>Global: <strong>#{osu.ranks.global}</strong></p>
<p>{osu.country}: <strong>#{osu.ranks.country}</strong></p>
</div>
</div>,
]);
} catch {
setError(true);
}
}
}, [osu]);
return (
<Website
name="osu!"
link="https://osu.ppy.sh/users/7276846/osu"
elements={elements}
error={error}
/>
);
}

View file

@ -0,0 +1,54 @@
import React, {useState, useEffect} from "react";
import Website from "../../Website.js";
export type FruitsInfo = {
country: string;
ranks: {
global: number;
country: number;
};
} | undefined;
export default function Fruits() {
const [fruits, setFruits]: [FruitsInfo, React.Dispatch<React.SetStateAction<FruitsInfo>>] = useState();
const [elements, setElements] = useState([] as React.JSX.Element[]);
const [error, setError] = useState(false);
const getFruits = async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
setFruits(await fetch("/.netlify/functions/osu_fruits").then(async r => r.json()));
};
useEffect(() => {
getFruits().catch(() => {
setError(true);
});
}, []);
useEffect(() => {
if (fruits) {
try {
setElements([
<div key={"fruits"} className="flex">
<img className="m-auto w-16 h-16" alt="fruits mode logo" src="/mode-fruits.png"/>
<div className="m-auto">
<p>Global: <strong>#{fruits.ranks.global}</strong></p>
<p>{fruits.country}: <strong>#{fruits.ranks.country}</strong></p>
</div>
</div>,
]);
} catch {
setError(true);
}
}
}, [fruits]);
return (
<Website
name="osu!catch"
link="https://osu.ppy.sh/users/7276846/fruits"
elements={elements}
error={error}
/>
);
}

View file

@ -0,0 +1,54 @@
import React, {useState, useEffect} from "react";
import Website from "../../Website.js";
export type ManiaInfo = {
country: string;
ranks: {
global: number;
country: number;
};
} | undefined;
export default function Mania() {
const [mania, setMania]: [ManiaInfo, React.Dispatch<React.SetStateAction<ManiaInfo>>] = useState();
const [elements, setElements] = useState([] as React.JSX.Element[]);
const [error, setError] = useState(false);
const getMania = async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
setMania(await fetch("/.netlify/functions/osu_mania").then(async r => r.json()));
};
useEffect(() => {
getMania().catch(() => {
setError(true);
});
}, []);
useEffect(() => {
if (mania) {
try {
setElements([
<div key={"mania"} className="flex">
<img className="m-auto w-16 h-16" alt="mania mode logo" src="/mode-mania.png"/>
<div className="m-auto">
<p>Global: <strong>#{mania.ranks.global}</strong></p>
<p>{mania.country}: <strong>#{mania.ranks.country}</strong></p>
</div>
</div>,
]);
} catch {
setError(true);
}
}
}, [mania]);
return (
<Website
name="osu!mania"
link="https://osu.ppy.sh/users/7276846/mania"
elements={elements}
error={error}
/>
);
}

View file

@ -0,0 +1,54 @@
import React, {useState, useEffect} from "react";
import Website from "../../Website.js";
export type TaikoInfo = {
country: string;
ranks: {
global: number;
country: number;
};
} | undefined;
export default function Taiko() {
const [taiko, setTaiko]: [TaikoInfo, React.Dispatch<React.SetStateAction<TaikoInfo>>] = useState();
const [elements, setElements] = useState([] as React.JSX.Element[]);
const [error, setError] = useState(false);
const getTaiko = async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
setTaiko(await fetch("/.netlify/functions/osu_taiko").then(async r => r.json()));
};
useEffect(() => {
getTaiko().catch(() => {
setError(true);
});
}, []);
useEffect(() => {
if (taiko) {
try {
setElements([
<div key={"taiko"} className="flex">
<img className="m-auto w-16 h-16" alt="taiko mode logo" src="/mode-taiko.png"/>
<div className="m-auto">
<p>Global: <strong>#{taiko.ranks.global}</strong></p>
<p>{taiko.country}: <strong>#{taiko.ranks.country}</strong></p>
</div>
</div>,
]);
} catch {
setError(true);
}
}
}, [taiko]);
return (
<Website
name="osu!taiko"
link="https://osu.ppy.sh/users/7276846/taiko"
elements={elements}
error={error}
/>
);
}

View file

@ -0,0 +1,16 @@
import React from "react";
import Info from "../Info.js";
import Speedruncom from "./Speedrunning/Speedruncom.js";
export default function Speedrun() {
const speedruncom = <Speedruncom key={"speedruncom"}/>;
return (
<Info
type="Speedrun"
websites={[
speedruncom,
]}
/>
);
}

View file

@ -1,59 +0,0 @@
import React, {useState, useEffect} from "react";
import Info from "../Info.js";
import {handleError} from "./shared/handleError.js";
export type SpeedruncomInfo = {
place: number;
link: string;
date: string;
thumbnail: string;
game: string;
details: string[];
} | undefined;
export default function Speedruncom() {
const [speedruncom, setSpeedruncom]: [SpeedruncomInfo, React.Dispatch<React.SetStateAction<SpeedruncomInfo>>] = useState();
const [error, setError] = useState(false);
const getSpeedruncom = async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
setSpeedruncom(await fetch("/.netlify/functions/speedruncom").then(async r => r.json()));
};
useEffect(() => {
getSpeedruncom().catch(() => {
setError(true);
});
}, []);
if (!speedruncom) {
return handleError("Speedrun", error);
}
try {
return (
<Info
type="Speedrun"
websites={[{
name: "speedrun.com",
link: "https://www.speedrun.com/Taevas/",
elements: [
<div key={"data"} className="flex pb-2">
<img alt="game thumbnail" src={speedruncom.thumbnail} className="h-32 m-auto" />
<div className="m-auto pl-2">
<p className="mb-2">Placed <strong>#{speedruncom.place}</strong> on:</p>
<p className="font-bold">{speedruncom.game}</p>
{speedruncom.details.map((d, i) => <p key={`detail-${i}`}>{d}</p>)}
</div>
</div>,
<p key={"date"} className="mt-2 font-bold">{speedruncom.date}</p>,
<a key={"more"} className="button-link" href={speedruncom.link} target="_blank" rel="noreferrer">Run Details</a>,
],
}]}
/>
);
} catch (e) {
return handleError("Speedrun", error, e);
}
}

View file

@ -0,0 +1,58 @@
import React, {useState, useEffect} from "react";
import Website from "../../Website.js";
export type SpeedruncomInfo = {
place: number;
link: string;
date: string;
thumbnail: string;
game: string;
details: string[];
} | undefined;
export default function Speedruncom() {
const [speedruncom, setSpeedruncom]: [SpeedruncomInfo, React.Dispatch<React.SetStateAction<SpeedruncomInfo>>] = useState();
const [elements, setElements] = useState([] as React.JSX.Element[]);
const [error, setError] = useState(false);
const getSpeedruncom = async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
setSpeedruncom(await fetch("/.netlify/functions/speedruncom").then(async r => r.json()));
};
useEffect(() => {
getSpeedruncom().catch(() => {
setError(true);
});
}, []);
useEffect(() => {
if (speedruncom) {
try {
setElements([
<div key={"data"} className="flex pb-2">
<img alt="game thumbnail" src={speedruncom.thumbnail} className="h-32 m-auto" />
<div className="m-auto pl-2">
<p className="mb-2">Placed <strong>#{speedruncom.place}</strong> on:</p>
<p className="font-bold">{speedruncom.game}</p>
{speedruncom.details.map((d, i) => <p key={`detail-${i}`}>{d}</p>)}
</div>
</div>,
<p key={"date"} className="mt-2 font-bold">{speedruncom.date}</p>,
<a key={"more"} className="button-link" href={speedruncom.link} target="_blank" rel="noreferrer">Run Details</a>,
]);
} catch {
setError(true);
}
}
}, [speedruncom]);
return (
<Website
name="Speedrun.com"
link="https://www.speedrun.com/Taevas/"
elements={elements}
error={error}
/>
);
}

View file

@ -1,142 +0,0 @@
import React, {useState, useEffect} from "react";
import Info from "../Info.js";
import {handleError} from "./shared/handleError.js";
export type WanikaniInfo = {
progression: {
total_count: number;
data: Array<{
data: {
level: number;
unlocked_at: undefined | string;
completed_at: undefined | string;
abandoned_at: undefined | string;
};
}>;
};
resets: Array<{
data: {
created_at: string;
original_level: number;
target_level: number;
};
}>;
lessons: Item[];
reviews: Item[];
moreThingsToReviewAt: string | undefined;
} | undefined;
type Item = {
available_at: string;
type: string;
writing: string;
meanings: Array<{
meaning: string;
}>;
url: string;
};
function Button(item: Item) {
const colour = item.type === "radical" ? "bg-sky-600" : item.type === "kanji" ? "bg-pink-500" : "bg-fuchsia-700";
const title = `(${item.type}) ${item.meanings.map((m) => m.meaning).toString().replace(/,/g, ", ")}`;
return (
<a href={item.url} target="_blank" rel="noreferrer">
<button title={title} className={`m-1 p-2 ${colour} border-solid border-white border-2 rounded-md`}>
{item.writing}
</button></a>
);
}
export default function Wanikani() {
const [wanikani, setWanikani]: [WanikaniInfo, React.Dispatch<React.SetStateAction<WanikaniInfo>>] = useState();
const [error, setError] = useState(false);
const getWanikani = async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
setWanikani(await fetch("/.netlify/functions/wanikani").then(async r => r.json()));
};
useEffect(() => {
getWanikani().catch((e) => {
setError(true);
});
}, []);
if (!wanikani) {
return handleError("Japanese", error);
}
try {
const now = new Date();
let level = <></>;
const unlockedLevels = wanikani.progression.data.filter(d => typeof d.data.unlocked_at === "string");
if (unlockedLevels.length) {
let arr = unlockedLevels.sort((a, b) => new Date(b.data.unlocked_at!).getTime() - new Date(a.data.unlocked_at!).getTime());
level = <p className="mb-4"><b>Level {arr[0].data.level}</b> reached!<br/>
<b>{new Date(arr[0].data.unlocked_at!).toISOString().substring(0, 10)}</b></p>;
}
let resets = <></>;
if (wanikani.resets.length) {
let allResets: React.JSX.Element[] = [];
for (const dataWrapper of wanikani.resets) {
const data = dataWrapper.data;
allResets.push(<p><b>{`${new Date(data.created_at).toISOString().substring(0, 10)}`}</b>{`: Reset my progress from level ${data.original_level} to level ${data.target_level}`}</p>);
}
resets = <div className="mb-4">{...allResets}</div>;
}
const lessons: React.JSX.Element[] = [];
const filteredLessons = wanikani.lessons.filter(lesson => new Date(lesson.available_at) < now);
for (const lesson of filteredLessons) {
lessons.push(Button(lesson));
}
const lessonsDiv = lessons.length ? <div className="mt-2 font-bold text-sm">
{...lessons}
</div> : <p>No lesson available for now!</p>;
const reviews: React.JSX.Element[] = [];
const filteredReviews = wanikani.reviews.filter(review => new Date(review.available_at) < now);
for (const review of filteredReviews) {
reviews.push(Button(review));
}
const reviewsDiv = reviews.length ? <div className="mt-2 font-bold text-sm">
{ ...reviews}
</div> : <p>No review available for now!</p>;
let whenNextToReview = <></>;
if (wanikani.moreThingsToReviewAt) {
const rtf = new Intl.RelativeTimeFormat("en", {style: "long", numeric: "always"});
const timeDifference = new Date(Math.abs(new Date(wanikani.moreThingsToReviewAt).getTime() - now.getTime()));
const howManyHours = (timeDifference.getUTCHours() + 1) + ((24 * (timeDifference.getUTCDate() - 1)) * (timeDifference.getUTCMonth() + 1));
whenNextToReview = <p className="mt-2">{`There will be more stuff to review ${rtf.format(howManyHours, "hour")}!`}</p>;
}
return (
<Info
type="Japanese"
websites={[{
name: "Wanikani",
link: "https://www.wanikani.com/users/Taevas",
elements: [
resets,
level,
<p key={"lessons"} className="text-xl font-bold">Available lessons ({filteredLessons.length})</p>,
lessonsDiv,
<p key={"reviews"} className="mt-4 text-xl font-bold">Available reviews ({filteredReviews.length})</p>,
reviewsDiv,
whenNextToReview,
],
}]}
/>
);
} catch (e) {
return handleError("Japanese", true, e);
}
}

View file

@ -1,16 +0,0 @@
import React from "react";
import Info from "../../Info.js";
export function handleError(type: string, isError: boolean, seriousError?: unknown) {
if (seriousError) {
console.error("Something bad happened! ><\nPlease let me know about it!\n", seriousError, "\nSorry about that!!");
}
return (
<Info
type={type}
websites={[]}
error={isError}
/>
);
}

View file

@ -0,0 +1,59 @@
import React, {useEffect, useState} from "react";
import AnimateHeight, {type Height} from "react-animate-height";
export default function Website({
name,
link,
elements,
error,
}: {
name: string;
link: string;
elements: React.JSX.Element[];
error: boolean;
}) {
const [height, setHeight] = useState<Height>(15);
const state = elements.length ? 1 : error ? 2 : 0;
useEffect(() => {
if (elements.length) {
setHeight("auto");
}
}, [elements]);
return (
<div
key={name}
id={name.toLowerCase().match(/[a-z]/g)!.join().replace(/,/g, "")}
>
<a href={link} target="_blank" rel="noreferrer">
<h2 className="uppercase text-right font-bold pr-1 bg-white text-red-500">
{name}
</h2>
</a>
<div className={`info p-3 m-auto bg-gradient-to-r
${state !== 2 ? "from-sky-900 to-indigo-900" : "from-purple-900 to-pink-900"}
`}>
{
state === 1 ?
<AnimateHeight
duration={150}
height={height}
>
{elements}
</AnimateHeight> :
state === 2 ?
<div>
<img className="w-16 mb-2 m-auto" src="/cds/misuse--outline.svg"/>
<p className="mx-4">Something went wrong... {"><"}</p>
<p className="mx-4 mb-2">Please contact me and let me know about it!</p>
</div> :
<div className="animate-pulse h-min m-auto">
{/* <div className="animate-spin h-16 w-16 mx-auto mb-2 border-8 border-sky-600 border-r-gray-200 rounded-full"/> */}
<p className="mx-4">Loading...</p>
</div>
}
</div>
</div>
);
}

View file

@ -1,23 +1,23 @@
import React from "react";
import Lastfm from "../components/Info/Lastfm.js";
import Speedruncom from "../components/Info/Speedruncom.js";
import Hackthebox from "../components/Info/Hackthebox.js";
import Git from "../components/Info/Git.js";
import Osu from "../components/Info/Osu.js";
import Anilist from "../components/Info/Anilist.js";
import Wanikani from "../components/Info/Wanikani.js";
import Music from "../components/Info/Music.js";
import Speedrun from "../components/Info/Speedrun.js";
import Hacking from "../components/Info/Hacking.js";
import Coding from "../components/Info/Coding.js";
import RhythmGames from "../components/Info/RhythmGames.js";
import Anime from "../components/Info/Anime.js";
import Japanese from "../components/Info/Japanese.js";
function Infos() {
return (
<div id="infos" className="hidden lg:inline-block text-white static m-auto lg:bg-gradient-to-r from-sky-600 to-indigo-600
border-solid border-white lg:border-l-3 h-screen lg:fixed lg:right-0 lg:overflow-y-auto">
<Lastfm/>
<Git/>
<Speedruncom/>
<Anilist/>
<Wanikani/>
<Osu/>
<Hackthebox/>
<Music/>
<Coding/>
<Speedrun/>
<Anime/>
<Japanese/>
<RhythmGames/>
<Hacking/>
</div>
);
}