Back to comparisons

File Upload

Drag-and-drop upload zone with file previews, progress tracking, and per-file retry. AI output had no drag-over visual feedback, no client-side validation, no URL cleanup, and no individual retry. The production version fixes all six issues found in review.

AI Generated
1export function UploadZone() {
2 const dispatch = useAppDispatch();
3 const fileInputRef = useRef<HTMLInputElement>(null);
4
5 const handleDrop = useCallback(
6 (e: React.DragEvent) => {
7 e.preventDefault();
8 if (e.dataTransfer.files.length > 0) {
9 const files = Array.from(e.dataTransfer.files).map((file) => ({
10 id: `file-${Date.now()}-${Math.random().toString(36).slice(2)}`,
11 file: { name: file.name, size: file.size, type: file.type },
12 }));
13 dispatch(addFiles(files));
14 }
15 },
16 [dispatch],
17 );
18
19 const handleDragOver = useCallback((e: React.DragEvent) => {
20 e.preventDefault();
21 }, []);
22
23 const handleClick = useCallback(() => {
24 fileInputRef.current?.click();
25 }, []);
26
27 const handleFileChange = useCallback(
28 (e: React.ChangeEvent<HTMLInputElement>) => {
29 if (e.target.files) {
30 const files = Array.from(e.target.files).map((file) => ({
31 id: `file-${Date.now()}-${Math.random().toString(36).slice(2)}`,
32 file: { name: file.name, size: file.size, type: file.type },
33 }));
34 dispatch(addFiles(files));
35 }
36 },
37 [dispatch],
38 );
39
40 return (
41 <div
42 className="flex flex-col items-center justify-center gap-3 rounded-lg border-2 border-dashed border-muted-foreground/25 p-8 cursor-pointer"
43 onDragOver={handleDragOver}
44 onDrop={handleDrop}
45 onClick={handleClick}
46 >
47 <input ref={fileInputRef} type="file" className="hidden"
48 multiple onChange={handleFileChange} />
49 <Upload className="h-6 w-6 text-muted-foreground" />
50 <p className="text-sm font-medium">
51 Drag & drop files here or click to browse
52 </p>
53 </div>
54 );
55}
fixNo drag-over visual feedbackL5–16

The drop handler works but the zone appearance never changes when a file is dragged over it. There is no isDragOver state, no dragenter/dragleave handling, and no conditional styling. Users get no visual confirmation that the zone is a valid drop target.

fixNo file type or size validationL8–13

Files are added directly to the queue without checking type or size. Invalid files waste time uploading and produce confusing server errors. Client-side validation must reject files before they enter the Redux store.

fixNo aria-label, no disabled stateL38–43

The drop zone div has no role, aria-label, or aria-disabled. Screen readers cannot identify it as an interactive element. There is no max file count enforcement or disabled appearance when the limit is reached.

fixDrag-over state with counter for nested elementsL4–5

isDragOver state is toggled by dragenter/dragleave events. A dragCounter ref prevents flicker from nested DOM elements. The zone border changes to primary color and gains a tinted background during drag-over.

fixClient-side validation before queuingL13–39

validateAndAddFiles checks file type against ACCEPTED_FILE_TYPES, size against MAX_FILE_SIZE (10 MB), and count against MAX_FILE_COUNT (5). Invalid files are rejected with descriptive toast errors before entering the store.

additionAccessible drop zone with disabled stateL71–77

The zone has role=button, tabIndex, aria-label, and aria-disabled. When the file count limit is reached, the zone becomes visually muted and non-interactive. Keyboard users can activate it with Enter or Space.