General
File Upload code
Copy the implementation or inspect every file that belongs to this component unit.
resources/views/components/ui/file-upload.blade.php
@props([
'name' => 'file',
'accept' => null,
'multiple' => false,
'hint' => null,
'icon' => 'cloud-arrow-up',
'invalid' => false,
'disabled' => false,
'preview' => false,
'removable' => true,
'maxSize' => null,
'progress' => null,
])
@php
/*
| Drag-and-drop file dropzone wrapping a native file input. Shows picked
| files, optional image previews, remove controls and upload progress.
|
| Usage:
| <x-ui.file-upload name="avatar" accept="image/*" preview max-size="5120" />
| <x-ui.file-upload name="docs[]" multiple :progress="64" />
*/
$ring = $invalid
? 'border-red-400 dark:border-red-500'
: 'border-gray-300 dark:border-gray-700';
@endphp
<div
x-data="{
files: [],
errors: [],
dragging: false,
disabled: @js((bool) $disabled),
preview: @js((bool) $preview),
maxSize: @js($maxSize ? (int) $maxSize : null),
syncInput() {
const transfer = new DataTransfer();
this.files.forEach((file) => transfer.items.add(file.raw));
this.$refs.input.files = transfer.files;
},
handle(fileList) {
this.errors = [];
const accepted = [];
Array.from(fileList).forEach((file) => {
if (this.maxSize && file.size > this.maxSize * 1024) {
this.errors.push(`${file.name} is larger than ${this.humanSize(this.maxSize * 1024)}.`);
return;
}
accepted.push({
raw: file,
name: file.name,
size: file.size,
type: file.type,
previewUrl: this.preview && file.type.startsWith('image/') ? URL.createObjectURL(file) : null,
});
});
this.files = @js((bool) $multiple) ? accepted : accepted.slice(0, 1);
this.syncInput();
},
remove(index) {
const file = this.files[index];
if (file?.previewUrl) { URL.revokeObjectURL(file.previewUrl); }
this.files.splice(index, 1);
this.syncInput();
},
humanSize(bytes) {
if (bytes < 1024) { return bytes + ' B'; }
if (bytes < 1048576) { return (bytes / 1024).toFixed(1) + ' KB'; }
return (bytes / 1048576).toFixed(1) + ' MB';
},
}"
{{ $attributes->only('class') }}
>
<label
@dragover.prevent="if (! disabled) dragging = true"
@dragleave.prevent="dragging = false"
@drop.prevent="dragging = false; if (! disabled) { handle($event.dataTransfer.files); }"
:class="dragging ? 'border-brand-500 bg-brand-50/60 dark:bg-brand-950/30' : '{{ $ring }}'"
class="flex flex-col items-center justify-center gap-2 rounded-2xl border-2 border-dashed bg-gray-50/60 px-6 py-10 text-center transition dark:bg-gray-900/40 {{ $disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:border-gray-400 dark:hover:border-gray-600' }}"
>
<span class="flex h-12 w-12 items-center justify-center rounded-2xl bg-white text-gray-400 shadow-sm dark:bg-gray-800 dark:text-gray-500">
<x-ui.icon :name="$icon" size="2xl" />
</span>
<span class="text-sm text-gray-600 dark:text-gray-300">
<span class="font-semibold text-brand-600 dark:text-brand-400">Click to upload</span> or drag and drop
</span>
@if ($hint)
<span class="text-xs text-gray-400 dark:text-gray-500">{{ $hint }}</span>
@endif
@if ($maxSize)
<span class="text-xs text-gray-400 dark:text-gray-500">Max {{ number_format((int) $maxSize) }} KB</span>
@endif
<input
type="file"
x-ref="input"
name="{{ $name }}"
@if ($accept) accept="{{ $accept }}" @endif
@if ($multiple) multiple @endif
@disabled($disabled)
@if ($invalid) aria-invalid="true" @endif
@change="handle($event.target.files)"
class="sr-only"
>
</label>
@if ($progress !== null)
<div class="mt-3">
<x-ui.progress :value="$progress" label="Upload progress" show-value />
</div>
@endif
<ul x-show="files.length" x-cloak class="mt-3 space-y-2">
<template x-for="(file, index) in files" :key="file.name + index">
<li class="flex items-center justify-between gap-3 rounded-lg border border-gray-200 bg-white px-3 py-2 dark:border-gray-800 dark:bg-gray-900">
<span class="flex min-w-0 items-center gap-3">
<template x-if="file.previewUrl">
<img :src="file.previewUrl" alt="" class="h-10 w-10 shrink-0 rounded-lg object-cover ring-1 ring-gray-200 dark:ring-gray-800">
</template>
<template x-if="! file.previewUrl">
<x-ui.icon name="file" size="base" class="shrink-0 text-gray-400" />
</template>
<span class="min-w-0">
<span class="block truncate text-sm text-gray-700 dark:text-gray-200" x-text="file.name"></span>
<span class="block text-xs text-gray-400 dark:text-gray-500" x-text="humanSize(file.size)"></span>
</span>
</span>
@if ($removable)
<button type="button" @click="remove(index)" class="shrink-0 rounded-lg p-1.5 text-gray-400 transition hover:bg-gray-100 hover:text-red-600 dark:hover:bg-gray-800 dark:hover:text-red-400" aria-label="Remove file">
<x-ui.icon name="x" weight="bold" size="sm" />
</button>
@endif
</li>
</template>
</ul>
<ul x-show="errors.length" x-cloak class="mt-3 space-y-1 text-sm text-red-600 dark:text-red-400">
<template x-for="error in errors" :key="error">
<li class="flex items-center gap-1.5">
<x-ui.icon name="warning-circle" weight="fill" size="sm" />
<span x-text="error"></span>
</li>
</template>
</ul>
</div>