taevas.xyz/netlify/functions/wanikani.ts
Taevas 719672ffa0 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
2024-05-04 19:14:18 +02:00

145 lines
4.1 KiB
TypeScript

import {type Handler} from "@netlify/functions";
import {api} from "./shared/api.js";
import {type WanikaniInfo} from "../../src/components/Info/Japanese/Wanikani.js";
type Subject = {
id: number;
object: string;
data: {
characters: string;
slug: string;
document_url: string;
meanings: Array<{
meaning: string;
}>;
};
};
type StuffToLearn = {
available_at: string;
type: string;
writing: string;
meanings: Array<{
meaning: string;
}>;
url: string;
};
function addStuffToLearn(ids: number[], data: Array<{available_at: string; subject_ids: number[]}>, subjects: Subject[]): StuffToLearn[] {
const arr: StuffToLearn[] = [];
for (const id of ids) {
const summaryData = data.find(lesson => lesson.subject_ids.includes(id));
const subject = subjects.find(subject => subject.id === id);
if (!summaryData || !subject) {
console.error("Failed: ", summaryData, subject);
continue;
}
arr.push({
// eslint-disable-next-line @typescript-eslint/naming-convention
available_at: summaryData.available_at,
type: subject.object,
writing: subject.data.characters || subject.data.slug || subject.data.meanings[0].meaning,
meanings: subject.data.meanings,
url: subject.data.document_url,
});
}
return arr;
}
const handler: Handler = async () => {
const data: any[] = await Promise.all([
new Promise((resolve) => {
resolve(api("https://api.wanikani.com/v2/level_progressions", process.env.API_WANIKANI));
}),
new Promise((resolve) => {
resolve(api("https://api.wanikani.com/v2/resets", process.env.API_WANIKANI));
}),
new Promise((resolve) => {
resolve(api("https://api.wanikani.com/v2/summary", process.env.API_WANIKANI));
}),
]);
const progression: {
total_count: number;
data: Array<{
data: {
level: number;
unlocked_at: undefined | string;
completed_at: undefined | string;
abandoned_at: undefined | string;
};
}>;
} = data[0];
const resets: {
data: [{
data: {
created_at: string;
original_level: number;
target_level: number;
};
}];
} = data[1];
const summary: {
data: {
lessons: Array<{
available_at: string;
subject_ids: number[];
}>;
reviews: Array<{
available_at: string;
subject_ids: number[];
}>;
next_reviews_at: undefined | string;
};
} = data[2];
const subjectIdsLessons: number[] = [];
const subjectIdsReviews: number[] = [];
for (const lesson of summary.data.lessons) {
for (const subjectId of lesson.subject_ids) {
subjectIdsLessons.push(subjectId);
}
}
for (const review of summary.data.reviews) {
for (const subjectId of review.subject_ids) {
subjectIdsReviews.push(subjectId);
}
}
const now = new Date();
// next_reviews checks what reviews will be available in the next 23 hours
// summary.data.next_reviews_at checks beyond that, but will be the current time if a review is already available
const nextReviews = summary.data.reviews
.map((r: {subject_ids: number[]; available_at: Date | string}) => {
r.available_at = new Date(r.available_at); return r;
})
.filter((r) => r.available_at > now && r.subject_ids.length) as Array<{subject_ids: number[]; available_at: Date}>;
const moreThingsToReviewAt = nextReviews[0] ? nextReviews[0].available_at.toISOString() : summary.data.next_reviews_at ? summary.data.next_reviews_at : undefined;
const subjectIdsAll = subjectIdsLessons.concat(subjectIdsReviews);
const subjects = await api<{data: Subject[]}>(`https://api.wanikani.com/v2/subjects?ids=${subjectIdsAll.toString()}`, process.env.API_WANIKANI);
const lessons = addStuffToLearn(subjectIdsLessons, summary.data.lessons, subjects.data);
const reviews = addStuffToLearn(subjectIdsReviews, summary.data.reviews, subjects.data);
const info: WanikaniInfo = {
progression,
resets: resets.data,
lessons,
reviews,
moreThingsToReviewAt,
};
return {
statusCode: 200,
body: JSON.stringify(info),
};
};
export {handler};