/** * Component: Combined Shelves Section (Profile Page) * Documentation: documentation/frontend/components.md */ 'use client'; import React, { useState } from 'react'; import { useShelves, GenericShelf, useSyncShelves, } from '@/lib/hooks/useShelves'; import { useDeleteGoodreadsShelf, useUpdateGoodreadsShelf } from '@/lib/hooks/useGoodreadsShelves'; import { useDeleteHardcoverShelf, useUpdateHardcoverShelf } from '@/lib/hooks/useHardcoverShelves'; import { AddShelfModal } from '@/components/ui/AddShelfModal'; import { AudiobookDetailsModal } from '@/components/audiobooks/AudiobookDetailsModal'; import { usePreferences } from '@/contexts/PreferencesContext'; import { cn } from '@/lib/utils/cn'; import { Modal } from '@/components/ui/Modal'; import { ManageShelfModal } from '@/components/ui/ManageShelfModal'; import { ShelfBook } from '@/lib/hooks/useGoodreadsShelves'; function formatRelativeTime(dateStr: string | null): string { if (!dateStr) return 'Never'; const date = new Date(dateStr); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / 60000); if (diffMins < 1) return 'just now'; if (diffMins < 60) return `${diffMins}m ago`; const diffHours = Math.floor(diffMins / 60); if (diffHours < 24) return `${diffHours}h ago`; const diffDays = Math.floor(diffHours / 24); return `${diffDays}d ago`; } export function ShelvesSection() { const { shelves, isLoading } = useShelves(); const { deleteShelf: deleteGoodreads, isLoading: isDeletingGoodreads } = useDeleteGoodreadsShelf(); const { deleteShelf: deleteHardcover, isLoading: isDeletingHardcover } = useDeleteHardcoverShelf(); const { syncShelves, isSyncing: isSyncingAll } = useSyncShelves(); const { updateShelf: updateGoodreads } = useUpdateGoodreadsShelf(); const { updateShelf: updateHardcover } = useUpdateHardcoverShelf(); const { squareCovers } = usePreferences(); const [confirmDeleteId, setConfirmDeleteId] = useState(null); const [showAddShelf, setShowAddShelf] = useState(false); const [selectedAsin, setSelectedAsin] = useState(null); const [manageShelf, setManageShelf] = useState(null); const handleDelete = async (shelf: GenericShelf) => { try { if (shelf.type === 'goodreads') { await deleteGoodreads(shelf.id); } else { await deleteHardcover(shelf.id); } setConfirmDeleteId(null); } catch { // Error handled by hook } }; const handleToggleAutoRequest = async (shelf: GenericShelf) => { try { if (shelf.type === 'goodreads') { await updateGoodreads(shelf.id, { autoRequest: !shelf.autoRequest }); } else { await updateHardcover(shelf.id, { autoRequest: !shelf.autoRequest }); } } catch { // Error handled by hook } }; const isDeleting = isDeletingGoodreads || isDeletingHardcover; return (
{/* Section Header */}

Shelves

{!isLoading && shelves.length > 0 && (

{shelves.length} {shelves.length === 1 ? 'shelf' : 'shelves'}{' '} connected

)}
{shelves.length > 0 && (
)}
{/* Content */} {isLoading ? ( ) : shelves.length > 0 ? (
{shelves.map((shelf) => ( handleDelete(shelf)} onConfirmDelete={() => setConfirmDeleteId(shelf.id)} onCancelDelete={() => setConfirmDeleteId(null)} onManage={() => setManageShelf(shelf)} onToggleAutoRequest={() => handleToggleAutoRequest(shelf)} onBookClick={(asin) => setSelectedAsin(asin)} /> ))}
) : ( setShowAddShelf(true)} /> )} {/* Modals */} setShowAddShelf(false)} /> setManageShelf(null)} shelf={manageShelf} /> {selectedAsin && ( setSelectedAsin(null)} hideRequestActions /> )}
); } /* ─── Empty State ─── */ function EmptyState({ onAdd }: { onAdd: () => void }) { return (

Connect your reading list

Link a Goodreads or Hardcover shelf and we'll automatically request the audiobook for every book you add.

); } /* ─── Loading Skeleton ─── */ function ShelfCardSkeleton({ squareCovers }: { squareCovers: boolean }) { return (
{[...Array(5)].map((_, i) => (
0 ? '-16px' : 0, zIndex: 5 - i }} /> ))}
); } /* ─── Shelf Card ─── */ interface ShelfCardProps { shelf: GenericShelf; squareCovers: boolean; isDeleting: boolean; isConfirmingDelete: boolean; onDelete: () => void; onConfirmDelete: () => void; onCancelDelete: () => void; onManage: () => void; onToggleAutoRequest: () => void; onBookClick: (asin: string) => void; } function ShelfCard({ shelf, squareCovers, isDeleting, isConfirmingDelete, onDelete, onConfirmDelete, onCancelDelete, onManage, onToggleAutoRequest, onBookClick, }: ShelfCardProps) { const { syncShelves, isSyncing: isManualSyncing } = useSyncShelves(); const displayBooks = shelf.books.slice(0, 6); const hasCovers = displayBooks.length > 0; const remainingCount = Math.max( 0, (shelf.bookCount || 0) - displayBooks.length, ); const isSyncing = !shelf.lastSyncAt; const providerIcon = shelf.type === 'goodreads' ? ( Goodreads ) : ( Hardcover ); return (
{/* Top: Shelf info + actions */}

{shelf.name} {providerIcon}

{shelf.bookCount != null && ( {shelf.bookCount} {shelf.bookCount === 1 ? 'book' : 'books'} )} {!shelf.autoRequest && ( Paused )} {isSyncing ? ( <> Syncing… ) : shelf.lastSyncAt ? ( <> Synced {formatRelativeTime(shelf.lastSyncAt)} ) : ( 'Pending sync' )}
{/* Delete action */}
{isConfirmingDelete ? (
) : (
)}
{/* Bottom: Stacked book covers */}
{hasCovers ? ( ) : isSyncing ? (
{[...Array(3)].map((_, i) => (
0 ? '-16px' : 0, zIndex: 3 - i }} /> ))}
) : null}
); } /* ─── Stacked Cover Display ─── */ function CoverStack({ books, remainingCount, squareCovers, onBookClick, }: { books: ShelfBook[]; remainingCount: number; squareCovers: boolean; onBookClick: (asin: string) => void; }) { const [hoveredIndex, setHoveredIndex] = useState(null); const coverSize = squareCovers ? 'w-[80px] aspect-square' : 'w-[72px] aspect-[2/3]'; return (
{books.map((book, i) => (
0 ? '-16px' : 0, zIndex: hoveredIndex === i ? 50 : books.length - i, }} onMouseEnter={() => setHoveredIndex(i)} onMouseLeave={() => setHoveredIndex(null)} onClick={() => book.asin && onBookClick(book.asin)} title={ book.asin ? `${book.title}${book.author ? ` by ${book.author}` : ''}` : undefined } > {/* eslint-disable-next-line @next/next/no-img-element */} { (e.target as HTMLImageElement).src = '/placeholder_cover.svg'; }} />
))} {remainingCount > 0 && (
+{remainingCount}
)}
); }