Photo ID quality check after upload

Shows uploaded image at larger size with automated quality checks:
- File size (too small = low quality warning)
- File type validation (JPEG, PNG, PDF, HEIC)
- Resolution check (minimum 400x250 for readable text)
- Aspect ratio check (should look like an ID card)

Green checkmark for passing checks, red X for issues.
Yellow warning box for quality problems with specific guidance.
Accept & Continue button to confirm, Retake to re-upload.
After accept, collapses to small preview with "Change ID" option.

Front of ID only (sufficient for FMCSA MCS-150 filing).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
justin 2026-05-30 15:59:27 -05:00
parent e8769e4d5d
commit beb23d777e

View file

@ -210,9 +210,27 @@
<p class="pw-field-help">Required for FMCSA filings. Driver's license, passport, or state ID.</p>
<div class="pw-upload-area">
<input type="file" id="dot-photo-id" accept="image/*,.pdf" capture="environment" style="display:none" />
<div id="dot-id-preview" hidden style="display:flex;align-items:center;gap:12px;justify-content:center">
<img id="dot-id-img" style="max-width:200px;max-height:150px;border-radius:6px;border:1px solid #d1d5db" />
<button type="button" id="dot-id-remove" style="background:#fee2e2;color:#991b1b;border:none;padding:4px 12px;border-radius:4px;font-size:12px;cursor:pointer">Remove</button>
<!-- Preview + quality check -->
<div id="dot-id-preview" hidden>
<div style="text-align:center;margin-bottom:12px">
<img id="dot-id-img" style="max-width:320px;max-height:240px;border-radius:8px;border:2px solid #d1d5db;display:block;margin:0 auto" />
<p id="dot-id-resolution" style="font-size:11px;color:#94a3b8;margin:6px 0 0"></p>
</div>
<div id="dot-id-quality-check" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:14px;margin-bottom:12px">
<p style="font-size:13px;font-weight:600;color:#1a2744;margin:0 0 10px">Photo ID Quality Check</p>
<div id="dot-id-checks" style="font-size:12px;color:#374151;line-height:2"></div>
<div id="dot-id-quality-warn" hidden style="background:#fef3c7;border:1px solid #fbbf24;border-radius:6px;padding:10px;margin-top:10px">
<p style="font-size:12px;color:#92400e;margin:0;font-weight:600">&#9888; Image quality issue detected</p>
<p id="dot-id-quality-msg" style="font-size:11px;color:#92400e;margin:4px 0 0"></p>
</div>
<div id="dot-id-quality-ok" hidden style="background:#f0fdf4;border:1px solid #86efac;border-radius:6px;padding:10px;margin-top:10px">
<p style="font-size:12px;color:#166534;margin:0">&#10004; Image looks good — clear and properly sized for submission.</p>
</div>
</div>
<div style="display:flex;gap:8px;justify-content:center">
<button type="button" id="dot-id-accept" style="padding:8px 24px;background:#059669;color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer">Accept &amp; Continue</button>
<button type="button" id="dot-id-retake" style="padding:8px 24px;background:#fff;color:#374151;border:1px solid #d1d5db;border-radius:6px;font-size:13px;cursor:pointer">Retake / Re-upload</button>
</div>
</div>
<div id="dot-id-upload-options">
<div style="display:flex;flex-wrap:wrap;gap:8px;justify-content:center">
@ -433,23 +451,10 @@
// Upload button — opens file picker
idBtn?.addEventListener("click", function() { if (idInput) idInput.click(); });
// Scanner/camera button — uses capture attribute to trigger camera or scanner
// Scanner/camera button
var idScanBtn = document.getElementById("dot-id-scan-btn");
var idScanInput = document.getElementById("dot-photo-id-scan");
idScanBtn?.addEventListener("click", function() { if (idScanInput) idScanInput.click(); });
idScanInput?.addEventListener("change", function() {
var file = idScanInput.files?.[0];
if (!file) return;
// Copy to main input handler
window.__dotPhotoId = file;
if (file.type.startsWith("image/") && idImg) {
var reader = new FileReader();
reader.onload = function(e) { idImg.src = e.target?.result; };
reader.readAsDataURL(file);
}
if (idPreview) idPreview.hidden = false;
if (idUploadOpts) idUploadOpts.style.display = "none";
});
// QR code button — show QR with current page URL for phone upload
idQrBtn?.addEventListener("click", function() {
@ -476,24 +481,140 @@
}
});
idInput?.addEventListener("change", function() {
var file = idInput.files?.[0];
var idAcceptBtn = document.getElementById("dot-id-accept");
var idRetakeBtn = document.getElementById("dot-id-retake");
var idQualityWarn = document.getElementById("dot-id-quality-warn");
var idQualityOk = document.getElementById("dot-id-quality-ok");
var idQualityMsg = document.getElementById("dot-id-quality-msg");
var idChecksDiv = document.getElementById("dot-id-checks");
var idResolution = document.getElementById("dot-id-resolution");
var idAccepted = false;
function runQualityCheck(file, imgEl) {
var checks = [];
var warnings = [];
// File size check
var sizeMB = (file.size / 1024 / 1024).toFixed(1);
if (file.size < 50000) {
checks.push({ok: false, text: "File size: " + sizeMB + "MB — too small, may be low quality"});
warnings.push("Image file is very small (" + sizeMB + "MB). Please use a higher resolution.");
} else if (file.size > 15000000) {
checks.push({ok: false, text: "File size: " + sizeMB + "MB — very large"});
warnings.push("File is over 15MB. Consider a smaller image.");
} else {
checks.push({ok: true, text: "File size: " + sizeMB + "MB"});
}
// File type check
var validTypes = ["image/jpeg","image/png","image/heic","image/heif","image/webp","application/pdf"];
if (validTypes.indexOf(file.type) >= 0 || file.type.startsWith("image/")) {
checks.push({ok: true, text: "File type: " + file.type.split("/")[1].toUpperCase()});
} else {
checks.push({ok: false, text: "File type: " + file.type + " — not a recognized image format"});
warnings.push("Please upload a JPEG, PNG, or PDF image of your ID.");
}
// Resolution check (for images)
if (imgEl && imgEl.naturalWidth) {
var w = imgEl.naturalWidth;
var h = imgEl.naturalHeight;
if (idResolution) idResolution.textContent = w + " × " + h + " pixels";
if (w < 400 || h < 250) {
checks.push({ok: false, text: "Resolution: " + w + "×" + h + " — too low, text may be unreadable"});
warnings.push("Image resolution is too low. FMCSA may not accept an unreadable ID.");
} else if (w < 800 || h < 500) {
checks.push({ok: true, text: "Resolution: " + w + "×" + h + " — acceptable"});
} else {
checks.push({ok: true, text: "Resolution: " + w + "×" + h + " — good"});
}
// Aspect ratio check (ID cards are roughly 1.6:1)
var ratio = Math.max(w,h) / Math.min(w,h);
if (ratio > 0.8 && ratio < 2.5) {
checks.push({ok: true, text: "Aspect ratio: looks like an ID card"});
} else {
checks.push({ok: false, text: "Aspect ratio: " + ratio.toFixed(1) + ":1 — doesn't look like a standard ID"});
warnings.push("This doesn't appear to be a photo of an ID card. Please upload a clear photo of the front of your government-issued ID.");
}
}
// Render checks
if (idChecksDiv) {
idChecksDiv.innerHTML = checks.map(function(c) {
return '<div style="display:flex;align-items:center;gap:6px">' +
(c.ok ? '<span style="color:#059669">&#10004;</span>' : '<span style="color:#dc2626">&#10008;</span>') +
'<span>' + c.text + '</span></div>';
}).join("");
}
if (warnings.length > 0) {
if (idQualityWarn) { idQualityWarn.hidden = false; idQualityMsg.textContent = warnings.join(" "); }
if (idQualityOk) idQualityOk.hidden = true;
} else {
if (idQualityWarn) idQualityWarn.hidden = true;
if (idQualityOk) idQualityOk.hidden = false;
}
}
function handleIdFile(file) {
if (!file) return;
window.__dotPhotoId = file;
if (file.type.startsWith("image/")) {
const reader = new FileReader();
reader.onload = (e) => { idImg.src = e.target?.result; };
idAccepted = false;
if (file.type.startsWith("image/") && idImg) {
var reader = new FileReader();
reader.onload = function(e) {
idImg.src = e.target?.result;
// Wait for image to load to check dimensions
idImg.onload = function() { runQualityCheck(file, idImg); };
};
reader.readAsDataURL(file);
} else {
// PDF or other — can't preview but accept
if (idResolution) idResolution.textContent = "PDF document";
runQualityCheck(file, null);
}
if (idPreview) idPreview.hidden = false;
if (idUploadOpts) idUploadOpts.style.display = "none";
}
// Accept button
idAcceptBtn?.addEventListener("click", function() {
idAccepted = true;
if (idPreview) {
// Collapse to small preview
var qualCheck = document.getElementById("dot-id-quality-check");
if (qualCheck) qualCheck.hidden = true;
idAcceptBtn.hidden = true;
idRetakeBtn.style.fontSize = "11px";
idRetakeBtn.textContent = "Change ID";
if (idImg) { idImg.style.maxWidth = "150px"; idImg.style.maxHeight = "100px"; }
}
});
idRemove?.addEventListener("click", () => {
(window).__dotPhotoId = null;
// Retake button
idRetakeBtn?.addEventListener("click", function() {
window.__dotPhotoId = null;
idAccepted = false;
if (idInput) idInput.value = "";
if (idScanInput) idScanInput.value = "";
if (idPreview) idPreview.hidden = true;
if (idUploadOpts) idUploadOpts.style.display = "";
// Reset quality check UI
var qualCheck = document.getElementById("dot-id-quality-check");
if (qualCheck) qualCheck.hidden = false;
if (idAcceptBtn) idAcceptBtn.hidden = false;
if (idRetakeBtn) { idRetakeBtn.style.fontSize = "13px"; idRetakeBtn.textContent = "Retake / Re-upload"; }
if (idImg) { idImg.style.maxWidth = "320px"; idImg.style.maxHeight = "240px"; }
});
idInput?.addEventListener("change", function() {
handleIdFile(idInput.files?.[0]);
});
// Scanner input handler
idScanInput?.addEventListener("change", function() {
handleIdFile(idScanInput.files?.[0]);
});
} // end guard