- Add lib/hardcover.ts with GraphQL client for Hardcover API - Add routes/api/books/[name].ts for creating books via Hardcover ID - Add routes/api/books/enhance/[name].ts for enhancing books - Add routes/api/hardcover/query.ts for searching books - Add routes/books/[name].tsx and index.tsx for book pages
141 lines
3.4 KiB
TypeScript
141 lines
3.4 KiB
TypeScript
import { createCache } from "@lib/cache.ts";
|
|
|
|
const HARDCOVER_API_URL = "https://api.hardcover.app/v1/graphql";
|
|
|
|
const CACHE_INTERVAL = 1000 * 60 * 60 * 24 * 7;
|
|
const cache = createCache("hardcover", { expires: CACHE_INTERVAL });
|
|
|
|
export interface HardcoverBookResult {
|
|
id: string;
|
|
slug: string;
|
|
title: string;
|
|
subtitle?: string;
|
|
author_names: string[];
|
|
cover_color?: string;
|
|
description?: string;
|
|
isbn?: string;
|
|
isbn13?: string;
|
|
release_year?: string;
|
|
series_names?: string[];
|
|
rating?: number;
|
|
ratings_count?: number;
|
|
pages?: number;
|
|
image?: string;
|
|
}
|
|
|
|
async function hardcoverFetch(query: string, variables: Record<string, unknown> = {}) {
|
|
const response = await fetch(HARDCOVER_API_URL, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Authorization": Deno.env.get("HARDCOVER_API_TOKEN") || "",
|
|
},
|
|
body: JSON.stringify({ query, variables }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Hardcover API error: ${response.statusText}`);
|
|
}
|
|
|
|
const json = await response.json();
|
|
|
|
if (json.errors) {
|
|
console.error("Hardcover GraphQL errors:", JSON.stringify(json.errors, null, 2));
|
|
throw new Error(`Hardcover GraphQL error: ${json.errors[0]?.message || "Unknown error"}`);
|
|
}
|
|
|
|
return json;
|
|
}
|
|
|
|
export const searchBook = async (query: string) => {
|
|
const id = `query:booksearch:${query}`;
|
|
if (cache.has(id)) return cache.get(id) as HardcoverBookResult[];
|
|
|
|
const graphqlQuery = `
|
|
query SearchBooks($query: String!, $perPage: Int, $page: Int) {
|
|
search(
|
|
query: $query
|
|
query_type: "Book"
|
|
per_page: $perPage
|
|
page: $page
|
|
) {
|
|
results
|
|
}
|
|
}
|
|
`;
|
|
|
|
const result = await hardcoverFetch(graphqlQuery, {
|
|
query,
|
|
perPage: 10,
|
|
page: 1,
|
|
});
|
|
|
|
const typesenseResponse = result.data?.search?.results;
|
|
const hits = typesenseResponse?.hits;
|
|
const books = (hits || []).map((hit: { document: HardcoverBookResult }) => hit.document);
|
|
|
|
cache.set(id, books);
|
|
return books as HardcoverBookResult[];
|
|
};
|
|
|
|
export const getBookDetails = async (id: string) => {
|
|
const cacheId = `query:bookdetails:${id}`;
|
|
if (cache.has(cacheId)) return cache.get(cacheId);
|
|
|
|
const graphqlQuery = `
|
|
query GetBook($id: Int!) {
|
|
books(where: { id: { _eq: $id } }) {
|
|
id
|
|
slug
|
|
title
|
|
subtitle
|
|
description
|
|
release_date
|
|
release_year
|
|
rating
|
|
ratings_count
|
|
pages
|
|
cached_image
|
|
cached_contributors
|
|
featured_book_series {
|
|
series {
|
|
id
|
|
name
|
|
slug
|
|
}
|
|
}
|
|
editions(limit: 1) {
|
|
isbn_10
|
|
isbn_13
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
const result = await hardcoverFetch(graphqlQuery, { id: parseInt(id) });
|
|
|
|
const bookData = result.data?.books?.[0];
|
|
|
|
if (bookData) {
|
|
const isbn13 = bookData.editions?.[0]?.isbn_13;
|
|
const isbn10 = bookData.editions?.[0]?.isbn_10;
|
|
|
|
const cachedContributors = bookData.cached_contributors as Array<{ author: { name: string }; contribution: string }> | undefined;
|
|
const authorName = cachedContributors?.[0]?.author?.name;
|
|
|
|
const book = {
|
|
...bookData,
|
|
isbn: isbn13 || isbn10 || "",
|
|
isbn13,
|
|
isbn10,
|
|
author_names: authorName ? [authorName] : [],
|
|
image: bookData.cached_image?.url,
|
|
};
|
|
|
|
cache.set(cacheId, book);
|
|
return book;
|
|
}
|
|
|
|
return null;
|
|
};
|