feat(frontend): implement file upload with drag-and-drop and per-file progress
- client.ts: add uploadWithProgress() using XHR for upload progress events - FileUpload.svelte: drag-drop zone wrapper, multi-file queue with individual progress bars, success/error status, MIME rejection message, dismiss panel - Header.svelte: optional onUpload prop renders upload icon button - files/+page.svelte: wire upload button, prepend uploaded files to grid - vite-mock-plugin.ts: handle POST /files, unshift new file into mock array - Fix crypto.randomUUID() crash on non-secure HTTP context (use Date.now + Math.random) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
import { api } from '$lib/api/client';
|
||||
import { ApiError } from '$lib/api/client';
|
||||
import FileCard from '$lib/components/file/FileCard.svelte';
|
||||
import FileUpload from '$lib/components/file/FileUpload.svelte';
|
||||
import FilterBar from '$lib/components/file/FilterBar.svelte';
|
||||
import Header from '$lib/components/layout/Header.svelte';
|
||||
import SelectionBar from '$lib/components/layout/SelectionBar.svelte';
|
||||
@@ -13,6 +14,12 @@
|
||||
import { parseDslFilter } from '$lib/utils/dsl';
|
||||
import type { File, FileCursorPage } from '$lib/api/types';
|
||||
|
||||
let uploader = $state<{ open: () => void } | undefined>();
|
||||
|
||||
function handleUploaded(file: File) {
|
||||
files = [file, ...files];
|
||||
}
|
||||
|
||||
const LIMIT = 50;
|
||||
|
||||
const FILE_SORT_OPTIONS = [
|
||||
@@ -179,6 +186,7 @@
|
||||
onSortChange={(s) => fileSorting.setSort(s as FileSortField)}
|
||||
onOrderToggle={() => fileSorting.toggleOrder()}
|
||||
onFilterToggle={() => (filterOpen = !filterOpen)}
|
||||
onUpload={() => uploader?.open()}
|
||||
/>
|
||||
|
||||
{#if filterOpen}
|
||||
@@ -189,30 +197,32 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<main>
|
||||
{#if error}
|
||||
<p class="error" role="alert">{error}</p>
|
||||
{/if}
|
||||
<FileUpload bind:this={uploader} onUploaded={handleUploaded}>
|
||||
<main>
|
||||
{#if error}
|
||||
<p class="error" role="alert">{error}</p>
|
||||
{/if}
|
||||
|
||||
<div class="grid">
|
||||
{#each files as file, i (file.id)}
|
||||
<FileCard
|
||||
{file}
|
||||
index={i}
|
||||
selected={$selectionStore.ids.has(file.id ?? '')}
|
||||
selectionMode={$selectionActive}
|
||||
onTap={(e) => handleTap(file, i, e)}
|
||||
onLongPress={(pt) => handleLongPress(file, i, pt)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="grid">
|
||||
{#each files as file, i (file.id)}
|
||||
<FileCard
|
||||
{file}
|
||||
index={i}
|
||||
selected={$selectionStore.ids.has(file.id ?? '')}
|
||||
selectionMode={$selectionActive}
|
||||
onTap={(e) => handleTap(file, i, e)}
|
||||
onLongPress={(pt) => handleLongPress(file, i, pt)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<InfiniteScroll {loading} {hasMore} onLoadMore={loadMore} />
|
||||
<InfiniteScroll {loading} {hasMore} onLoadMore={loadMore} />
|
||||
|
||||
{#if !loading && !hasMore && files.length === 0}
|
||||
<div class="empty">No files yet.</div>
|
||||
{/if}
|
||||
</main>
|
||||
{#if !loading && !hasMore && files.length === 0}
|
||||
<div class="empty">No files yet.</div>
|
||||
{/if}
|
||||
</main>
|
||||
</FileUpload>
|
||||
</div>
|
||||
|
||||
{#if $selectionActive}
|
||||
|
||||
Reference in New Issue
Block a user