diff --git a/islands/KMenu/commands.ts b/islands/KMenu/commands.ts index e56db37..7894a3b 100644 --- a/islands/KMenu/commands.ts +++ b/islands/KMenu/commands.ts @@ -8,6 +8,8 @@ import { createNewSeries } from "@islands/KMenu/commands/create_series.ts"; import { updateAllRecommendations } from "@islands/KMenu/commands/create_recommendations.ts"; import { createNewRecipe } from "@islands/KMenu/commands/create_recipe.ts"; import { enhanceArticleInfo } from "@islands/KMenu/commands/enhance_article_infos.ts"; +import { createNewBook } from "@islands/KMenu/commands/create_book.ts"; +import { enhanceBookInfo } from "@islands/KMenu/commands/enhance_book_infos.ts"; export const menus: Record = { main: { @@ -74,11 +76,13 @@ export const menus: Record = { }, addSeriesInfo, createNewArticle, + createNewBook, createNewMovie, createNewSeries, createNewRecipe, addMovieInfos, enhanceArticleInfo, + enhanceBookInfo, // updateAllRecommendations, ], }, diff --git a/islands/KMenu/commands/create_book.ts b/islands/KMenu/commands/create_book.ts new file mode 100644 index 0000000..c7c37d6 --- /dev/null +++ b/islands/KMenu/commands/create_book.ts @@ -0,0 +1,134 @@ +import { MenuEntry } from "@islands/KMenu/types.ts"; +import { debounce } from "@lib/helpers.ts"; +import { getCookie } from "@lib/string.ts"; +import { BookResource } from "@lib/marka/schema.ts"; + +interface HardcoverBook { + id: string; + slug: string; + title: string; + subtitle?: string; + author_names: string[]; + series_names?: string[]; + release_year?: string; + image?: string; +} + +export const createNewBook: MenuEntry = { + title: "Create new book", + meta: "", + icon: "IconSquareRoundedPlus", + cb: (state) => { + state.menus["input_book"] = { + title: "Search", + entries: [], + }; + + state.menus["loading"] = { + title: "Search", + entries: [ + { + title: "Loading", + icon: "IconLoader2", + cb() {}, + }, + ], + }; + + state.activeMenu.value = "input_book"; + state.activeState.value = "normal"; + + let currentQuery: string; + const search = debounce(async function search(query: string) { + try { + currentQuery = query; + if (query.length < 2) { + state.menus["input_book"] = { + title: "Search", + entries: [ + { + title: "Type at least 2 characters...", + cb: () => {}, + }, + ], + }; + state.activeMenu.value = "input_book"; + return; + } + + const response = await fetch("/api/hardcover/query?q=" + encodeURIComponent(query)); + + if (!response.ok) { + throw new Error(await response.text()); + } + + const books = await response.json() as HardcoverBook[]; + + if (query !== currentQuery) return; + + if (books.length === 0) { + state.menus["input_book"] = { + title: "Search", + entries: [ + { + title: "No results found", + cb: () => {}, + }, + ], + }; + } else { + state.menus["input_book"] = { + title: "Search", + entries: books.map((b) => { + return { + title: `${b.title}${b.release_year ? ` (${b.release_year})` : ""}${b.author_names?.length ? ` - ${b.author_names.join(", ")}` : ""}`, + cb: async () => { + try { + state.activeState.value = "loading"; + const response = await fetch("/api/books/" + b.id, { + method: "POST", + }); + if (!response.ok) { + throw new Error(await response.text()); + } + const book = await response.json() as BookResource; + unsub(); + globalThis.location.href = "/books/" + book.name; + } catch (_e: unknown) { + state.activeState.value = "error"; + state.loadingText.value = _e instanceof Error ? _e.message : "Unknown error"; + } + }, + }; + }), + }; + } + state.activeMenu.value = "input_book"; + } catch (_e: unknown) { + state.activeState.value = "error"; + state.loadingText.value = _e instanceof Error ? _e.message : "Unknown error"; + } + }, 500); + + const unsub = state.commandInput.subscribe((value) => { + if (!value) { + state.menus["input_book"] = { + title: "Search", + entries: [], + }; + state.activeMenu.value = "input_book"; + return; + } + state.activeMenu.value = "loading"; + search(value); + }); + }, + visible: () => { + if (!getCookie("session_cookie")) return false; + if ( + !globalThis?.location?.pathname?.includes("book") && + globalThis?.location?.pathname !== "/" + ) return false; + return true; + }, +}; diff --git a/islands/KMenu/commands/enhance_book_infos.ts b/islands/KMenu/commands/enhance_book_infos.ts new file mode 100644 index 0000000..6ba1ba5 --- /dev/null +++ b/islands/KMenu/commands/enhance_book_infos.ts @@ -0,0 +1,41 @@ +import { getCookie } from "@lib/string.ts"; +import { MenuEntry } from "../types.ts"; +import { BookResource } from "@lib/marka/schema.ts"; +import { fetchStream } from "@lib/helpers.ts"; + +export const enhanceBookInfo: MenuEntry = { + title: "Enhance Book Info", + meta: "Update metadata and content from Hardcover", + icon: "IconWand", + cb: (state, context) => { + state.activeState.value = "loading"; + const book = context as BookResource; + + fetchStream( + `/api/books/enhance/${book.name}/`, + (chunk) => { + if (chunk.type === "error") { + state.activeState.value = "error"; + state.loadingText.value = chunk.message; + } else if (chunk.type == "finished") { + state.loadingText.value = "Finished"; + setTimeout(() => { + state.visible.value = false; + state.activeState.value = "normal"; + globalThis.location.reload(); + }, 500); + } else { + state.loadingText.value = chunk.message; + } + }, + { method: "POST" }, + ); + }, + visible: () => { + const loc = globalThis["location"]; + if (!getCookie("session_cookie")) return false; + + return (loc?.pathname?.includes("book") && + !loc.pathname.endsWith("books")); + }, +}; diff --git a/lib/marka/schema.ts b/lib/marka/schema.ts index ce7d8d8..55f76e9 100644 --- a/lib/marka/schema.ts +++ b/lib/marka/schema.ts @@ -32,7 +32,7 @@ export const BaseFileSchema = z.object({ }); const makeContentSchema = < - TName extends "Article" | "Review" | "Recipe", + TName extends "Article" | "Review" | "Recipe" | "Book", TShape extends z.ZodRawShape, >( name: TName, @@ -55,6 +55,9 @@ export const ArticleContentSchema = makeContentSchema("Article", { export const ReviewContentSchema = makeContentSchema("Review", { tmdbId: z.number().optional(), + headline: z.string().optional(), + subtitle: z.string().optional(), + bookBody: z.string().optional(), link: z.string().optional(), reviewRating: ReviewRatingSchema.optional(), reviewBody: z.string().optional(), @@ -76,6 +79,17 @@ export const RecipeContentSchema = makeContentSchema("Recipe", { url: z.string().optional(), }); +export const BookContentSchema = makeContentSchema("Book", { + headline: z.string().optional(), + subtitle: z.string().optional(), + bookBody: z.string().optional(), + reviewBody: z.string().optional(), + url: z.string().optional(), + reviewRating: ReviewRatingSchema.optional(), + isbn: z.string().optional(), + bookEdition: z.string().optional(), +}); + export const articleMetadataSchema = z.object({ headline: z.union([z.null(), z.string()]).describe("Headline of the article"), author: z.union([z.null(), z.string()]).describe("Author of the article"), @@ -99,10 +113,15 @@ export const RecipeSchema = BaseFileSchema.extend({ content: RecipeContentSchema, }); +export const BookSchema = BaseFileSchema.extend({ + content: BookContentSchema, +}); + export const GenericResourceSchema = z.union([ ArticleSchema, ReviewSchema, RecipeSchema, + BookSchema, ]); export type Person = z.infer; @@ -122,6 +141,10 @@ export type RecipeResource = z.infer & { image?: typeof imageTable.$inferSelect; }; +export type BookResource = z.infer & { + image?: typeof imageTable.$inferSelect; +}; + export type GenericResource = z.infer & { image?: typeof imageTable.$inferSelect; }; @@ -136,5 +159,8 @@ export function getNameOfResource(res: GenericResource): string { if (res.content?._type === "Recipe" && res.content.name) { return res.content.name; } + if (res.content?._type === "Book" && res.content.headline) { + return res.content.headline; + } return "Unnamed Resource"; } diff --git a/lib/resources.ts b/lib/resources.ts index 75bd9a2..06fe6b5 100644 --- a/lib/resources.ts +++ b/lib/resources.ts @@ -24,4 +24,9 @@ export const resources = { name: "Series", link: "/series", }, + "book": { + emoji: "Bookmark Tabs.png", + name: "Books", + link: "/books", + }, } as const;