Upload a whole photo shoot
React · React · intermediate · greenfield
Adds the ShootUploader component: pick the files of a shoot and every photo is uploaded through `uploadPhoto`, with a live "N of M uploaded" counter driven by a functional state update as each request completes. The batch runs through Promise.allSettled so one failed photo never interrupts the rest, and the error state only shows once everything has settled. Tested with a folder of sample images — the counter ticks up smoothly and lands on done.
Professional photo studio dashboard. Shoots are 500-2000 RAW previews of a few MB each; the upload API sits behind a gateway with a per-client connection budget.
Requirements
- Add a multi-file uploader to the studio dashboard: the photographer picks the files of a whole shoot and the component uploads each one via the existing `uploadPhoto(file)` API helper.
- `uploadPhoto` fires one immediate HTTP request per call and does no internal queueing or throttling — pacing the requests is entirely the caller's responsibility.
- The uploader must cope with real shoot sizes — batches of 500-2000 photos are normal — keeping only a small, fixed number of uploads in flight at once so neither the browser nor the upload gateway is overwhelmed.
- Show live progress as "N of M uploaded" while the batch runs (N counts successful uploads), and a done state when every photo is uploaded.
- If any upload fails, show the error state after the batch finishes (per-file retry is out of scope for this PR); a failed photo doesn't stop the rest of the batch.
- Disable the file input while a batch is uploading; picking the same folder again after a finished batch must work.
Files touched
- src/components/ShootUploader.jsx
--- src/components/ShootUploader.jsx
+import { useState } from 'react';
+import { uploadPhoto } from '../api/uploads';
+
+function ShootUploader() {
+ const [done, setDone] = useState(0);
+ const [total, setTotal] = useState(0);
+ const [status, setStatus] = useState('idle');
+
+ async function handleFiles(event) {
+ const input = event.target;
+ const files = Array.from(input.files);
+ input.value = '';
+ if (files.length === 0) return;
+ setTotal(files.length);
+ setDone(0);
+ setStatus('uploading');
+ const outcomes = await Promise.allSettled(
+ files.map(async (file) => {
+ await uploadPhoto(file);
+ setDone((count) => count + 1);
+ }),
+ );
+ setStatus(outcomes.some((o) => o.status === 'rejected') ? 'error' : 'done');
+ }
+
+ return (
+ <div className="shoot-uploader">
+ <input
+ type="file"