/** * Component: Audiobook Card * Documentation: documentation/frontend/components.md * * Premium "Cover First" design - Apple-inspired aesthetic * The cover is the hero. Metadata supports, never overwhelms. */ 'use client'; import React, { useState } from 'react'; import Image from 'next/image'; import { AudiobookDetailsModal } from '@/components/audiobooks/AudiobookDetailsModal'; import { useCreateRequest } from '@/lib/hooks/useRequests'; import { useAuth } from '@/contexts/AuthContext'; import { Audiobook } from '@/lib/hooks/useAudiobooks'; interface AudiobookCardProps { audiobook: Audiobook; isRequested?: boolean; requestStatus?: string; onRequestSuccess?: () => void; squareCovers?: boolean; } // Status configuration for elegant display const getStatusConfig = (audiobook: Audiobook) => { if (audiobook.isAvailable || audiobook.requestStatus === 'completed') { return { type: 'available', label: 'In Library', color: 'emerald' }; } const processingStatuses = ['downloading', 'processing', 'downloaded', 'awaiting_import']; if (audiobook.requestStatus && processingStatuses.includes(audiobook.requestStatus)) { return { type: 'processing', label: 'Processing', color: 'amber' }; } const pendingStatuses = ['pending', 'awaiting_search', 'searching', 'awaiting_approval']; if (audiobook.requestStatus && pendingStatuses.includes(audiobook.requestStatus)) { return { type: 'pending', label: 'Requested', color: 'blue' }; } if (audiobook.requestStatus === 'denied') { return { type: 'denied', label: 'Denied', color: 'red' }; } return null; }; const PLACEHOLDER_COVER = '/placeholder_cover.svg'; export function AudiobookCard({ audiobook, onRequestSuccess, squareCovers = false, }: AudiobookCardProps) { const { user } = useAuth(); const { createRequest, isLoading } = useCreateRequest(); const [showToast, setShowToast] = useState(false); const [error, setError] = useState(null); const [showModal, setShowModal] = useState(false); const [localRequestStatus, setLocalRequestStatus] = useState(undefined); const [coverError, setCoverError] = useState(false); // Build a display-only audiobook with the local status override const displayAudiobook = localRequestStatus !== undefined ? { ...audiobook, requestStatus: localRequestStatus } : audiobook; const status = getStatusConfig(displayAudiobook); const handleRequest = async (e: React.MouseEvent) => { e.stopPropagation(); if (!user) { setError('Please log in to request audiobooks'); setTimeout(() => setError(null), 3000); return; } try { await createRequest(audiobook); setLocalRequestStatus('pending'); setShowToast(true); setTimeout(() => setShowToast(false), 2500); onRequestSuccess?.(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to create request'); setTimeout(() => setError(null), 4000); } }; // Determine if we can request this book const canRequest = !status || status.type === 'denied'; return ( <>
setShowModal(true)} tabIndex={0} onKeyDown={(e) => e.key === 'Enter' && setShowModal(true)} role="button" aria-label={`View details for ${audiobook.title} by ${audiobook.author}`} > {/* Cover Container - The Hero */}
{/* Cover Image with Premium Shadow */}
{/* Cover Art */} {audiobook.coverArtUrl && !coverError ? ( setCoverError(true)} /> ) : ( )} {/* Hover Overlay with Actions - Desktop Only pointer-events-none by default so taps on mobile pass through to card Only enable pointer-events on devices that support hover */}
{/* Quick Action Button */} {canRequest ? ( ) : status?.type === 'available' ? (
In Your Library
) : (
{status?.type === 'processing' && ( Processing )} {status?.type === 'pending' && 'Requested'} {status?.type === 'denied' && 'Request Denied'}
)}
{/* Subtle Status Indicator (visible when not hovered) */} {status && (
)} {/* Rating Badge - Top Left, Elegant */} {audiobook.rating && audiobook.rating > 0 && (
{audiobook.rating.toFixed(1)}
)}
{/* Metadata - Clean, Minimal */}

{audiobook.title}

{audiobook.author}

{/* Toast Notifications - Floating */} {(showToast || error) && (

{showToast ? 'Request created!' : error}

)}
{/* Details Modal */} setShowModal(false)} onRequestSuccess={onRequestSuccess} onStatusChange={(newStatus) => setLocalRequestStatus(newStatus)} isRequested={audiobook.isRequested || localRequestStatus !== undefined} requestStatus={displayAudiobook.requestStatus} isAvailable={audiobook.isAvailable} requestedByUsername={audiobook.requestedByUsername} hasReportedIssue={audiobook.hasReportedIssue} /> ); }