diff --git a/src/app/api/user/hardcover-shelves/[id]/route.ts b/src/app/api/user/hardcover-shelves/[id]/route.ts index 438cbcb..b0a7916 100644 --- a/src/app/api/user/hardcover-shelves/[id]/route.ts +++ b/src/app/api/user/hardcover-shelves/[id]/route.ts @@ -106,13 +106,16 @@ export async function PATCH( // Validate token/listId by fetching the list before saving if (cleanedToken || newListId) { const encryptionService = getEncryptionService(); - const tokenToTest = cleanedToken || (() => { + let tokenToTest = cleanedToken || shelf.apiToken; + if (!cleanedToken) { try { - return encryptionService.isEncryptedFormat(shelf.apiToken) - ? encryptionService.decrypt(shelf.apiToken) - : shelf.apiToken; - } catch { return shelf.apiToken; } - })(); + if (encryptionService.isEncryptedFormat(shelf.apiToken)) { + tokenToTest = encryptionService.decrypt(shelf.apiToken); + } + } catch { + // Decryption failed, fall back to raw token + } + } const listIdToTest = newListId || shelf.listId; try { diff --git a/src/components/ui/AddShelfModal.tsx b/src/components/ui/AddShelfModal.tsx index 11cab78..537235f 100644 --- a/src/components/ui/AddShelfModal.tsx +++ b/src/components/ui/AddShelfModal.tsx @@ -77,11 +77,7 @@ export function AddShelfModal({ isOpen, onClose }: AddShelfModalProps) { setRssUrl(''); } else { const finalId = listType === 'status' ? `status-${statusId}` : customListId.trim(); - let cleanedToken = apiToken.trim(); - if (cleanedToken.toLowerCase().startsWith('bearer ')) { - cleanedToken = cleanedToken.slice(7).trim(); - } - const shelf = await addHardcover(cleanedToken, finalId); + const shelf = await addHardcover(apiToken.trim(), finalId); setSuccessMessage(`Added list "${shelf.name}" successfully!`); setApiToken(''); setCustomListId(''); diff --git a/src/components/ui/ManageShelfModal.tsx b/src/components/ui/ManageShelfModal.tsx index 5799907..3f58745 100644 --- a/src/components/ui/ManageShelfModal.tsx +++ b/src/components/ui/ManageShelfModal.tsx @@ -26,14 +26,14 @@ export function ManageShelfModal({ shelf, isOpen, onClose }: ManageShelfModalPro const { updateShelf: updateGoodreads, isLoading: isUpdatingGoodreads, error: goodreadsError } = useUpdateGoodreadsShelf(); const { updateShelf: updateHardcover, isLoading: isUpdatingHardcover, error: hardcoverError } = useUpdateHardcoverShelf(); - // Reset form when shelf changes + // Reset form when shelf changes (use shelf?.id for stable reference) React.useEffect(() => { if (shelf) { setRssUrl(shelf.type === 'goodreads' ? shelf.sourceId : ''); setListId(shelf.type === 'hardcover' ? shelf.sourceId : ''); setApiToken(''); } - }, [shelf]); + }, [shelf?.id]); if (!shelf) return null; diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx index 449e25d..baecde5 100644 --- a/src/components/ui/Modal.tsx +++ b/src/components/ui/Modal.tsx @@ -5,8 +5,7 @@ 'use client'; -import React, { useEffect, useRef, useCallback, useState } from 'react'; -import { createPortal } from 'react-dom'; +import React, { useEffect, useRef, useCallback } from 'react'; import { cn } from '@/lib/utils/cn'; interface ModalProps { @@ -26,12 +25,6 @@ export function Modal({ size = 'md', showCloseButton = true, }: ModalProps) { - const [mounted, setMounted] = useState(false); - - useEffect(() => { - setMounted(true); - }, []); - // Use ref to avoid re-running effect when onClose changes const onCloseRef = useRef(onClose); onCloseRef.current = onClose; @@ -60,7 +53,7 @@ export function Modal({ }; }, [isOpen, handleClose]); - if (!isOpen || !mounted) return null; + if (!isOpen) return null; const sizeClasses = { sm: 'max-w-md', @@ -70,8 +63,8 @@ export function Modal({ full: 'max-w-[95vw]', }; - const content = ( -
+ return ( +
{/* Backdrop */}
e.stopPropagation()} > @@ -123,6 +116,4 @@ export function Modal({
); - - return createPortal(content, document.body); } diff --git a/src/lib/services/hardcover-api.service.ts b/src/lib/services/hardcover-api.service.ts index a0441da..1c568a3 100644 --- a/src/lib/services/hardcover-api.service.ts +++ b/src/lib/services/hardcover-api.service.ts @@ -7,7 +7,9 @@ */ import axios from 'axios'; +import { RMABLogger } from '@/lib/utils/logger'; +const logger = RMABLogger.create('HardcoverAPI'); const HARDCOVER_API_URL = 'https://api.hardcover.app/v1/graphql'; export interface HardcoverApiBook { @@ -33,6 +35,7 @@ interface HardcoverListData { } const PAGE_SIZE = 100; +const MAX_PAGES = 50; /** Extract HardcoverApiBook[] from an array of book-containing items */ function extractBooks(items: Array<{ book?: HardcoverBookNode }>): HardcoverApiBook[] { @@ -106,9 +109,10 @@ export async function fetchHardcoverList( const allBooks: HardcoverApiBook[] = []; let offset = 0; + let page = 0; // Paginate until fewer results than PAGE_SIZE are returned - while (true) { + while (++page <= MAX_PAGES) { const response = await axios.post( HARDCOVER_API_URL, { query, variables: { statusId, limit: PAGE_SIZE, offset } }, @@ -274,8 +278,9 @@ export async function fetchHardcoverList( // Paginate if first page was full if (firstPageItems.length >= PAGE_SIZE) { let offset = PAGE_SIZE; + let page = 1; // first page already fetched - while (true) { + while (++page <= MAX_PAGES) { const pageResponse = await axios.post( HARDCOVER_API_URL, { @@ -291,7 +296,13 @@ export async function fetchHardcoverList( }, ); - if (pageResponse.data?.errors) break; + if (pageResponse.data?.errors) { + logger.warn('Hardcover pagination interrupted by API error', { + errors: pageResponse.data.errors, + offset, + }); + break; + } let pageListsData: HardcoverListData[]; if (isIntId) {