feat(submissions): add small file preview before submitting
This commit is contained in:
@@ -415,6 +415,26 @@ const removeFile = (index: number) => {
|
|||||||
uploadedFiles.value.splice(index, 1);
|
uploadedFiles.value.splice(index, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Thumbnails of the selected images, revoked once the file is removed/submitted
|
||||||
|
const thumbnailUrls = new Map<File, string>();
|
||||||
|
const thumbnails = computed(() =>
|
||||||
|
uploadedFiles.value.map((file) => {
|
||||||
|
if (!file.type.startsWith("image/")) return null;
|
||||||
|
if (!thumbnailUrls.has(file)) thumbnailUrls.set(file, URL.createObjectURL(file));
|
||||||
|
return thumbnailUrls.get(file)!;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const revokeThumbnails = (keep: File[] = []) => {
|
||||||
|
thumbnailUrls.forEach((url, file) => {
|
||||||
|
if (keep.includes(file)) return;
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
thumbnailUrls.delete(file);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(uploadedFiles, (files) => revokeThumbnails(files), { deep: true });
|
||||||
|
|
||||||
const submitSubmission = async () => {
|
const submitSubmission = async () => {
|
||||||
if (uploadedFiles.value.length === 0) return;
|
if (uploadedFiles.value.length === 0) return;
|
||||||
|
|
||||||
@@ -561,6 +581,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('paste', handlePasteOnUploadArea);
|
window.removeEventListener('paste', handlePasteOnUploadArea);
|
||||||
|
revokeThumbnails();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -629,17 +650,24 @@ onUnmounted(() => {
|
|||||||
<!-- Uploaded Files List -->
|
<!-- Uploaded Files List -->
|
||||||
<div v-if="uploadedFiles.length > 0" class="mb-6">
|
<div v-if="uploadedFiles.length > 0" class="mb-6">
|
||||||
<p class="font-semibold mb-3">{{ t("common.selectedFiles") }}</p>
|
<p class="font-semibold mb-3">{{ t("common.selectedFiles") }}</p>
|
||||||
<div class="space-y-2">
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||||
<div v-for="(file, index) in uploadedFiles" :key="index"
|
<div v-for="(file, index) in uploadedFiles" :key="index"
|
||||||
class="flex items-center gap-3 bg-base-200 p-3 rounded-lg cursor-pointer hover:bg-base-300 transition-colors group"
|
class="relative bg-base-200 rounded-lg overflow-hidden cursor-pointer hover:bg-base-300 transition-colors group"
|
||||||
@click="openFilePreview(file)">
|
@click="openFilePreview(file)">
|
||||||
|
<div class="aspect-video bg-base-300 flex items-center justify-center">
|
||||||
|
<img v-if="thumbnails[index]" :src="thumbnails[index]" :alt="file.name"
|
||||||
|
class="w-full h-full object-contain" />
|
||||||
|
<i v-else :class="['mdi text-5xl text-primary', getFileIcon(file)]"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 p-2">
|
||||||
<i :class="['mdi text-primary', getFileIcon(file)]"></i>
|
<i :class="['mdi text-primary', getFileIcon(file)]"></i>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<p class="font-medium truncate group-hover:text-primary">{{ file.name }}</p>
|
<p class="text-sm font-medium truncate group-hover:text-primary" :title="file.name">{{ file.name }}</p>
|
||||||
<p class="text-xs text-base-content/70">{{ formatFileSize(file.size) }}</p>
|
<p class="text-xs text-base-content/70">{{ formatFileSize(file.size) }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<button @click.stop="removeFile(index)"
|
<button @click.stop="removeFile(index)"
|
||||||
class="btn btn-ghost btn-xs opacity-0 group-hover:opacity-100">
|
class="btn btn-circle btn-xs absolute top-2 right-2 sm:opacity-0 sm:group-hover:opacity-100">
|
||||||
<i class="mdi mdi-close"></i>
|
<i class="mdi mdi-close"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user