/** * Component: Request Card * Documentation: documentation/frontend/components.md */ 'use client'; import React from 'react'; import Image from 'next/image'; import { StatusBadge } from './StatusBadge'; import { Button } from '@/components/ui/Button'; import { useCancelRequest, useManualSearch } from '@/lib/hooks/useRequests'; import { cn } from '@/lib/utils/cn'; import { usePreferences } from '@/contexts/PreferencesContext'; import { useAuth } from '@/contexts/AuthContext'; import { InteractiveTorrentSearchModal } from './InteractiveTorrentSearchModal'; import { AudiobookDetailsModal } from '@/components/audiobooks/AudiobookDetailsModal'; import { COMPLETED_STATUSES } from '@/lib/constants/request-statuses'; interface RequestCardProps { request: { id: string; type?: 'audiobook' | 'ebook'; status: string; progress: number; errorMessage?: string; createdAt: string; updatedAt: string; completedAt?: string; downloadAvailable?: boolean; audiobook: { id: string; audibleAsin?: string; title: string; author: string; coverArtUrl?: string; filePath?: string | null; fileFormat?: string | null; }; }; showActions?: boolean; } export function RequestCard({ request, showActions = true }: RequestCardProps) { const { cancelRequest, isLoading } = useCancelRequest(); const { triggerManualSearch, isLoading: isManualSearching } = useManualSearch(); const { squareCovers } = usePreferences(); const { user } = useAuth(); const [showError, setShowError] = React.useState(false); const [showInteractiveSearch, setShowInteractiveSearch] = React.useState(false); const [showDetailsModal, setShowDetailsModal] = React.useState(false); const requestType = request.type || 'audiobook'; const isEbook = requestType === 'ebook'; const isCompleted = COMPLETED_STATUSES.includes(request.status as typeof COMPLETED_STATUSES[number]); const canCancel = ['pending', 'searching', 'downloading'].includes(request.status); const isActive = ['searching', 'downloading', 'processing'].includes(request.status); const isFailed = request.status === 'failed'; // Ebook requests don't support interactive search (Anna's Archive only) // Interactive search also requires the interactiveSearch permission const hasInteractiveSearchAccess = user?.role === 'admin' || user?.permissions?.interactiveSearch !== false; const canSearch = hasInteractiveSearchAccess && !isEbook && ['pending', 'failed', 'awaiting_search'].includes(request.status); const handleCancel = async () => { if (window.confirm('Are you sure you want to cancel this request?')) { try { await cancelRequest(request.id); } catch (error) { console.error('Failed to cancel request:', error); } } }; const handleManualSearch = async () => { try { await triggerManualSearch(request.id); // Request list will auto-refresh via SWR } catch (error) { console.error('Failed to trigger manual search:', error); alert(error instanceof Error ? error.message : 'Failed to trigger manual search'); } }; const handleInteractiveSearch = () => { setShowInteractiveSearch(true); }; const formatDate = (dateString: string) => { const date = new Date(dateString); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 1) return 'Just now'; if (diffMins < 60) return `${diffMins}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays === 1) return 'Yesterday'; if (diffDays < 7) return `${diffDays}d ago`; return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); }; return (
{/* Cover Art */}
request.audiobook.audibleAsin && setShowDetailsModal(true)} role={request.audiobook.audibleAsin ? 'button' : undefined} tabIndex={request.audiobook.audibleAsin ? 0 : undefined} onKeyDown={(e) => e.key === 'Enter' && request.audiobook.audibleAsin && setShowDetailsModal(true)} > {request.audiobook.coverArtUrl ? ( {request.audiobook.title} ) : (
{isEbook ? ( ) : ( )}
)}
{/* Request Info */}
{/* Title and Author */}

{request.audiobook.title}

By {request.audiobook.author}

{/* Status Badge and Type Badge */}
{isEbook && ( Ebook )} {isActive && request.progress > 0 && (
Active
)} {isActive && request.progress === 0 && (
Setting up...
)}
{/* Progress Bar (for downloading/processing) */} {isActive && request.progress > 0 && (
Progress {request.progress}%
)} {/* Error Message */} {isFailed && request.errorMessage && (
{showError && (
{request.errorMessage}
)}
)} {/* Timestamps and Actions */}
{request.completedAt ? `Completed ${formatDate(request.completedAt)}` : `Requested ${formatDate(request.createdAt)}`}
{/* Action Buttons */} {showActions && (
{canSearch && ( <> )} {canCancel && ( )}
)}
{/* Interactive Search Modal */} setShowInteractiveSearch(false)} requestId={request.id} audiobook={{ title: request.audiobook.title, author: request.audiobook.author, }} /> {/* Audiobook Details Modal */} {request.audiobook.audibleAsin && ( setShowDetailsModal(false)} requestStatus={request.status} isAvailable={COMPLETED_STATUSES.includes(request.status as typeof COMPLETED_STATUSES[number])} hideRequestActions /> )}
); }