Files
Kol-Manager/resources/views/files/index.blade.php
I-Fun 1b90f730a6 feat: add notes management feature with CRUD operations
- Created Note model and controller for handling notes.
- Added routes for notes CRUD operations.
- Implemented notes index view with search and pinning functionality.
- Created migration for notes table with necessary fields.
- Developed event creation form with client and contract options.
2026-04-02 23:24:38 +07:00

352 lines
16 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@extends('layouts.app')
@section('title', 'Files')
@section('page-title', 'File Manager 📁')
@section('page-subtitle', 'Brand assets, logos, and media files')
@section('content')
{{-- ── Video Player Modal ───────────────────────────────────── --}}
<div id="video-player-modal"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 hidden"
onclick="if(event.target===this)closeVideoModal()">
<div class="relative w-full max-w-3xl mx-4">
<div class="flex items-center justify-between mb-3 px-1">
<p class="text-white text-sm font-semibold truncate" id="vpm-title"></p>
<button onclick="closeVideoModal()"
class="text-white/70 hover:text-white text-2xl leading-none ml-4 shrink-0">✕</button>
</div>
<video id="vpm-player"
class="w-full rounded-2xl bg-black shadow-2xl"
controls controlsList="nodownload"
style="max-height:70vh">
</video>
<div class="flex items-center justify-between mt-3 px-1 text-xs text-white/60">
<span id="vpm-meta"></span>
<a id="vpm-dl" href="#" target="_blank"
class="text-pink-300 hover:text-pink-200 font-medium">⬇️ Download</a>
</div>
</div>
</div>
{{-- ── File Detail Popup Modal ──────────────────────────────── --}}
<div id="file-detail-modal"
class="fixed inset-0 z-50 flex items-end sm:items-center justify-center bg-black/40 hidden"
onclick="if(event.target===this)this.classList.add('hidden')">
<div class="cat-card p-5 w-full sm:max-w-sm rounded-b-none sm:rounded-3xl shadow-xl mx-0 sm:mx-4">
<div class="w-10 h-1 bg-gray-200 rounded-full mx-auto mb-4 sm:hidden"></div>
<div class="flex items-center justify-between mb-3">
<h3 class="font-bold text-gray-800 text-base" id="fdm-name">File</h3>
<button onclick="document.getElementById('file-detail-modal').classList.add('hidden')"
class="text-gray-400 hover:text-gray-600 text-xl leading-none">✕</button>
</div>
<div id="fdm-preview" class="mb-3 flex justify-center"></div>
<div class="text-sm text-gray-600 space-y-1.5" id="fdm-body"></div>
<div class="flex gap-2 mt-4">
<a id="fdm-view-btn" href="#" target="_blank"
class="btn-sakura flex-1 text-center py-2 text-sm">View / Download</a>
<button onclick="document.getElementById('file-detail-modal').classList.add('hidden')"
class="flex-1 py-2 rounded-full border border-pink-200 text-gray-600 text-sm hover:bg-pink-50">Close</button>
</div>
</div>
</div>
{{-- ── Upload Form (collapsible) ────────────────────────────── --}}
<div class="cat-card p-4 mb-4" id="upload-panel"
x-data="{
open: false,
uploading: false,
progress: 0,
statusMsg: '',
statusOk: true,
submitUpload(form) {
const fd = new FormData(form);
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
this.progress = Math.round((e.loaded / e.total) * 100);
}
});
xhr.addEventListener('load', () => {
this.uploading = false;
if (xhr.status >= 200 && xhr.status < 300) {
this.statusOk = true;
this.statusMsg = '✅ File uploaded successfully!';
this.progress = 100;
form.reset();
setTimeout(() => {
this.open = false;
this.statusMsg = '';
this.progress = 0;
window.location.reload();
}, 1200);
} else {
this.statusOk = false;
try {
const body = JSON.parse(xhr.responseText);
this.statusMsg = body.message ?? 'Upload failed. Please try again.';
} catch {
this.statusMsg = 'Upload failed. Please try again.';
}
}
});
xhr.addEventListener('error', () => {
this.uploading = false;
this.statusOk = false;
this.statusMsg = 'Network error. Please try again.';
});
xhr.open('POST', form.action);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.setRequestHeader('Accept', 'application/json');
this.uploading = true;
this.progress = 0;
this.statusMsg = '';
xhr.send(fd);
}
}">
<div class="flex items-center justify-between">
<h3 class="text-sm font-bold text-gray-700 flex items-center gap-2">📤 Upload File</h3>
<button class="btn-sakura text-xs px-4 py-1.5" @click="open = !open; statusMsg = ''; progress = 0;"
x-text="open ? '✕ Close' : '+ Upload'"></button>
</div>
<div x-show="open" x-transition>
<form action="{{ route('files.store') }}" method="POST" enctype="multipart/form-data"
class="mt-4 grid grid-cols-1 sm:grid-cols-3 gap-3"
@submit.prevent="submitUpload($el)">
@csrf
<div class="sm:col-span-3">
<label class="block text-xs font-semibold text-gray-600 mb-1">
File <span class="text-pink-400">*</span>
<span class="text-gray-400 font-normal ml-1">Max 50 MB</span>
</label>
<input type="file" name="file" required
class="w-full text-sm text-gray-600
file:mr-3 file:py-2 file:px-4
file:rounded-full file:border-0
file:text-xs file:font-semibold
file:bg-pink-50 file:text-pink-600
hover:file:bg-pink-100
cursor-pointer">
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Folder</label>
<input type="text" name="folder" value="general" class="input-soft text-sm"
list="folder-list" placeholder="brand-assets, logos...">
<datalist id="folder-list">
@foreach($folders as $f)
<option value="{{ $f }}">
@endforeach
<option value="brand-assets">
<option value="logos">
<option value="videos">
<option value="photos">
<option value="documents">
</datalist>
</div>
<div>
<label class="block text-xs font-semibold text-gray-600 mb-1">Client (optional)</label>
<select name="client_id" class="input-soft text-sm">
<option value="">— None —</option>
@foreach($clients as $client)
<option value="{{ $client->id }}">{{ $client->name }}</option>
@endforeach
</select>
</div>
<div class="sm:flex sm:items-end">
<button type="submit" class="btn-sakura w-full py-2.5" :disabled="uploading">
<span x-show="!uploading">📤 Upload</span>
<span x-show="uploading">Uploading…</span>
</button>
</div>
</form>
{{-- Progress Bar --}}
<div x-show="uploading || progress > 0" x-transition class="mt-3">
<div class="flex items-center justify-between text-xs text-gray-500 mb-1">
<span>Uploading…</span>
<span x-text="progress + '%'"></span>
</div>
<div class="w-full bg-pink-100 rounded-full h-2.5 overflow-hidden">
<div class="h-2.5 rounded-full transition-all duration-200"
style="background:#FFB7C5"
:style="'width:' + progress + '%'"></div>
</div>
</div>
{{-- Status Message --}}
<div x-show="statusMsg" x-transition
class="mt-3 px-4 py-2.5 rounded-xl text-sm font-medium"
:class="statusOk ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-600'"
x-text="statusMsg">
</div>
</div>
</div>
{{-- ── Folder Filter Chips ──────────────────────────────────── --}}
<div class="flex gap-2 overflow-x-auto pb-2 mb-4 -mx-4 px-4 lg:mx-0 lg:px-0">
<a href="{{ route('files.index') }}"
class="shrink-0 px-3 py-1.5 rounded-full text-xs font-semibold transition-colors
{{ !request('folder') || request('folder') === 'all'
? 'text-white' : 'bg-white text-gray-500 border border-pink-100 hover:bg-pink-50' }}"
style="{{ !request('folder') || request('folder') === 'all' ? 'background:#FFB7C5' : '' }}">
All
</a>
@foreach($folders as $f)
<a href="{{ request()->fullUrlWithQuery(['folder' => $f]) }}"
class="shrink-0 px-3 py-1.5 rounded-full text-xs font-semibold capitalize transition-colors
{{ request('folder') === $f
? 'text-white' : 'bg-white text-gray-500 border border-pink-100 hover:bg-pink-50' }}"
style="{{ request('folder') === $f ? 'background:#FFB7C5' : '' }}">
{{ $f }}
</a>
@endforeach
</div>
{{-- ── File Grid ────────────────────────────────────────────── --}}
@forelse($files as $file)
@if($loop->first)
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3">
@endif
@php $fileData = ['name' => $file->name ?: $file->original_name, 'url' => $file->url, 'size' => $file->human_size, 'folder' => $file->folder, 'client' => $file->client?->name, 'is_image' => $file->is_image, 'is_video' => $file->is_video, 'icon' => $file->icon, 'created_at' => $file->created_at->format('d M Y')] @endphp
<div class="cat-card p-3 flex flex-col gap-2">
{{-- Preview / Icon --}}
@if($file->is_image)
<div class="w-full aspect-square rounded-2xl overflow-hidden bg-pink-50 cursor-pointer"
onclick="showFileDetail({{ json_encode($fileData) }})">
<img src="{{ $file->url }}" alt="{{ $file->name }}"
class="w-full h-full object-cover">
</div>
@elseif($file->is_video)
<div class="w-full aspect-square rounded-2xl overflow-hidden bg-gray-900 relative cursor-pointer group"
onclick="openVideoPlayer({{ json_encode($fileData) }})">
{{-- Native video poster (browser generates a frame) --}}
<video src="{{ $file->url }}"
class="w-full h-full object-cover opacity-80"
preload="metadata"
muted playsinline
style="pointer-events:none">
</video>
{{-- Play overlay --}}
<div class="absolute inset-0 flex items-center justify-center
bg-black/20 group-hover:bg-black/40 transition-colors">
<div class="w-12 h-12 rounded-full bg-white/90 flex items-center justify-center shadow-lg
group-hover:scale-110 transition-transform">
<span class="text-gray-800 text-xl ml-0.5">▶</span>
</div>
</div>
</div>
@else
<div class="w-full aspect-square rounded-2xl bg-pink-50 flex items-center justify-center text-4xl cursor-pointer"
onclick="showFileDetail({{ json_encode($fileData) }})">
{{ $file->icon }}
</div>
@endif
{{-- Info --}}
<div class="min-w-0">
<p class="text-xs font-semibold text-gray-700 truncate" title="{{ $file->original_name }}">
{{ $file->name ?: $file->original_name }}
</p>
<p class="text-xs text-gray-400">{{ $file->human_size }}</p>
@if($file->client)
<p class="text-xs text-pink-500 truncate">👥 {{ $file->client->name }}</p>
@endif
<span class="text-xs bg-pink-50 text-pink-600 px-2 py-0.5 rounded-full capitalize inline-block mt-0.5">
{{ $file->folder }}
</span>
</div>
{{-- Actions --}}
<div class="flex gap-1.5 mt-auto">
<a href="{{ $file->url }}" target="_blank"
class="flex-1 text-center text-xs py-1.5 rounded-full border border-pink-200 text-pink-600 hover:bg-pink-50 font-medium">
View
</a>
<form action="{{ route('files.destroy', $file) }}" method="POST"
onsubmit="return confirm('Delete this file?')">
@csrf @method('DELETE')
<button class="text-xs py-1.5 px-2.5 rounded-full border border-red-100 text-red-400 hover:bg-red-50">
✕
</button>
</form>
</div>
</div>
@if($loop->last)
</div>
@endif
@empty
<div class="text-center py-16 text-gray-400">
<span class="text-5xl block mb-3">📁</span>
<p class="text-sm">No files yet.</p>
<button onclick="document.getElementById('upload-panel')._x_dataStack[0].open = true; document.getElementById('upload-panel').scrollIntoView({behavior:'smooth'})"
class="btn-sakura mt-4 inline-block text-sm">Upload your first file</button>
</div>
@endforelse
<div class="mt-4">{{ $files->links() }}</div>
@push('scripts')
<script>
function showFileDetail(file) {
document.getElementById('fdm-name').textContent = file.name;
document.getElementById('fdm-view-btn').href = file.url;
const preview = document.getElementById('fdm-preview');
if (file.is_image) {
preview.innerHTML = `<img src="${file.url}" alt="${file.name}" class="max-h-48 rounded-2xl object-contain border border-pink-100">`;
} else {
preview.innerHTML = `<span class="text-6xl">${file.icon}</span>`;
}
const rows = [
['Size', file.size],
['Folder', file.folder],
file.client ? ['Client', file.client] : null,
['Uploaded', file.created_at],
].filter(Boolean);
document.getElementById('fdm-body').innerHTML = rows.map(([label, value]) =>
`<div class="flex gap-2"><span class="text-gray-400 w-16 shrink-0">${label}</span><span class="font-medium text-gray-700 truncate">${value}</span></div>`
).join('');
document.getElementById('file-detail-modal').classList.remove('hidden');
}
function openVideoPlayer(file) {
const player = document.getElementById('vpm-player');
player.src = file.url;
player.load();
document.getElementById('vpm-title').textContent = file.name;
document.getElementById('vpm-meta').textContent = file.size + (file.client ? ' · ' + file.client : '');
document.getElementById('vpm-dl').href = file.url;
document.getElementById('video-player-modal').classList.remove('hidden');
document.body.style.overflow = 'hidden';
}
function closeVideoModal() {
const player = document.getElementById('vpm-player');
player.pause();
player.src = '';
document.getElementById('video-player-modal').classList.add('hidden');
document.body.style.overflow = '';
}
// Close video modal on Escape
document.addEventListener('keydown', e => {
if (e.key === 'Escape') closeVideoModal();
});
</script>
@endpush
@endsection