import React, { useState, useEffect, useCallback } from 'react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog'; import { Button } from './ui/button'; import { Input } from './ui/input'; import { Badge } from './ui/badge'; import { toast } from 'sonner'; import { Upload, X, Image as ImageIcon, Search, Plus, Check } from 'lucide-react'; import { usePage } from '@inertiajs/react'; import { hasPermission } from '@/utils/authorization'; interface MediaItem { id: number; name: string; file_name: string; url: string; thumb_url: string; size: number; mime_type: string; created_at: string; } interface MediaLibraryModalProps { isOpen: boolean; onClose: () => void; onSelect: (url: string) => void; multiple?: boolean; } export default function MediaLibraryModal({ isOpen, onClose, onSelect, multiple = false }: MediaLibraryModalProps) { const { auth } = usePage().props as any; const permissions = auth?.permissions || []; const canCreateMedia = hasPermission(permissions, 'create-media'); const canManageMedia = hasPermission(permissions, 'manage-media'); const [media, setMedia] = useState([]); const [directories, setDirectories] = useState([]); const [currentDirectory, setCurrentDirectory] = useState(null); const [filteredMedia, setFilteredMedia] = useState([]); const [loading, setLoading] = useState(false); const [uploading, setUploading] = useState(false); const [selectedItems, setSelectedItems] = useState([]); const [dragActive, setDragActive] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 24; const fetchMedia = useCallback(async () => { setLoading(true); try { const params = new URLSearchParams(); if (currentDirectory) { params.append('directory_id', currentDirectory.toString()); } const response = await fetch(`${route('api.media.index')}?${params}`, { credentials: 'same-origin', headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest', }, }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); const mediaArray = Array.isArray(data.media) ? data.media : Array.isArray(data) ? data : []; setMedia(mediaArray); setDirectories(data.directories || []); setFilteredMedia(mediaArray); } catch (error) { toast.error('Failed to load media'); } finally { setLoading(false); } }, [currentDirectory]); useEffect(() => { if (isOpen) { fetchMedia(); setSearchTerm(''); } }, [isOpen, fetchMedia]); // Filter media based on search term useEffect(() => { if (!searchTerm.trim()) { setFilteredMedia(media); } else { const filtered = media.filter(item => item.name.toLowerCase().includes(searchTerm.toLowerCase()) || item.file_name.toLowerCase().includes(searchTerm.toLowerCase()) ); setFilteredMedia(filtered); } setCurrentPage(1); }, [searchTerm, media]); // Pagination calculations const totalPages = Math.ceil(filteredMedia.length / itemsPerPage); const startIndex = (currentPage - 1) * itemsPerPage; const currentMedia = filteredMedia.slice(startIndex, startIndex + itemsPerPage); const handleFileUpload = async (files: FileList) => { setUploading(true); const validFiles = Array.from(files); if (validFiles.length === 0) { setUploading(false); return; } const formData = new FormData(); validFiles.forEach(file => { formData.append('files[]', file); }); try { const response = await fetch(route('api.media.batch'), { method: 'POST', body: formData, credentials: 'same-origin', headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '', 'X-Requested-With': 'XMLHttpRequest', }, }); const result = await response.json(); if (response.ok) { if (result.data && result.data.length > 0) { setMedia(prev => [...result.data, ...prev]); } // Show appropriate success/warning messages if (result.errors && result.errors.length > 0) { toast.warning(result.message || `${result.data?.length || 0} uploaded, ${result.errors.length} failed`); result.errors.forEach((error: string) => { toast.error(error, { duration: 5000 }); }); } else { toast.success(result.message || `${result.data?.length || 0} file(s) uploaded successfully`); } } else { toast.error(result.message || 'Failed to upload files'); if (result.errors) { result.errors.forEach((error: string) => { toast.error(error, { duration: 5000 }); }); } } } catch (error) { toast.error('Error uploading files'); } setUploading(false); }; const handleDrag = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.type === 'dragenter' || e.type === 'dragover') { setDragActive(true); } else if (e.type === 'dragleave') { setDragActive(false); } }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setDragActive(false); if (e.dataTransfer.files && e.dataTransfer.files[0]) { handleFileUpload(e.dataTransfer.files); } }; const handleSelect = (url: string) => { if (multiple) { setSelectedItems(prev => prev.includes(url) ? prev.filter(item => item !== url) : [...prev, url] ); } else { onSelect(url); onClose(); } }; const handleConfirmSelection = () => { if (multiple && selectedItems.length > 0) { onSelect(selectedItems.join(',')); onClose(); } }; const getFileIcon = (mimeType: string) => { if (mimeType.startsWith('image/')) return ; if (mimeType.includes('pdf')) return
PDF
; if (mimeType.includes('word') || mimeType.includes('document')) return
DOC
; if (mimeType.includes('csv') || mimeType.includes('spreadsheet')) return
CSV
; if (mimeType.startsWith('video/')) return
VID
; if (mimeType.startsWith('audio/')) return
AUD
; return
FILE
; }; return (
Media Library {filteredMedia.length > 0 && ( {filteredMedia.length} )} Browse and select media files from your library
{/* Directory Navigation */} {directories.length > 0 && (
{directories.map((dir: any) => ( ))}
)} {/* Header with Search and Upload */}
setSearchTerm(e.target.value)} className="pl-10 h-10" />
{canCreateMedia && (
e.target.files && handleFileUpload(e.target.files)} className="hidden" id="file-upload" />
)}
{/* Stats and Selection Info */}
{filteredMedia.length} files {totalPages > 1 && ( Page {currentPage} of {totalPages} )}
{multiple && selectedItems.length > 0 && ( {selectedItems.length} selected )}
{/* Media Grid */}
{loading ? (

Loading media...

) : filteredMedia.length === 0 ? (

No media files found

{searchTerm && (

No results for "${searchTerm}"

)}

{searchTerm ? 'Try a different search term or upload new images' : 'Upload images to get started'}

{canCreateMedia && ( )}
) : (
{currentMedia.map((item) => (
handleSelect(item.url)} >
{item.mime_type.startsWith('image/') ? ( {item.name} { e.currentTarget.src = item.url; }} /> ) : (
{getFileIcon(item.mime_type)}
{item.mime_type.split('/')[1]?.toUpperCase() || 'FILE'}
)} {/* Selection Indicator */} {selectedItems.includes(item.url) && (
)} {/* Hover Overlay */}
{/* File Name Tooltip */}

{item.name}

))}
)}
{/* Pagination */} {totalPages > 1 && (
Showing {startIndex + 1} to {Math.min(startIndex + itemsPerPage, filteredMedia.length)} of {filteredMedia.length} files
{Array.from({ length: Math.min(totalPages, 5) }, (_, i) => { let page; if (totalPages <= 5) { page = i + 1; } else if (currentPage <= 3) { page = i + 1; } else if (currentPage >= totalPages - 2) { page = totalPages - 4 + i; } else { page = currentPage - 2 + i; } return ( ); })}
)} {/* Actions */}
{multiple && selectedItems.length > 0 && ( )} {multiple && selectedItems.length > 0 && ( )}
); }