From ea09afc2d9ce22ebb51aa87f108c77e6515f3ff2 Mon Sep 17 00:00:00 2001 From: Taevas <67872932+TTTaevas@users.noreply.github.com> Date: Wed, 1 Nov 2023 21:15:29 +0100 Subject: [PATCH] Add WaniKani --- bun.lockb | Bin 81880 -> 82582 bytes netlify/functions/shared/api.ts | 4 +- netlify/functions/wanikani.ts | 141 ++++++++++++++++++++++++++++++ package.json | 1 + src/App.css | 5 +- src/components/Infos.tsx | 2 + src/components/infos/Wanikani.tsx | 96 ++++++++++++++++++++ src/components/tabs/Projects.tsx | 8 +- 8 files changed, 248 insertions(+), 9 deletions(-) create mode 100644 netlify/functions/wanikani.ts create mode 100644 src/components/infos/Wanikani.tsx diff --git a/bun.lockb b/bun.lockb index fde847b0e783aea478a525b6f1faf93707ba1295..e33959de5914efacb50971a5e5ee0c99ba0bf9dd 100755 GIT binary patch delta 6146 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 CgMViL delta 5603 zcmeI0dr(x@9mmgESYSbxH@aPo@-m{bJQf6sdUe(MiZ-3;jP{S(!Bs2tKMN)kd`@j&`e$=A>3oG6LjfY*iv1-kF zA~IvRqDtQnAogRjZceEnTeAVJE=uhq7Gd`c12s_*YbISc?jYaJ-C0SRvJ} zR1!26O8W#f720C???ft<4BKxFU}1DHJ`>7%X7!R~sG@4tRaRGT zQtAfc^WqRlK>-fn8ivggc0k!sgyp{(rBn*+RjAC4pFt%KY&J9lIvUf%c#@pdU+Tqy zW3~l0$9xp#sD0wH&(xkkrG2Y*JOL+rN|swuBhf^#y^FrU*yx5R`8a-|ZjoH4VZV=v zSMr<{I$xTchTbOqxc)_Q6AZ@#tb-42C@FN7YD1b54E>t)Cm6O%Ho4-Qj4D-$hF&R6 zxHd|EqT%=wQ$8J4wA^x*Iw~-KWo84dXkfiGB^mm-^y8W%xygp38uL66@d&enGtvZK z2Bu(6s0F=3a#IY)QTWQ?vrA!0sbeK3>uFd~?1Sy0L=?s+YN=i%RcVIaBTZ?B;~pa4F>7$_Q!!~%VL8k`pND0@ie@eQ zNmw)FIwmbj`qK^j1}x7+(>@|qLk&kcmIDg?lyje$iOP<#-e!BeBbeq}{c|zQMCIHq4VwRZ4gUY%Ootr!vw%xd zXZ-`=${F1ZI1^he^}mP=(?E*5O1*4GgUX6K0H=JXPnZlLvBXn~8nW3W<@GkfxV4~806tJUXmi;Fv6P5nsfaOjArms`{%&89m z%bx;#l&TK_6O|2qL<1>Y=|2s)xk3->fs`}Y4!DH>089fZ_f}Wv{zu0A&p(B7K>q}6 z;4{E9ka9p*%xF+q?<%1GI!FNb0nFW>;rgEqjk-+SPF@K?YyjONo;TY*RPuHhl@DThC;OTr9;Cscd zz>8oj*alt#zW{aMdGG?r2QKg};0ApE;Mpw+Bm*9!cw&DS90R)n-+4BJ5&RG^92p+& zc+lgK{y5kHehGNE`!h5E3V7;8O6=hF2oztq6zQA^drF7tRyXfTX6JON?M!!bd9j?e z+ul8Jc391s@iBT)VVS4MQ&{Yg)=ro9$>~ln7T^)`i^X->t5?aq%PzMc_2N-4;_&j+ znw(c-ZF*6Or@&K)5lh`=mu`^PQMOV3j(nf2=yK^VB%;e3%)3Kh%|83x_JsEKsA`*D zgW+==EbG;4Wp9_u9r|XrbJxtDeC}?}M<2!L1M9zC&UJa+p>Jp3tr@$w(*K>RN_pGi93skI}=yth7U)|QtIrgvd7xE5TU8Cs}a;w{=Z%SN`%N_bumbh_k z>wy^~XP{JRZ~!S{^tg1E{O}W(E|d86veexF@#Ks6(url: string): Promise { - return fetch(url) +export async function api(url: string, restful_token?: string): Promise { + return (restful_token ? fetch(url, {headers: {"Authorization": `Bearer ${restful_token}`}}) : fetch(url)) .then(response => { if (!response.ok) { console.error(response.status, response.statusText) diff --git a/netlify/functions/wanikani.ts b/netlify/functions/wanikani.ts new file mode 100644 index 0000000..e99933b --- /dev/null +++ b/netlify/functions/wanikani.ts @@ -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 } diff --git a/package.json b/package.json index fc061a8..fc5edda 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.css b/src/App.css index bfa775c..e339ad7 100644 --- a/src/App.css +++ b/src/App.css @@ -13,7 +13,7 @@ width: 100%; text-align: center; margin: auto; - font-family:"LexendDeca", "Arial", sans-serif; + font-family: "LexendDeca", "Arial", sans-serif; } p { @@ -26,9 +26,8 @@ p { .text-link { background-color: #0a2e99; - color: #ffe88f; + color: white; line-height: 20px; - padding: 0 5px; } .button_link { diff --git a/src/components/Infos.tsx b/src/components/Infos.tsx index 08b2f6b..7bd1aed 100644 --- a/src/components/Infos.tsx +++ b/src/components/Infos.tsx @@ -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() { + {/* */} ) } diff --git a/src/components/infos/Wanikani.tsx b/src/components/infos/Wanikani.tsx new file mode 100644 index 0000000..48c675f --- /dev/null +++ b/src/components/infos/Wanikani.tsx @@ -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 ( + + + ) +} + +export default function Wanikani() { + const [wanikani, setWanikani]: [WanikaniInfo, React.Dispatch>] = 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 =
+ {...lessons} +
+ + 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 =
+ {...reviews} +
+ + return ( + + ) +} diff --git a/src/components/tabs/Projects.tsx b/src/components/tabs/Projects.tsx index a16864b..c2a59f3 100644 --- a/src/components/tabs/Projects.tsx +++ b/src/components/tabs/Projects.tsx @@ -18,13 +18,13 @@ function Projects({ } let elements = [(
-
+

SwordVenture initially was a game made by a friend in React which I helped develop, but I've made the choice months later to recode it from scratch in Godot, a proper game engine!


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!

-
+
Kanaguessr logo

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.


@@ -32,14 +32,14 @@ function Projects({

It helped friends with remembering katakanas, so I'm glad I took the time to make and polish this webpage!

-
+

Still in early 2023, I've made osu-api-v1-js, my first JavaScript (TypeScript) package!


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.


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!

-
+
Website-Finder logo

...Website-Finder is actually an odd one. What started off in 2020 as a simple Ruby script that downloads images from every website it can find, became a way for me to experiment with different programming languages and their networking capabilities, without dependencies.