Files
ladill-transfer/resources/views/transfer/transfers/create.blade.php
T
isaaccladandCursor 6cd2a75498
Deploy Ladill Transfer / deploy (push) Successful in 31s
Fix transfer create uploads getting stuck and failing on submit.
All files now upload through chunked sessions only, the file picker no longer
rewrites the queue on change, and create submits upload_ids instead of a broken
multipart files field.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 13:36:27 +00:00

119 lines
8.4 KiB
PHP

<x-user-layout>
<x-slot name="title">New transfer</x-slot>
<div class="mx-auto max-w-2xl space-y-6">
<div>
<a href="{{ route('transfer.transfers.index') }}" class="text-sm text-slate-500 hover:text-slate-700"> All transfers</a>
<h1 class="mt-2 text-xl font-semibold text-slate-900">New transfer</h1>
<p class="mt-1 text-sm text-slate-500">Upload files and get a share link + QR code. Storage is billed monthly from your Ladill wallet while the transfer stays active.</p>
</div>
@if(session('error'))
<div class="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{{ session('error') }}</div>
@endif
<form method="post" action="{{ route('transfer.transfers.store') }}" enctype="multipart/form-data"
x-data="transferCreateForm(@js([
'initUrl' => route('transfer.transfers.upload.init'),
'chunkUrl' => route('transfer.transfers.upload.chunk'),
'finalizeUrl' => route('transfer.transfers.upload.finalize'),
'csrfToken' => csrf_token(),
]))"
@submit.prevent="submit"
class="space-y-5 rounded-2xl border border-slate-200 bg-white p-6">
@csrf
<div>
<label for="title" class="block text-sm font-medium text-slate-700">Title</label>
<input type="text" name="title" id="title" value="{{ old('title') }}" required maxlength="120"
class="mt-1 block w-full rounded-xl border-slate-200 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
@error('title')<p class="mt-1 text-xs text-red-600">{{ $message }}</p>@enderror
</div>
<div>
<label for="message" class="block text-sm font-medium text-slate-700">Message <span class="font-normal text-slate-400">(optional)</span></label>
<textarea name="message" id="message" rows="3" maxlength="2000"
class="mt-1 block w-full rounded-xl border-slate-200 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500">{{ old('message') }}</textarea>
</div>
<div>
<label for="files" class="block text-sm font-medium text-slate-700">Files</label>
<input type="file" id="files" multiple @change="onFilesChange"
class="mt-1 block w-full text-sm text-slate-600 file:mr-4 file:rounded-lg file:border-0 file:bg-indigo-50 file:px-4 file:py-2 file:text-sm file:font-semibold file:text-indigo-700 hover:file:bg-indigo-100">
<p class="mt-1 text-xs text-slate-500">Up to {{ $maxFiles }} files. Uploads start immediately pick more files anytime before creating the transfer.</p>
<div x-show="fileItems.length" x-cloak class="mt-3 space-y-2">
<template x-for="item in fileItems" :key="item.id">
<div class="rounded-xl border border-slate-200 bg-slate-50 px-3 py-2.5">
<div class="flex items-start gap-2">
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-slate-800" x-text="item.name"></p>
<p class="text-xs text-slate-500" x-text="formatBytes(item.size)"></p>
</div>
<span class="shrink-0 text-xs font-medium"
:class="{
'text-indigo-600': item.status === 'uploading' || item.status === 'pending',
'text-emerald-600': item.status === 'ready',
'text-red-600': item.status === 'error',
}"
x-text="item.status === 'uploading' ? `${item.progress}%` : (item.status === 'pending' ? 'Waiting…' : (item.status === 'ready' ? 'Ready' : 'Failed'))"></span>
<button type="button"
@click="removeFileItem(item.id)"
:disabled="item.status === 'uploading'"
class="shrink-0 rounded p-0.5 text-slate-400 hover:bg-white hover:text-rose-600 disabled:opacity-40"
title="Remove">
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" d="M6 18 18 6M6 6l12 12"/></svg>
</button>
</div>
<div x-show="item.status === 'uploading'" class="mt-2">
<div class="upload-progress-track">
<div class="upload-progress-bar" :style="`width: ${item.progress}%`"></div>
</div>
</div>
<p x-show="item.status === 'error'" class="mt-1 text-xs text-red-600" x-text="item.error"></p>
</div>
</template>
<div x-show="uploading" class="rounded-xl border border-indigo-100 bg-indigo-50/70 px-3 py-2">
<div class="flex items-center justify-between gap-2 text-xs text-indigo-800">
<span class="font-medium">Uploading files…</span>
<span x-text="`${overallProgress}%`"></span>
</div>
<div class="upload-progress-track mt-2">
<div class="upload-progress-bar" :style="`width: ${overallProgress}%`"></div>
</div>
</div>
</div>
@error('files')<p class="mt-1 text-xs text-red-600">{{ $message }}</p>@enderror
@error('files.*')<p class="mt-1 text-xs text-red-600">{{ $message }}</p>@enderror
</div>
<div>
<label for="recipient_email" class="block text-sm font-medium text-slate-700">Recipient email <span class="font-normal text-slate-400">(optional)</span></label>
<input type="email" name="recipient_email" id="recipient_email" value="{{ old('recipient_email') }}" maxlength="255"
placeholder="friend@example.com"
class="mt-1 block w-full rounded-xl border-slate-200 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
@error('recipient_email')<p class="mt-1 text-xs text-red-600">{{ $message }}</p>@enderror
<p class="mt-1 text-xs text-slate-500">A download link is emailed immediately when you create the transfer.</p>
</div>
<div class="rounded-xl border border-indigo-100 bg-indigo-50/60 px-4 py-3 text-sm text-slate-600">
<p class="font-medium text-slate-900">Monthly storage billing</p>
<p class="mt-1">GHS {{ number_format($pricePerGb, 2) }} per GB per month, debited from your wallet. Files stay available while payments succeed. If a monthly renewal fails, files are kept for {{ $gracePeriodDays }} days before deletion.</p>
</div>
<div>
<label for="password" class="block text-sm font-medium text-slate-700">Password <span class="font-normal text-slate-400">(optional)</span></label>
<input type="password" name="password" id="password" minlength="4" maxlength="64"
class="mt-1 block w-full rounded-xl border-slate-200 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
autocomplete="new-password">
</div>
<div class="flex justify-end gap-3 pt-2">
<a href="{{ route('transfer.transfers.index') }}" class="rounded-xl border border-slate-200 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Cancel</a>
<button type="submit" class="rounded-xl bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-700 disabled:opacity-60"
:disabled="uploading || submitting">
<span x-show="!uploading && !submitting">Create transfer</span>
<span x-show="uploading || submitting" x-cloak>Uploading…</span>
</button>
</div>
</form>
</div>
</x-user-layout>