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:
parent
dec30acf14
commit
719672ffa0
45 changed files with 1125 additions and 759 deletions
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
@ -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",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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", {
|
||||
|
|
|
@ -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});
|
||||
|
|
|
@ -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", {
|
||||
|
|
|
@ -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<{
|
||||
|
|
|
@ -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<{
|
||||
|
|
|
@ -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};
|
24
netlify/functions/osu_fruits.ts
Normal file
24
netlify/functions/osu_fruits.ts
Normal 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};
|
24
netlify/functions/osu_mania.ts
Normal file
24
netlify/functions/osu_mania.ts
Normal 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};
|
24
netlify/functions/osu_osu.ts
Normal file
24
netlify/functions/osu_osu.ts
Normal 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};
|
24
netlify/functions/osu_taiko.ts
Normal file
24
netlify/functions/osu_taiko.ts
Normal 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};
|
31
netlify/functions/osu_token.ts
Normal file
31
netlify/functions/osu_token.ts
Normal 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};
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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">
|
||||
|
@ -72,3 +39,4 @@ export default function Info({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
16
src/components/Info/Anime.tsx
Normal file
16
src/components/Info/Anime.tsx
Normal 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,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
73
src/components/Info/Anime/Anilist.tsx
Normal file
73
src/components/Info/Anime/Anilist.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
19
src/components/Info/Coding.tsx
Normal file
19
src/components/Info/Coding.tsx
Normal 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,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
65
src/components/Info/Coding/GitHub.tsx
Normal file
65
src/components/Info/Coding/GitHub.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
44
src/components/Info/Coding/GitLab.tsx
Normal file
44
src/components/Info/Coding/GitLab.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
16
src/components/Info/Hacking.tsx
Normal file
16
src/components/Info/Hacking.tsx
Normal 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,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
65
src/components/Info/Hacking/Hackthebox.tsx
Normal file
65
src/components/Info/Hacking/Hackthebox.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
16
src/components/Info/Japanese.tsx
Normal file
16
src/components/Info/Japanese.tsx
Normal 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,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
141
src/components/Info/Japanese/Wanikani.tsx
Normal file
141
src/components/Info/Japanese/Wanikani.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
16
src/components/Info/Music.tsx
Normal file
16
src/components/Info/Music.tsx
Normal 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,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
69
src/components/Info/Music/Lastfm.tsx
Normal file
69
src/components/Info/Music/Lastfm.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
49
src/components/Info/RhythmGames.tsx
Normal file
49
src/components/Info/RhythmGames.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
54
src/components/Info/RhythmGames/Osu.tsx
Normal file
54
src/components/Info/RhythmGames/Osu.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
54
src/components/Info/RhythmGames/OsuFruits.tsx
Normal file
54
src/components/Info/RhythmGames/OsuFruits.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
54
src/components/Info/RhythmGames/OsuMania.tsx
Normal file
54
src/components/Info/RhythmGames/OsuMania.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
54
src/components/Info/RhythmGames/OsuTaiko.tsx
Normal file
54
src/components/Info/RhythmGames/OsuTaiko.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
16
src/components/Info/Speedrun.tsx
Normal file
16
src/components/Info/Speedrun.tsx
Normal 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,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
58
src/components/Info/Speedrunning/Speedruncom.tsx
Normal file
58
src/components/Info/Speedrunning/Speedruncom.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
59
src/components/Website.tsx
Normal file
59
src/components/Website.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue