282 lines
No EOL
12 KiB
PHP
282 lines
No EOL
12 KiB
PHP
<div class="space-y-6">
|
|
{{-- Header --}}
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h1 class="text-2xl font-semibold text-gray-900">Media Manager</h1>
|
|
<p class="text-sm text-gray-500 mt-1">
|
|
Manage your media files and assets
|
|
</p>
|
|
</div>
|
|
|
|
<div class="flex items-center space-x-3">
|
|
<flux:button wire:click="$dispatch('open-upload-modal')" variant="primary">
|
|
Upload Media
|
|
</flux:button>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Filters & Search --}}
|
|
<flux:card>
|
|
<div class="flex items-center space-x-4">
|
|
<div class="flex-1">
|
|
<flux:input wire:model.live="search"
|
|
placeholder="Search media files..."
|
|
class="w-full" />
|
|
</div>
|
|
|
|
<flux:select wire:model.live="filterType" placeholder="All Types">
|
|
<flux:option value="">All Types</flux:option>
|
|
<flux:option value="image">Images</flux:option>
|
|
<flux:option value="document">Documents</flux:option>
|
|
<flux:option value="video">Videos</flux:option>
|
|
<flux:option value="audio">Audio</flux:option>
|
|
</flux:select>
|
|
|
|
<flux:select wire:model.live="sortBy">
|
|
<flux:option value="created_at">Upload Date</flux:option>
|
|
<flux:option value="name">Name</flux:option>
|
|
<flux:option value="size">Size</flux:option>
|
|
</flux:select>
|
|
|
|
<flux:select wire:model.live="sortDirection">
|
|
<flux:option value="desc">Newest First</flux:option>
|
|
<flux:option value="asc">Oldest First</flux:option>
|
|
</flux:select>
|
|
</div>
|
|
</flux:card>
|
|
|
|
{{-- Media Grid --}}
|
|
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
|
@forelse($mediaItems as $media)
|
|
<div wire:key="media-{{ $media->id }}"
|
|
class="group relative bg-white border border-gray-200 rounded-lg overflow-hidden hover:shadow-md transition-shadow">
|
|
|
|
{{-- Media Preview --}}
|
|
<div class="aspect-square bg-gray-100 flex items-center justify-center">
|
|
@if($media->hasGeneratedConversion('thumb'))
|
|
<img src="{{ $media->getUrl('thumb') }}"
|
|
alt="{{ $media->name }}"
|
|
class="w-full h-full object-cover">
|
|
@elseif(str_starts_with($media->mime_type, 'image/'))
|
|
<img src="{{ $media->getUrl() }}"
|
|
alt="{{ $media->name }}"
|
|
class="w-full h-full object-cover">
|
|
@else
|
|
<div class="text-center p-4">
|
|
@switch(explode('/', $media->mime_type)[0])
|
|
@case('video')
|
|
<flux:icon.film class="w-8 h-8 mx-auto text-gray-400 mb-2" />
|
|
@break
|
|
@case('audio')
|
|
<flux:icon.musical-note class="w-8 h-8 mx-auto text-gray-400 mb-2" />
|
|
@break
|
|
@default
|
|
<flux:icon.document class="w-8 h-8 mx-auto text-gray-400 mb-2" />
|
|
@endswitch
|
|
<span class="text-xs text-gray-500 font-medium">
|
|
{{ strtoupper(pathinfo($media->name, PATHINFO_EXTENSION)) }}
|
|
</span>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
{{-- Media Info --}}
|
|
<div class="p-3">
|
|
<h3 class="text-sm font-medium text-gray-900 truncate"
|
|
title="{{ $media->name }}">
|
|
{{ $media->name }}
|
|
</h3>
|
|
<p class="text-xs text-gray-500 mt-1">
|
|
{{ $this->formatFileSize($media->size) }}
|
|
</p>
|
|
</div>
|
|
|
|
{{-- Actions Overlay --}}
|
|
<div class="absolute inset-0 bg-black bg-opacity-50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center space-x-2">
|
|
<flux:button wire:click="viewMedia({{ $media->id }})"
|
|
size="sm"
|
|
variant="white">
|
|
View
|
|
</flux:button>
|
|
<flux:button wire:click="editMedia({{ $media->id }})"
|
|
size="sm"
|
|
variant="white">
|
|
Edit
|
|
</flux:button>
|
|
<flux:button wire:click="deleteMedia({{ $media->id }})"
|
|
size="sm"
|
|
variant="danger">
|
|
Delete
|
|
</flux:button>
|
|
</div>
|
|
|
|
{{-- Selection Checkbox --}}
|
|
@if($selectionMode)
|
|
<div class="absolute top-2 left-2">
|
|
<flux:checkbox wire:model.live="selectedMedia"
|
|
value="{{ $media->id }}"
|
|
class="bg-white" />
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@empty
|
|
<div class="col-span-full text-center py-12">
|
|
<flux:icon.photo class="w-12 h-12 mx-auto text-gray-300 mb-4" />
|
|
<h3 class="text-lg font-medium text-gray-900 mb-2">No media files</h3>
|
|
<p class="text-gray-500 mb-4">Upload your first media file to get started.</p>
|
|
<flux:button wire:click="$dispatch('open-upload-modal')" variant="primary">
|
|
Upload Media
|
|
</flux:button>
|
|
</div>
|
|
@endforelse
|
|
</div>
|
|
|
|
{{-- Pagination --}}
|
|
@if($mediaItems->hasPages())
|
|
<div class="flex justify-center">
|
|
{{ $mediaItems->links() }}
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Upload Modal --}}
|
|
<flux:modal name="upload-modal" class="md:w-2xl">
|
|
<div class="space-y-6">
|
|
<flux:heading size="lg">Upload Media</flux:heading>
|
|
|
|
<div class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center"
|
|
x-data="{
|
|
isDragging: false,
|
|
handleDrop(e) {
|
|
this.isDragging = false;
|
|
// Handle file drop
|
|
}
|
|
}"
|
|
x-on:dragover.prevent="isDragging = true"
|
|
x-on:dragleave.prevent="isDragging = false"
|
|
x-on:drop.prevent="handleDrop"
|
|
:class="{ 'border-blue-400 bg-blue-50': isDragging }">
|
|
|
|
<flux:icon.cloud-arrow-up class="w-12 h-12 mx-auto text-gray-400 mb-4" />
|
|
<h3 class="text-lg font-medium text-gray-900 mb-2">Upload files</h3>
|
|
<p class="text-gray-500 mb-4">Drag and drop files here or click to browse</p>
|
|
|
|
<flux:button wire:click="triggerFileInput" variant="primary">
|
|
Choose Files
|
|
</flux:button>
|
|
|
|
<input type="file"
|
|
wire:model="uploadFiles"
|
|
multiple
|
|
accept="image/*,video/*,audio/*,.pdf,.doc,.docx"
|
|
class="hidden"
|
|
id="file-input">
|
|
</div>
|
|
|
|
{{-- Upload Progress --}}
|
|
@if($uploading)
|
|
<div class="space-y-3">
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-sm font-medium">Uploading...</span>
|
|
<span class="text-sm text-gray-500">{{ $uploadProgress }}%</span>
|
|
</div>
|
|
<div class="w-full bg-gray-200 rounded-full h-2">
|
|
<div class="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
|
style="width: {{ $uploadProgress }}%"></div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</flux:modal>
|
|
|
|
{{-- Media Detail Modal --}}
|
|
<flux:modal name="media-detail" class="md:w-4xl">
|
|
@if($viewingMedia)
|
|
<div class="space-y-6">
|
|
<flux:heading size="lg">{{ $viewingMedia->name }}</flux:heading>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{{-- Media Preview --}}
|
|
<div class="space-y-4">
|
|
@if(str_starts_with($viewingMedia->mime_type, 'image/'))
|
|
<img src="{{ $viewingMedia->getUrl() }}"
|
|
alt="{{ $viewingMedia->name }}"
|
|
class="w-full rounded-lg">
|
|
@else
|
|
<div class="bg-gray-100 rounded-lg p-8 text-center">
|
|
<flux:icon.document class="w-16 h-16 mx-auto text-gray-400 mb-4" />
|
|
<p class="text-gray-600">{{ $viewingMedia->name }}</p>
|
|
</div>
|
|
@endif
|
|
|
|
<flux:button href="{{ $viewingMedia->getUrl() }}"
|
|
target="_blank"
|
|
variant="outline"
|
|
class="w-full">
|
|
Download Original
|
|
</flux:button>
|
|
</div>
|
|
|
|
{{-- Media Details --}}
|
|
<div class="space-y-4">
|
|
<flux:field>
|
|
<flux:label>File Name</flux:label>
|
|
<flux:input wire:model.live="editingMedia.name" />
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:label>Alt Text</flux:label>
|
|
<flux:input wire:model.live="editingMedia.alt_text"
|
|
placeholder="Description for accessibility" />
|
|
</flux:field>
|
|
|
|
<flux:field>
|
|
<flux:label>Caption</flux:label>
|
|
<flux:textarea wire:model.live="editingMedia.caption"
|
|
rows="3"
|
|
placeholder="Optional caption" />
|
|
</flux:field>
|
|
|
|
<div class="grid grid-cols-2 gap-4 text-sm">
|
|
<div>
|
|
<span class="font-medium text-gray-700">File Type:</span>
|
|
<span class="text-gray-600">{{ $viewingMedia->mime_type }}</span>
|
|
</div>
|
|
<div>
|
|
<span class="font-medium text-gray-700">File Size:</span>
|
|
<span class="text-gray-600">{{ $this->formatFileSize($viewingMedia->size) }}</span>
|
|
</div>
|
|
<div>
|
|
<span class="font-medium text-gray-700">Uploaded:</span>
|
|
<span class="text-gray-600">{{ $viewingMedia->created_at->format('M j, Y') }}</span>
|
|
</div>
|
|
<div>
|
|
<span class="font-medium text-gray-700">Collection:</span>
|
|
<span class="text-gray-600">{{ $viewingMedia->collection_name }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex space-x-3 pt-4">
|
|
<flux:button wire:click="updateMedia" variant="primary">
|
|
Update
|
|
</flux:button>
|
|
<flux:button wire:click="$dispatch('close-modal')" variant="ghost">
|
|
Cancel
|
|
</flux:button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</flux:modal>
|
|
</div>
|
|
|
|
@push('scripts')
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// File input trigger
|
|
window.addEventListener('triggerFileInput', function() {
|
|
document.getElementById('file-input').click();
|
|
});
|
|
});
|
|
</script>
|
|
@endpush |