"use client"; import { useEffect, useMemo, useState } from "react"; import { BookOpen, Check, ChevronRight, Layers3, Loader2, Search, } from "lucide-react"; import { useTranslation } from "react-i18next"; import PickerShell from "@/components/common/PickerShell"; import PickerHeader from "@/components/common/PickerHeader"; import { bookApi } from "@/lib/book-api"; import type { Book, BookDetail, Chapter, Page } from "@/lib/book-types"; import type { SelectedBookPage, SelectedBookReference, } from "@/lib/book-references"; interface BookReferencePickerProps { open: boolean; initialReferences: SelectedBookReference[]; onClose: () => void; onApply: (references: SelectedBookReference[]) => void; } function pageKey(bookId: string, pageId: string): string { return `${bookId}:${pageId}`; } function groupPages(detail: BookDetail | null): Array<{ chapter: Chapter | null; pages: Page[]; }> { if (!detail) return []; const byId = new Map(detail.pages.map((page) => [page.id, page])); const used = new Set(); const groups: Array<{ chapter: Chapter | null; pages: Page[] }> = ( detail.spine?.chapters || [] ).map((chapter) => { const pages = chapter.page_ids .map((pageId) => byId.get(pageId)) .filter((page): page is Page => Boolean(page)); pages.forEach((page) => used.add(page.id)); return { chapter, pages }; }); const orphanPages = detail.pages.filter((page) => !used.has(page.id)); if (orphanPages.length) groups.push({ chapter: null, pages: orphanPages }); return groups.filter((group) => group.pages.length > 0); } export default function BookReferencePicker({ open, initialReferences, onClose, onApply, }: BookReferencePickerProps) { const { t } = useTranslation(); const [books, setBooks] = useState([]); const [details, setDetails] = useState>({}); const [activeBookId, setActiveBookId] = useState(null); const [selected, setSelected] = useState([]); const [query, setQuery] = useState(""); const [loadingBooks, setLoadingBooks] = useState(false); const [loadingDetail, setLoadingDetail] = useState(false); useEffect(() => { if (!open) return; let mounted = true; // Re-seed selection each time the picker opens. // eslint-disable-next-line react-hooks/set-state-in-effect setSelected(initialReferences); setLoadingBooks(true); void bookApi .list() .then((data) => { if (!mounted) return; setBooks(data.books || []); setActiveBookId((current) => current || data.books?.[0]?.id || null); }) .catch(() => { if (mounted) setBooks([]); }) .finally(() => { if (mounted) setLoadingBooks(false); }); return () => { mounted = false; }; }, [initialReferences, open]); useEffect(() => { if (!open || !activeBookId || details[activeBookId]) return; let mounted = true; // Show detail loading before the async book lookup resolves. // eslint-disable-next-line react-hooks/set-state-in-effect setLoadingDetail(true); void bookApi .get(activeBookId) .then((detail) => { if (!mounted) return; setDetails((prev) => ({ ...prev, [activeBookId]: detail })); }) .catch(() => undefined) .finally(() => { if (mounted) setLoadingDetail(false); }); return () => { mounted = false; }; }, [activeBookId, details, open]); const filteredBooks = useMemo(() => { const keyword = query.trim().toLowerCase(); if (!keyword) return books; return books.filter((book) => `${book.title} ${book.description}`.toLowerCase().includes(keyword), ); }, [books, query]); const activeBook = books.find((book) => book.id === activeBookId) || null; const activeDetail = activeBookId ? details[activeBookId] || null : null; const pageGroups = useMemo(() => groupPages(activeDetail), [activeDetail]); const selectedKeys = useMemo(() => { const keys = new Set(); selected.forEach((book) => book.pages.forEach((page) => keys.add(pageKey(book.bookId, page.pageId))), ); return keys; }, [selected]); const togglePage = (book: Book, page: Page, chapter: Chapter | null) => { const nextPage: SelectedBookPage = { bookId: book.id, bookTitle: book.title || t("Untitled book"), pageId: page.id, pageTitle: page.title || t("Untitled chapter"), chapterId: chapter?.id || page.chapter_id, chapterTitle: chapter?.title || page.title || t("Untitled chapter"), }; setSelected((prev) => { const existing = prev.find((ref) => ref.bookId === book.id); const selectedAlready = existing?.pages.some((p) => p.pageId === page.id); if (selectedAlready) { return prev .map((ref) => ref.bookId === book.id ? { ...ref, pages: ref.pages.filter((p) => p.pageId !== page.id) } : ref, ) .filter((ref) => ref.pages.length > 0); } if (existing) { return prev.map((ref) => ref.bookId === book.id ? { ...ref, pages: [...ref.pages, nextPage] } : ref, ); } return [ ...prev, { bookId: book.id, bookTitle: book.title || t("Untitled book"), pages: [nextPage], }, ]; }); }; const toggleChapter = ( book: Book, chapter: Chapter | null, pages: Page[], ) => { const allSelected = pages.every((page) => selectedKeys.has(pageKey(book.id, page.id)), ); setSelected((prev) => { const existing = prev.find((ref) => ref.bookId === book.id); const existingPages = existing?.pages || []; const pageIds = new Set(pages.map((page) => page.id)); const remaining = allSelected ? existingPages.filter((page) => !pageIds.has(page.pageId)) : [ ...existingPages, ...pages .filter( (page) => !existingPages.some((p) => p.pageId === page.id), ) .map((page) => ({ bookId: book.id, bookTitle: book.title || t("Untitled book"), pageId: page.id, pageTitle: page.title || t("Untitled chapter"), chapterId: chapter?.id || page.chapter_id, chapterTitle: chapter?.title || page.title || t("Untitled chapter"), })), ]; const nextRef = { bookId: book.id, bookTitle: book.title || t("Untitled book"), pages: remaining, }; const others = prev.filter((ref) => ref.bookId !== book.id); return nextRef.pages.length ? [...others, nextRef] : others; }); }; const selectedCount = selected.reduce( (total, ref) => total + ref.pages.length, 0, ); return (
{!activeBook ? (
{t("Select a book to view chapters.")}
) : loadingDetail && !activeDetail ? (
) : (

{activeBook.title || t("Untitled book")}

{activeBook.description && (

{activeBook.description}

)}
{pageGroups.map(({ chapter, pages }) => { const allSelected = pages.every((page) => selectedKeys.has(pageKey(activeBook.id, page.id)), ); const showPageRows = pages.length > 1; const firstPage = pages[0]; return (
{showPageRows && (
{pages.map((page) => { const checked = selectedKeys.has( pageKey(activeBook.id, page.id), ); return ( ); })}
)}
); })}
)}
{selectedCount ? t("{{count}} chapters selected", { count: selectedCount }) : t("No chapters selected")}
); }