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:
parent
daf6d1f831
commit
1611b67543
1 changed files with 204 additions and 0 deletions
204
site/public/portal/upload-id/index.html
Normal file
204
site/public/portal/upload-id/index.html
Normal 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. · (888) 411-0383 · 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue