Files
ladill-transfer/resources/views/transfer/files/index.blade.php
T
isaaccladandCursor 19170d6bc7
Deploy Ladill Transfer / deploy (push) Successful in 29s
Add transfer search and OneDrive-style Files storage UI.
Transfers now lists all shares with filters and pagination; Files becomes a selectable cloud view with download, share, delete, move, and open-in-email actions.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 20:59:05 +00:00

249 lines
17 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.
<x-user-layout>
<x-slot name="title">Files</x-slot>
@php
$fmtBytes = function (int $bytes) {
if ($bytes >= 1073741824) return number_format($bytes / 1073741824, 2).' GB';
if ($bytes >= 1048576) return number_format($bytes / 1048576, 1).' MB';
return number_format($bytes / 1024, 0).' KB';
};
$fileIcon = function (\App\Models\TransferFile $file): string {
$mime = (string) $file->mime_type;
$ext = strtolower(pathinfo($file->original_name, PATHINFO_EXTENSION));
if (str_starts_with($mime, 'image/') || in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'], true)) {
return 'image';
}
if (str_starts_with($mime, 'video/') || in_array($ext, ['mp4', 'mov', 'avi', 'mkv'], true)) {
return 'video';
}
if (str_starts_with($mime, 'audio/') || in_array($ext, ['mp3', 'wav', 'aac'], true)) {
return 'audio';
}
if (in_array($ext, ['zip', 'rar', '7z', 'tar', 'gz'], true)) {
return 'archive';
}
if (in_array($ext, ['pdf', 'doc', 'docx', 'txt', 'rtf'], true)) {
return 'document';
}
return 'file';
};
$fileRows = $files->map(fn ($file) => [
'id' => $file->id,
'name' => $file->original_name,
'size' => $file->humanSize(),
'downloads' => $file->downloads_total,
'modified' => $file->updated_at?->format('M j, Y') ?? '—',
'transferId' => $file->transfer_id,
'transferTitle' => $file->transfer->title,
'shareUrl' => $file->transfer->qrCode?->publicUrl() ?? '',
'downloadUrl' => route('transfer.files.download', $file),
'icon' => $fileIcon($file),
'shared' => $file->transfer->recipient_email !== null || $file->downloads_total > 0,
])->values();
@endphp
<div class="space-y-4"
x-data="filesManager(@js([
'fileIds' => $fileRows->pluck('id')->all(),
'files' => $fileRows->all(),
'csrf' => csrf_token(),
'routes' => [
'bulkDownload' => route('transfer.files.bulk-download'),
'bulkDelete' => route('transfer.files.bulk-delete'),
'move' => route('transfer.files.move'),
'share' => route('transfer.files.share'),
'openInEmail' => route('transfer.files.open-in-email'),
'reshareEmail' => route('transfer.files.reshare-email'),
],
'moveTargets' => $moveTargets->map(fn ($t) => ['id' => $t->id, 'title' => $t->title])->values()->all(),
]))">
{{-- Header --}}
<div class="flex flex-wrap items-start justify-between gap-4">
<div>
<h1 class="text-xl font-semibold text-slate-900">Files</h1>
<p class="mt-1 text-sm text-slate-500">{{ $fmtBytes($storageBytes) }} stored (~GHS {{ number_format($storageGb * $pricePerGb, 2) }}/month)</p>
</div>
</div>
{{-- Breadcrumbs --}}
<nav class="flex flex-wrap items-center gap-1 text-sm text-slate-500">
<a href="{{ route('transfer.files.index', request()->except('folder')) }}" class="font-medium text-indigo-600 hover:text-indigo-800">My files</a>
@if($folderTransfer)
<span>/</span>
<span class="font-medium text-slate-900">{{ $folderTransfer->title }}</span>
@endif
</nav>
{{-- Action toolbar --}}
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white">
<div class="flex flex-wrap items-center gap-2 border-b border-slate-100 px-4 py-2.5">
<template x-if="selected.length === 0">
<div class="flex flex-wrap items-center gap-2 text-sm">
<button type="button" disabled class="rounded-lg px-3 py-1.5 text-slate-400">Download</button>
<button type="button" disabled class="rounded-lg px-3 py-1.5 text-slate-400">Share</button>
<button type="button" disabled class="rounded-lg px-3 py-1.5 text-slate-400">Copy link</button>
<button type="button" disabled class="rounded-lg px-3 py-1.5 text-slate-400">Delete</button>
<button type="button" disabled class="rounded-lg px-3 py-1.5 text-slate-400">Move to</button>
<button type="button" disabled class="rounded-lg px-3 py-1.5 text-slate-400">Open in email</button>
</div>
</template>
<template x-if="selected.length > 0">
<div class="flex flex-wrap items-center gap-2 text-sm">
<button type="button" @click="downloadSelected()" class="rounded-lg px-3 py-1.5 font-medium text-slate-700 hover:bg-slate-100">Download</button>
<button type="button" @click="shareSelected()" class="rounded-lg px-3 py-1.5 font-medium text-slate-700 hover:bg-slate-100">Share</button>
<button type="button" @click="copyLinks()" class="rounded-lg px-3 py-1.5 font-medium text-slate-700 hover:bg-slate-100">Copy link</button>
<button type="button" @click="deleteSelected()" class="rounded-lg px-3 py-1.5 font-medium text-red-700 hover:bg-red-50">Delete</button>
<button type="button" @click="moveModalOpen = true" class="rounded-lg px-3 py-1.5 font-medium text-slate-700 hover:bg-slate-100">Move to</button>
<button type="button" @click="openInEmail()" class="rounded-lg bg-indigo-600 px-3 py-1.5 font-medium text-white hover:bg-indigo-700">Open in email</button>
<span class="ml-2 text-xs text-slate-500" x-text="`${selected.length} selected`"></span>
<button type="button" @click="clearSelection()" class="text-xs text-slate-400 hover:text-slate-600">Clear</button>
</div>
</template>
<div class="ml-auto flex flex-wrap items-center gap-2">
<form method="get" action="{{ route('transfer.files.index') }}" class="flex items-center gap-2">
@if($folderTransfer)
<input type="hidden" name="folder" value="{{ $folderTransfer->id }}">
@endif
<input type="search" name="q" value="{{ $search }}" placeholder="Search files…"
class="w-40 rounded-lg border border-slate-200 px-2.5 py-1.5 text-xs focus:border-indigo-300 focus:outline-none focus:ring-2 focus:ring-indigo-100 sm:w-52">
<select name="sort" onchange="this.form.submit()" class="rounded-lg border border-slate-200 px-2 py-1.5 text-xs text-slate-700">
<option value="newest" @selected($sort === 'newest')>Newest</option>
<option value="oldest" @selected($sort === 'oldest')>Oldest</option>
<option value="name" @selected($sort === 'name')>Name</option>
<option value="size" @selected($sort === 'size')>Size</option>
<option value="downloads" @selected($sort === 'downloads')>Downloads</option>
</select>
</form>
</div>
</div>
{{-- Folders row (when at root) --}}
@if(! $folderTransfer && $folders->isNotEmpty() && $search === '')
<div class="border-b border-slate-100 px-4 py-3">
<p class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-400">Folders</p>
<div class="flex flex-wrap gap-2">
@foreach($folders as $folder)
<a href="{{ route('transfer.files.index', ['folder' => $folder->id]) }}"
class="inline-flex items-center gap-2 rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-700 transition hover:border-indigo-200 hover:bg-indigo-50/60">
<svg class="h-5 w-5 text-amber-500" fill="currentColor" viewBox="0 0 20 20"><path d="M2 6a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6Z"/></svg>
<span class="font-medium">{{ $folder->title }}</span>
<span class="text-xs text-slate-400">{{ $folder->files_count }}</span>
</a>
@endforeach
</div>
</div>
@endif
@if($files->isEmpty())
<div class="px-6 py-12 text-center text-sm text-slate-500">
@if($search !== '')
No files match your search.
@elseif($folderTransfer)
This folder is empty.
@else
No stored files yet.
@endif
</div>
@else
<table class="min-w-full divide-y divide-slate-100 text-sm">
<thead class="bg-slate-50 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">
<tr>
<th class="w-10 px-4 py-3">
<input type="checkbox" @change="toggleAll($event)" :checked="allSelected" class="rounded border-slate-300 text-indigo-600 focus:ring-indigo-500">
</th>
<th class="px-4 py-3">Name</th>
<th class="hidden px-4 py-3 sm:table-cell">Modified</th>
<th class="px-4 py-3">Size</th>
<th class="hidden px-4 py-3 md:table-cell">Sharing</th>
@unless($folderTransfer)
<th class="hidden px-4 py-3 lg:table-cell">Folder</th>
@endunless
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
@foreach($files as $file)
@php $icon = $fileIcon($file); @endphp
<tr class="transition"
:class="selected.includes({{ $file->id }}) ? 'bg-indigo-50/80' : 'hover:bg-slate-50/80'">
<td class="px-4 py-3">
<input type="checkbox" value="{{ $file->id }}" x-model.number="selected" class="rounded border-slate-300 text-indigo-600 focus:ring-indigo-500">
</td>
<td class="px-4 py-3">
<div class="flex items-center gap-3">
@include('transfer.files.partials.file-icon', ['icon' => $icon])
<div class="min-w-0">
<p class="truncate font-medium text-slate-900">{{ $file->original_name }}</p>
<div class="mt-0.5 flex items-center gap-2 sm:hidden">
<span class="text-xs text-slate-500">{{ $file->humanSize() }}</span>
</div>
</div>
<div class="ml-auto flex items-center gap-1 opacity-0 transition group-hover:opacity-100 sm:opacity-100" x-show="selected.includes({{ $file->id }})">
<a href="{{ route('transfer.files.download', $file) }}" class="rounded p-1 text-slate-500 hover:bg-white hover:text-indigo-600" title="Download">
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 7.5 12 3m0 0L7.5 7.5M12 3v13.5"/></svg>
</a>
</div>
</div>
</td>
<td class="hidden px-4 py-3 text-slate-600 sm:table-cell">{{ $file->updated_at?->format('M j, Y') ?? '—' }}</td>
<td class="px-4 py-3 text-slate-600">{{ $file->humanSize() }}</td>
<td class="hidden px-4 py-3 md:table-cell">
@if($file->transfer->recipient_email || $file->downloads_total > 0)
<span class="inline-flex items-center gap-1 text-xs text-slate-600">
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z"/></svg>
Shared
</span>
@else
<span class="text-xs text-slate-400">Private</span>
@endif
</td>
@unless($folderTransfer)
<td class="hidden px-4 py-3 lg:table-cell">
<a href="{{ route('transfer.files.index', ['folder' => $file->transfer_id]) }}" class="text-indigo-600 hover:text-indigo-800">{{ $file->transfer->title }}</a>
</td>
@endunless
</tr>
@endforeach
</tbody>
</table>
<div class="flex items-center justify-between border-t border-slate-100 px-4 py-3 text-xs text-slate-500">
<span>{{ $files->total() }} file{{ $files->total() === 1 ? '' : 's' }}</span>
<span>Showing {{ $files->firstItem() }}{{ $files->lastItem() }}</span>
</div>
@endif
</div>
@if($files->hasPages())
<div>{{ $files->links() }}</div>
@endif
{{-- Move modal --}}
<div x-show="moveModalOpen" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4" @keydown.escape.window="moveModalOpen = false">
<div @click.outside="moveModalOpen = false" class="w-full max-w-md rounded-2xl bg-white p-6 shadow-xl">
<h3 class="text-lg font-semibold text-slate-900">Move to folder</h3>
<p class="mt-1 text-sm text-slate-500">Group selected files into another transfer folder.</p>
<form method="post" action="{{ route('transfer.files.move') }}" class="mt-4 space-y-4">
@csrf
<template x-for="id in selected" :key="id">
<input type="hidden" name="files[]" :value="id">
</template>
<select name="destination" required class="w-full rounded-xl border border-slate-200 px-3 py-2 text-sm focus:border-indigo-300 focus:outline-none focus:ring-2 focus:ring-indigo-100">
<option value="">Choose a transfer folder…</option>
@foreach($moveTargets as $target)
<option value="{{ $target->id }}">{{ $target->title }}</option>
@endforeach
</select>
<div class="flex justify-end gap-2">
<button type="button" @click="moveModalOpen = false" class="rounded-xl px-4 py-2 text-sm font-medium text-slate-600 hover:bg-slate-100">Cancel</button>
<button type="submit" class="rounded-xl bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-700">Move</button>
</div>
</form>
</div>
</div>
{{-- Toast --}}
<div x-show="toast" x-cloak x-transition class="fixed bottom-24 left-1/2 z-50 -translate-x-1/2 rounded-xl bg-slate-900 px-4 py-2 text-sm text-white shadow-lg lg:bottom-8" x-text="toast"></div>
</div>
</x-user-layout>