Skip to main content

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>