Add mobile photo ID upload page at /portal/upload-id/

Dedicated mobile-friendly page for phone camera ID capture:
- Big "Take Photo of ID" button with camera capture
- Image preview with basic quality check
- Submit uploads to API with JWT auth
- Success/error states with retry
- QR code on desktop intake links here instead of full form

Still needs: API upload endpoint, polling from desktop, OCR validation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
justin 2026-05-30 16:20:27 -05:00
parent daf6d1f831
commit 1611b67543

View file

@ -0,0 +1,204 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex">
<title>Upload Photo ID | Performance West</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<script>
window.__PW_API = (function() {
var h = window.location.hostname;
if (h === "localhost" || h === "127.0.0.1") return "http://" + h + ":3001";
if (h === "dev.performancewest.net") return "https://api.dev.performancewest.net";
return "https://api.performancewest.net";
})();
</script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, system-ui, sans-serif; background: #f8fafc; min-height: 100vh; display: flex; flex-direction: column; }
.header { background: #1a2744; padding: 16px 20px; text-align: center; }
.header img { height: 36px; }
.header p { color: #94a3b8; font-size: 12px; margin-top: 4px; }
.main { flex: 1; padding: 20px; max-width: 480px; margin: 0 auto; width: 100%; }
h1 { font-size: 20px; color: #1a2744; margin-bottom: 8px; text-align: center; }
.subtitle { font-size: 14px; color: #64748b; text-align: center; margin-bottom: 20px; }
.security { display: flex; gap: 8px; align-items: flex-start; background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px; padding: 10px 14px; margin-bottom: 16px; font-size: 11px; color: #1e40af; }
.upload-area { border: 3px dashed #d1d5db; border-radius: 12px; padding: 32px 20px; text-align: center; background: #fff; }
.upload-btn { display: flex; align-items: center; justify-content: center; gap: 10px; width: 100%; padding: 18px 24px; background: #f97316; color: #fff; border: none; border-radius: 10px; cursor: pointer; font-size: 17px; font-weight: 600; box-shadow: 0 4px 12px rgba(249,115,22,0.3); }
.upload-btn:active { transform: scale(0.98); }
.hint { font-size: 12px; color: #94a3b8; margin-top: 10px; }
.preview { text-align: center; margin: 16px 0; }
.preview img { max-width: 100%; max-height: 300px; border-radius: 8px; border: 2px solid #d1d5db; }
.status { text-align: center; padding: 20px; }
.status.success { background: #f0fdf4; border: 2px solid #86efac; border-radius: 12px; }
.status.error { background: #fef2f2; border: 2px solid #fca5a5; border-radius: 12px; }
.actions { display: flex; gap: 8px; justify-content: center; margin-top: 12px; }
.btn-green { padding: 12px 28px; background: #059669; color: #fff; border: none; border-radius: 8px; font-size: 15px; font-weight: 600; cursor: pointer; }
.btn-outline { padding: 12px 28px; background: #fff; color: #374151; border: 1px solid #d1d5db; border-radius: 8px; font-size: 14px; cursor: pointer; }
.footer { padding: 16px; text-align: center; font-size: 11px; color: #94a3b8; border-top: 1px solid #e2e8f0; }
</style>
</head>
<body>
<div class="header">
<img src="/images/logo.png" alt="Performance West">
<p>Secure ID Upload</p>
</div>
<div class="main">
<h1>📷 Take a Photo of Your ID</h1>
<p class="subtitle">Hold your ID flat and take a clear photo of the front. Make sure all text is readable.</p>
<div class="security">
<span>🔒</span>
<span>Your photo is encrypted and transmitted securely. It is used only to verify your identity for your FMCSA filing and is automatically deleted afterward.</span>
</div>
<div id="upload-section">
<div class="upload-area">
<input type="file" id="id-file" accept="image/*" capture="environment" style="display:none">
<button type="button" id="take-photo-btn" class="upload-btn">
📷 Take Photo of ID
</button>
<p class="hint">Tap the button to open your camera</p>
</div>
</div>
<div id="preview-section" style="display:none">
<div class="preview">
<img id="preview-img" alt="Your ID">
</div>
<div id="quality-info" style="font-size:12px;color:#374151;text-align:center;margin-bottom:12px"></div>
<div class="actions">
<button type="button" id="submit-btn" class="btn-green">✓ Submit ID</button>
<button type="button" id="retake-btn" class="btn-outline">Retake</button>
</div>
</div>
<div id="uploading-section" style="display:none">
<div style="text-align:center;padding:32px">
<div style="font-size:32px;margin-bottom:12px"></div>
<p style="font-size:15px;color:#374151;font-weight:500">Uploading securely...</p>
</div>
</div>
<div id="success-section" style="display:none">
<div class="status success">
<div style="font-size:32px;margin-bottom:8px"></div>
<p style="font-size:16px;font-weight:600;color:#166534">ID Uploaded Successfully</p>
<p style="font-size:13px;color:#374151;margin-top:8px">You can close this page and return to the form on your computer. Your ID has been attached to your order.</p>
</div>
</div>
<div id="error-section" style="display:none">
<div class="status error">
<div style="font-size:32px;margin-bottom:8px"></div>
<p style="font-size:16px;font-weight:600;color:#991b1b">Upload Failed</p>
<p id="error-msg" style="font-size:13px;color:#7f1d1d;margin-top:8px"></p>
<div class="actions">
<button type="button" id="retry-btn" class="btn-outline">Try Again</button>
</div>
</div>
</div>
</div>
<div class="footer">
Performance West Inc. &middot; (888) 411-0383 &middot; performancewest.net
</div>
<script>
var API = window.__PW_API;
var params = new URLSearchParams(window.location.search);
var orderNumber = params.get("order") || "";
var token = params.get("token") || "";
var fileInput = document.getElementById("id-file");
var takeBtn = document.getElementById("take-photo-btn");
var previewSection = document.getElementById("preview-section");
var uploadSection = document.getElementById("upload-section");
var uploadingSection = document.getElementById("uploading-section");
var successSection = document.getElementById("success-section");
var errorSection = document.getElementById("error-section");
var previewImg = document.getElementById("preview-img");
var qualityInfo = document.getElementById("quality-info");
var submitBtn = document.getElementById("submit-btn");
var retakeBtn = document.getElementById("retake-btn");
var retryBtn = document.getElementById("retry-btn");
var selectedFile = null;
takeBtn.addEventListener("click", function() { fileInput.click(); });
fileInput.addEventListener("change", function() {
var file = fileInput.files[0];
if (!file) return;
selectedFile = file;
var reader = new FileReader();
reader.onload = function(e) {
previewImg.src = e.target.result;
previewImg.onload = function() {
var w = previewImg.naturalWidth;
var h = previewImg.naturalHeight;
var sizeMB = (file.size / 1024 / 1024).toFixed(1);
var ok = w >= 400 && h >= 250 && file.size >= 50000;
qualityInfo.innerHTML = ok
? '<span style="color:#059669">✓ ' + w + '×' + h + ' pixels, ' + sizeMB + 'MB — looks good</span>'
: '<span style="color:#dc2626">⚠ ' + w + '×' + h + ' pixels, ' + sizeMB + 'MB — image may be too small. Try getting closer to the ID.</span>';
};
};
reader.readAsDataURL(file);
uploadSection.style.display = "none";
previewSection.style.display = "block";
});
retakeBtn.addEventListener("click", function() {
selectedFile = null;
fileInput.value = "";
previewSection.style.display = "none";
uploadSection.style.display = "block";
});
retryBtn.addEventListener("click", function() {
errorSection.style.display = "none";
uploadSection.style.display = "block";
});
submitBtn.addEventListener("click", function() {
if (!selectedFile || !orderNumber) return;
previewSection.style.display = "none";
uploadingSection.style.display = "block";
var formData = new FormData();
formData.append("photo_id", selectedFile);
formData.append("order_number", orderNumber);
fetch(API + "/api/v1/compliance-orders/" + orderNumber + "/upload-id", {
method: "POST",
headers: token ? { "Authorization": "Bearer " + token } : {},
body: formData,
})
.then(function(r) {
if (!r.ok) return r.json().then(function(d) { throw new Error(d.error || "Upload failed"); });
return r.json();
})
.then(function() {
uploadingSection.style.display = "none";
successSection.style.display = "block";
})
.catch(function(err) {
uploadingSection.style.display = "none";
errorSection.style.display = "block";
document.getElementById("error-msg").textContent = err.message;
});
});
// If no order number, show error
if (!orderNumber) {
uploadSection.innerHTML = '<div class="status error"><p style="font-size:14px;color:#991b1b">Invalid link — no order number found. Please use the link from your email.</p></div>';
}
</script>
</body>
</html>