134 lines
3.5 KiB
TypeScript
134 lines
3.5 KiB
TypeScript
import type { APIRoute } from "astro";
|
|
import { env as cfEnv } from "cloudflare:workers";
|
|
|
|
export const prerender = false;
|
|
|
|
|
|
type Env = Record<string, string | undefined>;
|
|
|
|
interface Book {
|
|
title: string;
|
|
author: string;
|
|
cover: string | null;
|
|
url: string;
|
|
progress: number | null;
|
|
}
|
|
|
|
const GQL = "https://api.hardcover.app/v1/graphql";
|
|
const SITE = "https://hardcover.app";
|
|
|
|
const QUERY = `
|
|
query SiteReading {
|
|
me {
|
|
current: user_books(where: {status_id: {_eq: 2}}, limit: 5) {
|
|
id
|
|
user_book_reads(order_by: {id: desc}, limit: 1) { progress_pages }
|
|
book {
|
|
title
|
|
slug
|
|
pages
|
|
image { url }
|
|
contributions { author { name } }
|
|
}
|
|
}
|
|
want: user_books(where: {status_id: {_eq: 1}}, order_by: {id: desc}, limit: 12) {
|
|
book {
|
|
title
|
|
slug
|
|
image { url }
|
|
contributions { author { name } }
|
|
}
|
|
}
|
|
recent: user_books(where: {status_id: {_eq: 3}}, order_by: {id: desc}, limit: 8) {
|
|
book {
|
|
title
|
|
slug
|
|
image { url }
|
|
contributions { author { name } }
|
|
}
|
|
}
|
|
}
|
|
}`;
|
|
|
|
function getEnv(): Env {
|
|
const nodeEnv = typeof process !== "undefined" ? process.env : {};
|
|
let workerEnv: Env = {};
|
|
try {
|
|
workerEnv = cfEnv as unknown as Env;
|
|
} catch {
|
|
}
|
|
return { ...nodeEnv, ...workerEnv };
|
|
}
|
|
|
|
function mapBook(ub: any, withProgress: boolean): Book {
|
|
const b = ub?.book ?? {};
|
|
const author = (b?.contributions?.[0]?.author?.name ?? "").replace(/\s+/g, " ").trim();
|
|
const cover = b?.image?.url ?? null;
|
|
|
|
let progress: number | null = null;
|
|
if (withProgress) {
|
|
const read = ub?.user_book_reads?.[0];
|
|
const done = read?.progress_pages;
|
|
const total = b?.pages;
|
|
if (typeof done === "number" && typeof total === "number" && total > 0) {
|
|
progress = Math.max(0, Math.min(100, Math.round((done / total) * 100)));
|
|
}
|
|
}
|
|
|
|
return {
|
|
title: b?.title ?? "",
|
|
author,
|
|
cover,
|
|
url: b?.slug ? `${SITE}/books/${b.slug}` : SITE,
|
|
progress,
|
|
};
|
|
}
|
|
|
|
export const GET: APIRoute = async () => {
|
|
const token = getEnv().HARDCOVER_TOKEN;
|
|
const empty = { configured: false, current: [], want: [], recent: [] };
|
|
const json = (data: unknown, status = 200) =>
|
|
new Response(JSON.stringify(data), {
|
|
status,
|
|
headers: {
|
|
"content-type": "application/json",
|
|
"cache-control": "public, max-age=300, s-maxage=1800, stale-while-revalidate=86400",
|
|
},
|
|
});
|
|
|
|
if (!token) return json(empty);
|
|
|
|
let payload: any;
|
|
try {
|
|
const res = await fetch(GQL, {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
authorization: token.startsWith("Bearer ") ? token : `Bearer ${token}`,
|
|
"user-agent": "zachy.cc reading widget",
|
|
},
|
|
body: JSON.stringify({ query: QUERY }),
|
|
});
|
|
payload = await res.json();
|
|
} catch {
|
|
return json({ ...empty, configured: true });
|
|
}
|
|
|
|
const me = Array.isArray(payload?.data?.me) ? payload.data.me[0] : payload?.data?.me;
|
|
if (!me || payload.errors) {
|
|
return json({ ...empty, configured: true });
|
|
}
|
|
|
|
const current: Book[] = (me.current ?? [])
|
|
.map((ub: any) => mapBook(ub, true))
|
|
.filter((b: Book) => b.title);
|
|
const want: Book[] = (me.want ?? [])
|
|
.map((ub: any) => mapBook(ub, false))
|
|
.filter((b: Book) => b.title);
|
|
const recent: Book[] = (me.recent ?? [])
|
|
.map((ub: any) => mapBook(ub, false))
|
|
.filter((b: Book) => b.title);
|
|
|
|
return json({ configured: true, current, want, recent });
|
|
};
|