Redesign file uploads: drag-and-drop zones for images and documents
Deploy to LXC / deploy (push) Successful in 20s

New reusable components:
- ImageUpload.svelte: drag-and-drop / click / paste zone with file
  icon, preview of selected filename, caption field, 50MB limit
- DocumentUpload.svelte: same pattern for documents with description
  field instead of caption

Applied to both device and component detail pages, replacing the old
inline file input forms. Cleaner look matching modern upload UIs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-09 16:23:30 +07:00
parent 7429130630
commit 36f4d4b8d5
4 changed files with 219 additions and 76 deletions
+7 -38
View File
@@ -2,6 +2,8 @@
import { enhance } from '$app/forms';
import { COMPONENT_CONDITIONS } from '$lib/constants.js';
import { formatDate, timeAgo } from '$lib/utils/date.js';
import ImageUpload from '$lib/components/ui/ImageUpload.svelte';
import DocumentUpload from '$lib/components/ui/DocumentUpload.svelte';
let { data, form } = $props();
const c = $derived(data.component);
@@ -88,30 +90,9 @@
<div class="space-y-6 lg:col-span-2">
<!-- Images -->
<div class="rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
<div class="mb-3 flex items-center justify-between">
<h2 class="text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Images</h2>
<button type="button" onclick={() => (showUploadForm = !showUploadForm)}
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400">
{showUploadForm ? 'Cancel' : 'Upload'}
</button>
</div>
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Images</h2>
{#if showUploadForm}
<form method="POST" action="?/uploadImage" enctype="multipart/form-data" use:enhance class="mb-4 flex flex-wrap items-end gap-3">
<input type="file" name="image" accept="image/*" required
onchange={(e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file && file.size > 50 * 1024 * 1024) {
alert('File too large. Maximum size is 50MB.');
(e.target as HTMLInputElement).value = '';
}
}}
class="text-sm text-gray-600 dark:text-gray-400" />
<input type="text" name="caption" placeholder="Caption (optional)"
class="rounded-md border border-gray-300 px-3 py-1.5 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
<button type="submit" class="rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700">Upload</button>
</form>
{/if}
<ImageUpload action="?/uploadImage" />
{#if data.images.length === 0}
<p class="text-sm text-gray-500 dark:text-gray-400">No images yet.</p>
@@ -330,21 +311,9 @@
<!-- Documents -->
<div class="rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
<div class="mb-3 flex items-center justify-between">
<h2 class="text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Documents</h2>
<button type="button" onclick={() => (showDocForm = !showDocForm)}
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400">
{showDocForm ? 'Cancel' : 'Upload'}
</button>
</div>
{#if showDocForm}
<form method="POST" action="?/uploadDocument" enctype="multipart/form-data" use:enhance class="mb-3 space-y-2">
<input type="file" name="document" required class="text-sm text-gray-600 dark:text-gray-400" />
<input type="text" name="description" placeholder="Description"
class="w-full rounded-md border border-gray-300 px-3 py-1.5 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
<button type="submit" class="rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700">Upload</button>
</form>
{/if}
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Documents</h2>
<DocumentUpload action="?/uploadDocument" />
{#if data.documents.length === 0}
<p class="text-sm text-gray-500 dark:text-gray-400">No documents.</p>
{:else}
+6 -38
View File
@@ -1,6 +1,8 @@
<script lang="ts">
import { enhance } from '$app/forms';
import { DEVICE_CONDITIONS, DEVICE_LOG_TYPES } from '$lib/constants.js';
import ImageUpload from '$lib/components/ui/ImageUpload.svelte';
import DocumentUpload from '$lib/components/ui/DocumentUpload.svelte';
import { formatDate, timeAgo } from '$lib/utils/date.js';
let { data, form } = $props();
@@ -124,34 +126,13 @@
<div class="space-y-6 lg:col-span-2">
<!-- Images -->
<div class="rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
<div class="mb-3 flex items-center justify-between">
<h2 class="text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Images</h2>
<button onclick={() => (showUploadForm = !showUploadForm)}
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400">
{showUploadForm ? 'Cancel' : 'Upload'}
</button>
</div>
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Images</h2>
{#if form?.error}
<div class="mb-3 rounded-md bg-red-50 p-3 text-sm text-red-700 dark:bg-red-900/30 dark:text-red-300">{form.error}</div>
{/if}
{#if showUploadForm}
<form method="POST" action="?/uploadImage" enctype="multipart/form-data" use:enhance class="mb-4 flex flex-wrap items-end gap-3">
<input type="file" name="image" accept="image/*" required
onchange={(e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file && file.size > 50 * 1024 * 1024) {
alert('File too large. Maximum size is 50MB.');
(e.target as HTMLInputElement).value = '';
}
}}
class="text-sm text-gray-600 dark:text-gray-400" />
<input type="text" name="caption" placeholder="Caption (optional)"
class="rounded-md border border-gray-300 px-3 py-1.5 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
<button type="submit" class="rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700">Upload</button>
</form>
{/if}
<ImageUpload action="?/uploadImage" />
{#if data.images.length === 0}
<p class="text-sm text-gray-500 dark:text-gray-400">No images yet.</p>
@@ -657,22 +638,9 @@
<!-- Documents -->
<div class="rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
<div class="mb-3 flex items-center justify-between">
<h2 class="text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Documents</h2>
<button onclick={() => (showDocForm = !showDocForm)}
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400">
{showDocForm ? 'Cancel' : 'Upload'}
</button>
</div>
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Documents</h2>
{#if showDocForm}
<form method="POST" action="?/uploadDocument" enctype="multipart/form-data" use:enhance class="mb-3 space-y-2">
<input type="file" name="document" required class="text-sm text-gray-600 dark:text-gray-400" />
<input type="text" name="description" placeholder="Description"
class="w-full rounded-md border border-gray-300 px-3 py-1.5 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
<button type="submit" class="rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700">Upload</button>
</form>
{/if}
<DocumentUpload action="?/uploadDocument" />
{#if data.documents.length === 0}
<p class="text-sm text-gray-500 dark:text-gray-400">No documents.</p>