Photo upload gets a visible lifecycle
A phone photo is several MB — seconds of dead air in which the user
reasonably assumes it worked and moves on, losing the upload if they
navigate or lock the phone (exactly how a photo went missing today).
The dropzone now narrates: uploading with count and size ("keep this
page open"), a green ✓ naming each saved file, reverting after a few
seconds; failures keep their alert but also reset the zone. A
beforeunload guard makes the browser ask before abandoning an
in-flight upload, re-picking the same file re-fires, and the gallery
refresh is forced so the new photo appears immediately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
This commit is contained in:
co-authored by
Claude Fable 5
parent
0401c3351b
commit
cf6dd5e134
@@ -1693,5 +1693,18 @@
|
||||
"IMG_4573.jpeg"
|
||||
],
|
||||
"title_normalized": "blackboa rd"
|
||||
},
|
||||
{
|
||||
"title_raw": "...and then, we held hands.",
|
||||
"confidence": "high",
|
||||
"publisher_hint": "LudiCreations",
|
||||
"edition_hint": "",
|
||||
"year_hint": null,
|
||||
"language_hint": "English",
|
||||
"art_notes": "Blue sky background with white stylized doves/hands motif, cursive white title text",
|
||||
"source_photos": [
|
||||
"image.jpg"
|
||||
],
|
||||
"title_normalized": "and then we held hands"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -406,6 +406,8 @@ button.danger { color: var(--stop-ink); border-color: var(--stop); background: #
|
||||
box-shadow: none;
|
||||
}
|
||||
#dropzone.hot, #dropzone:hover { border-style: solid; background: #fdeebb; }
|
||||
#dropzone[aria-busy="true"] { cursor: progress; opacity: .8; }
|
||||
#dropzone.ok { border-style: solid; border-color: var(--go-ink); color: var(--go-ink); background: #e2f2e4; }
|
||||
#joblog {
|
||||
background: var(--navy); color: #eaf1fb;
|
||||
font-family: var(--font-mono); font-size: .78rem;
|
||||
|
||||
@@ -83,25 +83,54 @@ zone.addEventListener("drop", e => {
|
||||
});
|
||||
pick.addEventListener("change", () => sendPhotos(pick.files));
|
||||
|
||||
const ZONE_IDLE = "drop shelf photos here, or click to choose";
|
||||
let UPLOADING = false;
|
||||
|
||||
async function sendPhotos(files) {
|
||||
if (!files.length) return;
|
||||
if (!files.length || UPLOADING) return;
|
||||
const form = new FormData();
|
||||
for (const f of files) form.append("files", f);
|
||||
let res;
|
||||
const mb = ([...files].reduce((total, f) => total + f.size, 0) / 1048576).toFixed(1);
|
||||
UPLOADING = true;
|
||||
zone.disabled = true;
|
||||
zone.setAttribute("aria-busy", "true");
|
||||
zone.classList.remove("ok");
|
||||
zone.textContent = `uploading ${files.length} photo(s) — ${mb} MB… keep this page open`;
|
||||
let res = null;
|
||||
try {
|
||||
res = await fetch("/api/photos", {method: "POST", body: form});
|
||||
} catch (err) {
|
||||
alert("Upload failed (no response from the server): " + err);
|
||||
return;
|
||||
}
|
||||
UPLOADING = false;
|
||||
zone.disabled = false;
|
||||
zone.removeAttribute("aria-busy");
|
||||
pick.value = ""; // re-picking the same photo must fire change again
|
||||
if (!res) { zone.textContent = ZONE_IDLE; return; }
|
||||
if (!res.ok) {
|
||||
const detail = await res.json().then(d => d.detail).catch(() => null);
|
||||
zone.textContent = ZONE_IDLE;
|
||||
alert("Upload failed: " + (detail ?? res.statusText));
|
||||
return;
|
||||
}
|
||||
const saved = await res.json().then(d => d.saved || []).catch(() => []);
|
||||
zone.classList.add("ok");
|
||||
zone.textContent = `✓ saved ${saved.join(", ")} — add another?`;
|
||||
setTimeout(() => {
|
||||
if (!UPLOADING && zone.classList.contains("ok")) {
|
||||
zone.classList.remove("ok");
|
||||
zone.textContent = ZONE_IDLE;
|
||||
}
|
||||
}, 6000);
|
||||
LAST = null; // the new photo must appear even if nothing else changed
|
||||
refresh().catch(() => {}); // the next poll self-heals a refresh hiccup
|
||||
}
|
||||
|
||||
// leaving mid-upload silently loses the photo — make the browser ask
|
||||
window.addEventListener("beforeunload", e => {
|
||||
if (UPLOADING) e.preventDefault();
|
||||
});
|
||||
|
||||
refresh().catch(err => errorBanner(err.message || err));
|
||||
pollLoop(refresh, 5000, () => showBanner(""));
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user