Add WaniKani

This commit is contained in:
Taevas 2023-11-01 21:15:29 +01:00
parent d2dfb621f4
commit ea09afc2d9
8 changed files with 248 additions and 9 deletions

BIN
bun.lockb

Binary file not shown.

View file

@ -1,7 +1,7 @@
import fetch from "node-fetch"
export async function api<T>(url: string): Promise<T> {
return fetch(url)
export async function api<T>(url: string, restful_token?: string): Promise<T> {
return (restful_token ? fetch(url, {headers: {"Authorization": `Bearer ${restful_token}`}}) : fetch(url))
.then(response => {
if (!response.ok) {
console.error(response.status, response.statusText)

View file

@ -0,0 +1,141 @@
import { Handler } from '@netlify/functions'
import { api } from "./shared/api"
import { WanikaniInfo } from '../../src/components/infos/Wanikani'
const handler: Handler = async (event, context) => {
let progression = await api<{
total_count: number
data: {
data: {
level: number,
unlocked_at: null | string,
completed_at: null | string
}
}[]
}>
("https://api.wanikani.com/v2/level_progressions", process.env["API_WANIKANI"])
let resets = await api<{
data: [{
data: {
created_at: string,
original_level: number,
target_level: number
}
}]
}>
("https://api.wanikani.com/v2/resets", process.env["API_WANIKANI"])
let summary = await api<{
data: {
lessons: [{
available_at: string
subject_ids: number[]
}],
reviews: [{
available_at: string
subject_ids: number[]
}],
next_reviews_at: null | string,
}
}>
("https://api.wanikani.com/v2/summary", process.env["API_WANIKANI"])
let subject_ids_lessons: number[] = []
let subject_ids_reviews: number[] = []
let subject_ids_all: number[] = []
for (let i = 0; i < summary.data.lessons.length; i++) {
for (let e = 0; e < summary.data.lessons[i].subject_ids.length; e++) {
subject_ids_lessons.push(summary.data.lessons[i].subject_ids[e])
}
}
for (let i = 0; i < summary.data.reviews.length; i++) {
for (let e = 0; e < summary.data.reviews[i].subject_ids.length; e++) {
subject_ids_reviews.push(summary.data.reviews[i].subject_ids[e])
}
}
subject_ids_all = subject_ids_lessons.concat(subject_ids_reviews)
let subjects = await api<{
data: {
id: number,
object: string,
data: {
characters: string,
document_url: string,
meanings: [{
meaning: string
}]
}
}[]
}>
(`https://api.wanikani.com/v2/subjects?ids=${subject_ids_all.toString()}`, process.env["API_WANIKANI"])
let lessons: {
available_at: Date,
type: string,
writing: string
meanings: [{
meaning: string
}],
url: string
}[] = []
for (let i = 0; i < subject_ids_lessons.length; i++) {
let summary_data = summary.data.lessons.find(lesson => lesson.subject_ids.includes(subject_ids_lessons[i]))
let subject = subjects.data.find(subject => subject.id === subject_ids_lessons[i])
if (!summary_data || !subject) {
console.error("Failed: ", summary_data, subject)
continue
}
lessons.push({
available_at: new Date(summary_data.available_at),
type: subject.object,
writing: subject.data.characters,
meanings: subject.data.meanings,
url: subject.data.document_url
})
}
let reviews: {
available_at: Date,
type: string,
writing: string
meanings: [{
meaning: string
}],
url: string
}[] = []
for (let i = 0; i < subject_ids_reviews.length; i++) {
let summary_data = summary.data.reviews.find(lesson => lesson.subject_ids.includes(subject_ids_reviews[i]))
let subject = subjects.data.find(subject => subject.id === subject_ids_reviews[i])
if (!summary_data || !subject) {
console.error("Failed: ", summary_data, subject)
continue
}
reviews.push({
available_at: new Date(summary_data.available_at),
type: subject.object,
writing: subject.data.characters,
meanings: subject.data.meanings,
url: subject.data.document_url
})
}
let info: WanikaniInfo = {
resets: resets.data,
lessons,
reviews
}
return {
statusCode: 200,
body: JSON.stringify(info)
}
}
export { handler }

View file

@ -14,6 +14,7 @@
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.6",
"@types/node": "^20.8.10",
"@types/react": "^18.2.31",
"@types/react-dom": "^18.2.14",
"@vitejs/plugin-react": "^4.1.0",

View file

@ -26,9 +26,8 @@ p {
.text-link {
background-color: #0a2e99;
color: #ffe88f;
color: white;
line-height: 20px;
padding: 0 5px;
}
.button_link {

View file

@ -5,6 +5,7 @@ import Hackthebox from "./infos/Hackthebox";
import Git from "./infos/Git";
import Osu from "./infos/Osu";
import Anilist from "./infos/Anilist";
import Wanikani from "./infos/Wanikani";
function Infos() {
return (
@ -16,6 +17,7 @@ function Infos() {
<Hackthebox/>
<Anilist/>
<Osu/>
{/* <Wanikani/> */}
</div>
)
}

View file

@ -0,0 +1,96 @@
import React, { useState, useEffect } from "react";
import Info from "./structure";
export type WanikaniInfo = {
resets: {
data: {
created_at: string,
original_level: number,
target_level: number
}
}[],
lessons: {
available_at: Date,
type: string,
writing: string
meanings: [{
meaning: string
}],
url: string
}[]
reviews: {
available_at: Date,
type: string,
writing: string
meanings: [{
meaning: string
}],
url: string
}[]
} | undefined
function Button(lesson: {
available_at: Date,
type: string,
writing: string
meanings: [{
meaning: string
}],
url: string
}) {
let colour = lesson.type === "radical" ? "bg-sky-400" : lesson.type === "kanji" ? "bg-pink-500" : "bg-fuchsia-700"
let title = `(${lesson.type}) ${lesson.meanings.map((m) => m.meaning).toString().replace(/,/g, ", ")}`
return (
<a href={lesson.url} target="_blank">
<button title={title} className={`m-1 p-2 ${colour} border-solid border-white border-2 rounded-md`}>
{lesson.writing}
</button></a>
)
}
export default function Wanikani() {
const [wanikani, setWanikani]: [WanikaniInfo, React.Dispatch<React.SetStateAction<WanikaniInfo>>] = useState()
const getWanikani = async () => {
const response = await fetch("/.netlify/functions/wanikani").then(r => r.json())
setWanikani(response)
}
useEffect(() => {
getWanikani()
}, [])
if (wanikani === undefined) {
return <></>
}
let lessons: React.JSX.Element[] = []
for (let i = 0; i < Math.min(wanikani.lessons.length, 20); i++) {
lessons.push(Button(wanikani.lessons[i]))
}
let lessons_div = <div className="m-4 font-bold">
{...lessons}
</div>
let reviews: React.JSX.Element[] = []
for (let i = 0; i < Math.min(wanikani.reviews.length, 20); i++) {
reviews.push(Button(wanikani.reviews[i]))
}
let reviews_div = <div className="m-4 font-bold">
{...reviews}
</div>
return (
<Info
type="Japanese"
websites={[{
name: "Wanikani",
link: "https://www.wanikani.com/users/Taevas",
elements: [
lessons_div,
reviews_div,
]
}]}
/>
)
}

View file

@ -18,13 +18,13 @@ function Projects({
}
let elements = [(
<div className="inline-block m-4 text-white">
<div className="border-4 p-4 m-4 max-w-3xl text-center">
<div className="border-4 p-4 m-4 max-w-3xl text-center bg-blue-700 transition hover:scale-105 hover:shadow-[0px_0_400px_400px_rgba(0,0,0,0.3)]">
<iframe className="float-right m-4" src="https://itch.io/embed/2295061?border_width=5&amp;bg_color=1d0e11&amp;fg_color=ffffff&amp;link_color=32c400&amp;border_color=6c5129" width="560" height="175"><a href="https://tttaevas.itch.io/swordventure">SwordVenture by Taevas</a></iframe>
<p><b>SwordVenture</b> initially was <a className="text-link" href="https://github.com/RemiL-Nel/Clicker-game" target="_blank">a game made by a friend in React which I helped develop,</a> but I've made the choice months later to <b>recode it from scratch in Godot</b>, a proper game engine!</p>
<br/>
<p>This was my first experience in this engine, and development took me a little more than 100 hours, in the span of less than a month. While a little barebones, I'm still very satisfied with the result!</p>
</div>
<div className="border-4 p-4 m-4 max-w-3xl text-center">
<div className="border-4 p-4 m-4 max-w-3xl text-center bg-blue-700 transition hover:scale-105 hover:shadow-[0px_0_400px_400px_rgba(0,0,0,0.3)]">
<a href="https://kanaguessr.taevas.xyz" target="_blank"><img className="m-4 float-left h-32" src="https://kanaguessr.taevas.xyz/favicon.png" alt="Kanaguessr logo"/></a>
<p>Working on kanaguessr is one of the first things I've done in 2021, and I essentially made it better in every aspect in early 2023.</p>
<br/>
@ -32,14 +32,14 @@ function Projects({
<br/>
<p>It helped friends with remembering katakanas, so I'm glad I took the time to make and polish this webpage!</p>
</div>
<div className="border-4 p-4 m-4 max-w-3xl text-center">
<div className="border-4 p-4 m-4 max-w-3xl text-center bg-blue-700 transition hover:scale-105 hover:shadow-[0px_0_400px_400px_rgba(0,0,0,0.3)]">
<p>Still in early 2023, I've made <a className="text-link" href="https://github.com/TTTaevas/osu-api-v1-js" target="_blank">osu-api-v1-js</a>, my first JavaScript (TypeScript) package!</p>
<br/>
<p>I've been using the first version of osu!'s API in several ways for years at this point, and yet I've never been using any package to make my life easier, I remember simply using axios directly and copypasting code between some of my projects. I never felt like using third party software to use such a simple API.</p>
<br/>
<p>Yet it's not really great to keep writing the same code over and over again, so I used this excuse to finally try my hand at writing packages! I honestly think the result is great, it fully covers the API, is fully documented, and even makes things more intuitive and consistent, I literally don't know how I could make it better!</p>
</div>
<div className="border-4 p-4 m-4 max-w-3xl text-center">
<div className="border-4 p-4 m-4 max-w-3xl text-center bg-blue-700 transition hover:scale-105 hover:shadow-[0px_0_400px_400px_rgba(0,0,0,0.3)]">
<a href="https://finder.taevas.xyz" target="_blank"><img className="m-4 float-right h-16" src="https://finder.taevas.xyz/Webpage/favicon.png" alt="Website-Finder logo"/></a>
<p>...Website-Finder is actually an odd one. What started off in 2020 as <a className="text-link" href="https://gitlab.com/Isterix/rif2" target="_blank">a simple Ruby script that downloads images from <i>every website it can find</i>,</a> became a way for me to experiment with different programming languages and their networking capabilities, without dependencies.</p>
<br/>