/** * Component: Audiobook Details Modal * Documentation: documentation/frontend/components.md * * Premium modal design with mobile-first sticky actions * Matches the Apple-inspired card aesthetic */ 'use client'; import React, { useEffect, useState } from 'react'; import Image from 'next/image'; import Link from 'next/link'; import { createPortal } from 'react-dom'; import { useAudiobookDetails } from '@/lib/hooks/useAudiobooks'; import { useCreateRequest, useEbookStatus, useFetchEbookByAsin } from '@/lib/hooks/useRequests'; import { useAuth } from '@/contexts/AuthContext'; import { usePreferences } from '@/contexts/PreferencesContext'; import { InteractiveTorrentSearchModal } from '@/components/requests/InteractiveTorrentSearchModal'; import { ReportIssueModal } from '@/components/audiobooks/ReportIssueModal'; interface AudiobookDetailsModalProps { asin: string; isOpen: boolean; onClose: () => void; onRequestSuccess?: () => void; isRequested?: boolean; requestStatus?: string | null; isAvailable?: boolean; requestedByUsername?: string | null; hideRequestActions?: boolean; hasReportedIssue?: boolean; } // Status helper const getStatusInfo = (isAvailable: boolean, requestStatus: string | null, requestedByUsername: string | null) => { if (isAvailable || requestStatus === 'completed') { return { type: 'available', label: 'In Your Library', canRequest: false }; } const processingStatuses = ['downloading', 'processing', 'downloaded', 'awaiting_import']; if (requestStatus && processingStatuses.includes(requestStatus)) { return { type: 'processing', label: 'Processing', canRequest: false }; } const pendingStatuses = ['pending', 'awaiting_search', 'searching', 'awaiting_approval']; if (requestStatus && pendingStatuses.includes(requestStatus)) { const label = requestStatus === 'awaiting_approval' ? requestedByUsername ? `Pending Approval (${requestedByUsername})` : 'Pending Approval' : requestedByUsername ? `Requested by ${requestedByUsername}` : 'Requested'; return { type: 'pending', label, canRequest: false }; } if (requestStatus === 'denied') { return { type: 'denied', label: 'Request Denied', canRequest: true }; } return { type: 'none', label: '', canRequest: true }; }; export function AudiobookDetailsModal({ asin, isOpen, onClose, onRequestSuccess, isRequested = false, requestStatus = null, isAvailable = false, requestedByUsername = null, hideRequestActions = false, hasReportedIssue = false, }: AudiobookDetailsModalProps) { const { user } = useAuth(); const { squareCovers } = usePreferences(); const { audiobook, audibleBaseUrl, isLoading, error } = useAudiobookDetails(isOpen ? asin : null); const { createRequest, isLoading: isRequesting } = useCreateRequest(); const { ebookStatus, revalidate: revalidateEbookStatus } = useEbookStatus(isOpen && isAvailable ? asin : null); const { fetchEbook, isLoading: isFetchingEbook } = useFetchEbookByAsin(); const [showToast, setShowToast] = useState(false); const [toastMessage, setToastMessage] = useState(''); const [toastType, setToastType] = useState<'success' | 'error'>('success'); const [mounted, setMounted] = useState(false); const [showInteractiveSearch, setShowInteractiveSearch] = useState(false); const [showInteractiveSearchEbook, setShowInteractiveSearchEbook] = useState(false); const [showReportIssue, setShowReportIssue] = useState(false); const [asinCopied, setAsinCopied] = useState(false); const status = getStatusInfo(isAvailable, requestStatus, requestedByUsername); const canShowEbookButtons = isAvailable && ebookStatus?.ebookSourcesEnabled && !ebookStatus?.hasActiveEbookRequest; useEffect(() => { setMounted(true); }, []); useEffect(() => { if (isOpen) { document.body.style.overflow = 'hidden'; } else { document.body.style.overflow = 'unset'; } return () => { document.body.style.overflow = 'unset'; }; }, [isOpen]); const showNotification = (message: string, type: 'success' | 'error' = 'success') => { setToastMessage(message); setToastType(type); setShowToast(true); setTimeout(() => setShowToast(false), 3000); }; const handleRequest = async () => { if (!user || !audiobook) { showNotification('Please log in to request audiobooks', 'error'); return; } try { await createRequest(audiobook); showNotification('Request created!'); setTimeout(onClose, 1500); onRequestSuccess?.(); } catch (err) { showNotification(err instanceof Error ? err.message : 'Failed to create request', 'error'); } }; const handleInteractiveSearch = () => { if (!user || !audiobook) { showNotification('Please log in to request audiobooks', 'error'); return; } setShowInteractiveSearch(true); }; const handleFetchEbook = async () => { if (!user) { showNotification('Please log in to request ebooks', 'error'); return; } try { const result = await fetchEbook(asin); revalidateEbookStatus(); showNotification(result.needsApproval ? 'Ebook request submitted for approval!' : 'Ebook search started!'); } catch (err) { showNotification(err instanceof Error ? err.message : 'Failed to request ebook', 'error'); } }; const handleCopyAsin = async () => { try { await navigator.clipboard.writeText(asin); setAsinCopied(true); setTimeout(() => setAsinCopied(false), 2000); } catch (err) { console.error('Failed to copy ASIN:', err); } }; const formatDuration = (minutes?: number) => { if (!minutes) return null; const hours = Math.floor(minutes / 60); const mins = minutes % 60; return `${hours}h ${mins}m`; }; const formatDate = (dateString?: string) => { if (!dateString) return null; try { return new Date(dateString).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); } catch { return dateString; } }; if (!isOpen || !mounted) return null; const modalContent = (
Failed to load details
Please try again later
{audiobook.authorAsin ? ( { e.stopPropagation(); onClose(); }} className="hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors" > {audiobook.author} ) : ( audiobook.author )}
{audiobook.narrator && (Narrated by {audiobook.narrator}
)} {audiobook.series && ({audiobook.seriesAsin ? ( { e.stopPropagation(); onClose(); }} className="hover:text-emerald-600 dark:hover:text-emerald-400 transition-colors" > {audiobook.series}{audiobook.seriesPart ? `, Book ${audiobook.seriesPart}` : ''} ) : ( {audiobook.series}{audiobook.seriesPart ? `, Book ${audiobook.seriesPart}` : ''} )}
)} {/* Status Badge */} {status.type !== 'none' && ({audiobook.description}
ASIN
Source
Audible{toastMessage}