Extract upload dropzone and queue UI with shared pick validation utils.

This commit is contained in:
2026-10-20 22:20:16 +02:00
parent 3ab66c250c
commit d82d451448
12 changed files with 547 additions and 94 deletions
@@ -0,0 +1,57 @@
<template>
<div>
<label
class="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:opacity-90"
:class="disabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'"
>
Choose images
<input
ref="fileInputRef"
type="file"
accept="image/*"
multiple
class="sr-only"
:disabled="disabled"
@change="onFileInputChange"
/>
</label>
<p v-if="selectionError" class="mt-2 text-sm text-destructive">{{ selectionError }}</p>
</div>
</template>
<script setup lang="ts">
import { ref, type PropType } from 'vue';
defineProps({
disabled: {
type: Boolean,
required: true
},
selectionError: {
type: String as PropType<string | null>,
default: null
}
});
const emit = defineEmits<{
pick: [files: File[]];
}>();
const fileInputRef = ref<HTMLInputElement | null>(null);
const onFileInputChange = (event: Event): void => {
const input = event.target as HTMLInputElement;
const picked = input.files ? [...input.files] : [];
emit('pick', picked);
};
const clearInput = (): void => {
if (fileInputRef.value) {
fileInputRef.value.value = '';
}
};
defineExpose({ clearInput });
</script>
@@ -0,0 +1,53 @@
<template>
<ul v-if="rows.length > 0" class="divide-y divide-border rounded-lg border border-border">
<li v-for="row in rows" :key="row.key">
<button
type="button"
class="flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-muted/60"
:class="isRowSelected(row) ? 'bg-muted/80' : ''"
@click="emit('select', row.file)"
>
<span class="min-w-0 flex-1 truncate text-sm font-medium">{{ row.file.name }}</span>
<span class="shrink-0 text-xs text-muted-foreground">
{{ prettyBytes(row.file.size, { maximumFractionDigits: 1 }) }}
</span>
<span class="shrink-0 text-xs" :class="statusTextClass(row.preview)">
{{ row.preview.message }}
</span>
</button>
</li>
</ul>
</template>
<script setup lang="ts">
import { type PropType } from 'vue';
import { useMetadataStore } from '@/stores/metadata';
import type { MetadataPreviewEntry } from '@/types/metadata/MetadataPreview';
import type { UploadQueueRow } from '@/types/upload/UploadQueueRow';
import prettyBytes from 'pretty-bytes';
const metadataStore = useMetadataStore();
const { makeKeyFromFile } = metadataStore;
const props = defineProps({
rows: {
type: Array as PropType<UploadQueueRow[]>,
required: true
},
selectedFile: {
type: Object as PropType<File | null>,
default: null
}
});
const emit = defineEmits<{
select: [file: File];
}>();
const isRowSelected = (row: UploadQueueRow): boolean =>
props.selectedFile !== null && makeKeyFromFile(props.selectedFile) === row.key;
const statusTextClass = (entry: MetadataPreviewEntry): string =>
entry.status === 'error' ? 'text-destructive' : 'text-muted-foreground';
</script>