Add WaniKani
This commit is contained in:
parent
d2dfb621f4
commit
ea09afc2d9
8 changed files with 248 additions and 9 deletions
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
@ -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)
|
||||
|
|
141
netlify/functions/wanikani.ts
Normal file
141
netlify/functions/wanikani.ts
Normal 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 }
|
|
@ -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",
|
||||
|
|
|
@ -26,9 +26,8 @@ p {
|
|||
|
||||
.text-link {
|
||||
background-color: #0a2e99;
|
||||
color: #ffe88f;
|
||||
color: white;
|
||||
line-height: 20px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.button_link {
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
96
src/components/infos/Wanikani.tsx
Normal file
96
src/components/infos/Wanikani.tsx
Normal 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,
|
||||
]
|
||||
}]}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -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&bg_color=1d0e11&fg_color=ffffff&link_color=32c400&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/>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue