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,38 @@
import { describe, expect, it } from 'vitest';
import { analyzePickedUploadFiles } from '@/utils/upload/analyzePickedUploadFiles';
const makeFile = (name: string, type: string, size: number): File =>
new File([new Uint8Array(size)], name, { type });
describe('analyzePickedUploadFiles', () => {
const maxImageBytes = 100;
const maxQueueSize = 2;
it('accepts images within size and queue limits', () => {
const picked = [makeFile('a.png', 'image/png', 50), makeFile('b.jpg', 'image/jpeg', 80)];
const result = analyzePickedUploadFiles(picked, maxImageBytes, maxQueueSize);
expect(result.accepted).toHaveLength(2);
expect(result.nonImageCount).toBe(0);
expect(result.oversizedImagesCount).toBe(0);
expect(result.queueOmittedImagesCount).toBe(0);
});
it('skips non-images and oversized files and caps the queue', () => {
const picked = [
makeFile('doc.pdf', 'application/pdf', 10),
makeFile('big.png', 'image/png', 200),
makeFile('1.png', 'image/png', 10),
makeFile('2.png', 'image/png', 10),
makeFile('3.png', 'image/png', 10)
];
const result = analyzePickedUploadFiles(picked, maxImageBytes, maxQueueSize);
expect(result.accepted.map(f => f.name)).toEqual(['1.png', '2.png']);
expect(result.nonImageCount).toBe(1);
expect(result.oversizedImagesCount).toBe(1);
expect(result.queueOmittedImagesCount).toBe(1);
});
});
@@ -0,0 +1,21 @@
import type { PickedUploadAnalysis } from '@/types/upload/PickedUploadAnalysis';
export const analyzePickedUploadFiles = (
picked: File[],
maxImageBytes: number,
maxQueueSize: number
): PickedUploadAnalysis => {
const images = picked.filter(file => file.type.startsWith('image/'));
const nonImageCount = picked.length - images.length;
const imagesWithinSizeLimit = images.filter(file => file.size <= maxImageBytes);
const oversizedImagesCount = images.length - imagesWithinSizeLimit.length;
const accepted = imagesWithinSizeLimit.slice(0, maxQueueSize);
const queueOmittedImagesCount = imagesWithinSizeLimit.length - accepted.length;
return {
accepted,
nonImageCount,
oversizedImagesCount,
queueOmittedImagesCount
};
};
@@ -0,0 +1,24 @@
import { describe, expect, it } from 'vitest';
import { buildPickedUploadWarning } from '@/utils/upload/buildPickedUploadWarning';
describe('buildPickedUploadWarning', () => {
it('returns null when nothing was skipped', () => {
expect(
buildPickedUploadWarning(
{ nonImageCount: 0, oversizedImagesCount: 0, queueOmittedImagesCount: 0 },
5
)
).toBeNull();
});
it('joins skip reasons into one message', () => {
const message = buildPickedUploadWarning(
{ nonImageCount: 1, oversizedImagesCount: 2, queueOmittedImagesCount: 3 },
4
);
expect(message).toContain('1 non-image');
expect(message).toContain('2 image(s) exceeded');
expect(message).toContain('first 4 valid images');
});
});
@@ -0,0 +1,22 @@
import type { PickedUploadAnalysis } from '@/types/upload/PickedUploadAnalysis';
export const buildPickedUploadWarning = (
summary: Pick<PickedUploadAnalysis, 'nonImageCount' | 'oversizedImagesCount' | 'queueOmittedImagesCount'>,
maxQueueSize: number
): string | null => {
const parts: string[] = [];
if (summary.nonImageCount > 0) {
parts.push(`${summary.nonImageCount} non-image file(s) were skipped.`);
}
if (summary.oversizedImagesCount > 0) {
parts.push(`${summary.oversizedImagesCount} image(s) exceeded the size limit and were skipped.`);
}
if (summary.queueOmittedImagesCount > 0) {
parts.push(`Only the first ${maxQueueSize} valid images were added.`);
}
return parts.length > 0 ? parts.join(' ') : null;
};