feat: integrate Hardcover API for books
- 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
This commit is contained in:
140
lib/hardcover.ts
Normal file
140
lib/hardcover.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
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;
|
||||
};
|
||||
Reference in New Issue
Block a user