new-site/site/public/upload/id/index.html
justin f8cd37ac8c Initial commit — Performance West telecom compliance platform
Includes: API (Express/TypeScript), Astro site, Python workers,
document generators, FCC compliance tools, Canada CRTC formation,
Ansible infrastructure, and deployment scripts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 06:54:22 -05:00

202 lines
No EOL
12 KiB
HTML

<!DOCTYPE html><html lang="en" data-astro-cid-gbztjzwt> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Upload Your ID &mdash; Performance West</title><meta name="robots" content="noindex, nofollow"><link rel="stylesheet" href="/_astro/about.DhmoKVOS.css">
<link rel="stylesheet" href="/_astro/id.CO0Q-ZdR.css"></head> <body data-astro-cid-gbztjzwt> <div class="container" data-astro-cid-gbztjzwt> <!-- Logo --> <div class="logo-wrap" data-astro-cid-gbztjzwt> <span class="logo-text" data-astro-cid-gbztjzwt>Performance West</span> </div> <!-- Loading state --> <div id="state-loading" class="loading" data-astro-cid-gbztjzwt> <div class="spinner" data-astro-cid-gbztjzwt></div> <p data-astro-cid-gbztjzwt>Validating upload link...</p> </div> <!-- Error state (invalid/expired token) --> <div id="state-error" style="display:none;" data-astro-cid-gbztjzwt> <div class="error-box" data-astro-cid-gbztjzwt> <svg style="display:block;margin:0 auto 0.75rem;width:2.5rem;height:2.5rem;color:#dc2626;" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" data-astro-cid-gbztjzwt> <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" data-astro-cid-gbztjzwt></path> </svg> <p id="error-message" data-astro-cid-gbztjzwt>This upload link is invalid or has expired. Please request a new link from your order form.</p> </div> </div> <!-- Upload form --> <div id="state-form" style="display:none;" data-astro-cid-gbztjzwt> <h1 data-astro-cid-gbztjzwt>Upload your ID</h1> <p class="subtitle" data-astro-cid-gbztjzwt>This link expires in 24 hours</p> <!-- Front of ID --> <div class="upload-section" data-astro-cid-gbztjzwt> <label class="upload-label" data-astro-cid-gbztjzwt>Front of ID</label> <div class="upload-zone" id="zone-front" data-astro-cid-gbztjzwt> <input type="file" id="file-front" accept="image/*,.pdf" capture="environment" data-astro-cid-gbztjzwt> <div class="placeholder" id="placeholder-front" data-astro-cid-gbztjzwt> <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" data-astro-cid-gbztjzwt> <path stroke-linecap="round" stroke-linejoin="round" d="M6.827 6.175A2.31 2.31 0 015.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 00-1.134-.175 2.31 2.31 0 01-1.64-1.055l-.822-1.316a2.192 2.192 0 00-1.736-1.039 48.774 48.774 0 00-5.232 0 2.192 2.192 0 00-1.736 1.039l-.821 1.316z" data-astro-cid-gbztjzwt></path> <path stroke-linecap="round" stroke-linejoin="round" d="M16.5 12.75a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0z" data-astro-cid-gbztjzwt></path> </svg>
Tap to take a photo or select file
<div class="formats" data-astro-cid-gbztjzwt>JPG, PNG, or PDF &mdash; max 10 MB</div> </div> </div> <div id="preview-front" style="display:none;margin-top:0.5rem;" data-astro-cid-gbztjzwt></div> </div> <!-- Back of ID --> <div class="upload-section" data-astro-cid-gbztjzwt> <label class="upload-label" data-astro-cid-gbztjzwt>Back of ID</label> <div class="upload-zone" id="zone-back" data-astro-cid-gbztjzwt> <input type="file" id="file-back" accept="image/*,.pdf" capture="environment" data-astro-cid-gbztjzwt> <div class="placeholder" id="placeholder-back" data-astro-cid-gbztjzwt> <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" data-astro-cid-gbztjzwt> <path stroke-linecap="round" stroke-linejoin="round" d="M6.827 6.175A2.31 2.31 0 015.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 00-1.134-.175 2.31 2.31 0 01-1.64-1.055l-.822-1.316a2.192 2.192 0 00-1.736-1.039 48.774 48.774 0 00-5.232 0 2.192 2.192 0 00-1.736 1.039l-.821 1.316z" data-astro-cid-gbztjzwt></path> <path stroke-linecap="round" stroke-linejoin="round" d="M16.5 12.75a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0z" data-astro-cid-gbztjzwt></path> </svg>
Tap to take a photo or select file
<div class="formats" data-astro-cid-gbztjzwt>JPG, PNG, or PDF &mdash; max 10 MB</div> </div> </div> <div id="preview-back" style="display:none;margin-top:0.5rem;" data-astro-cid-gbztjzwt></div> </div> <!-- Submit --> <button type="button" id="btn-upload" class="btn-submit" disabled data-astro-cid-gbztjzwt>Upload ID</button> <div id="upload-status" class="status-msg" style="display:none;" data-astro-cid-gbztjzwt></div> </div> <!-- Success state --> <div id="state-success" style="display:none;" data-astro-cid-gbztjzwt> <div class="success-box" data-astro-cid-gbztjzwt> <div class="icon" data-astro-cid-gbztjzwt> <svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" data-astro-cid-gbztjzwt> <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" data-astro-cid-gbztjzwt></path> </svg> </div> <h2 data-astro-cid-gbztjzwt>ID uploaded successfully</h2> <p data-astro-cid-gbztjzwt>You can close this page and return to the order form.</p> </div> </div> </div> <div class="footer-line" data-astro-cid-gbztjzwt>
Performance West Inc. &mdash; Secure ID verification
</div> <script>
window.__PW_API = (function() {
var h = window.location.hostname;
if (h === "performancewest.net" || h === "www.performancewest.net") return "https://api.performancewest.net";
return "http://" + h + ":3001";
})();
</script> <script>
(function() {
var API = window.__PW_API;
var MAX_SIZE = 10 * 1024 * 1024; // 10 MB
var ALLOWED = ["image/jpeg", "image/png", "image/jpg", "application/pdf"];
// Get token from URL
var params = new URLSearchParams(window.location.search);
var token = params.get("token");
var stateLoading = document.getElementById("state-loading");
var stateError = document.getElementById("state-error");
var stateForm = document.getElementById("state-form");
var stateSuccess = document.getElementById("state-success");
var errorMessage = document.getElementById("error-message");
var fileFront = document.getElementById("file-front");
var fileBack = document.getElementById("file-back");
var zoneFront = document.getElementById("zone-front");
var zoneBack = document.getElementById("zone-back");
var previewFront = document.getElementById("preview-front");
var previewBack = document.getElementById("preview-back");
var btnUpload = document.getElementById("btn-upload");
var uploadStatus = document.getElementById("upload-status");
var frontFile = null;
var backFile = null;
function showState(state) {
stateLoading.style.display = state === "loading" ? "" : "none";
stateError.style.display = state === "error" ? "" : "none";
stateForm.style.display = state === "form" ? "" : "none";
stateSuccess.style.display = state === "success" ? "" : "none";
}
function formatBytes(bytes) {
if (bytes < 1024) return bytes + " B";
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + " KB";
return (bytes / 1048576).toFixed(1) + " MB";
}
function validateFile(file) {
if (!file) return "No file selected.";
if (file.size > MAX_SIZE) return "File is too large. Maximum size is 10 MB.";
// Check extension as fallback for mime type
var ext = file.name.split(".").pop().toLowerCase();
var validExts = ["jpg", "jpeg", "png", "pdf"];
if (validExts.indexOf(ext) === -1 && ALLOWED.indexOf(file.type) === -1) {
return "Unsupported file type. Please use JPG, PNG, or PDF.";
}
return null;
}
function renderPreview(file, previewEl, zone, side) {
previewEl.innerHTML = "";
previewEl.style.display = "";
zone.querySelector(".placeholder").style.display = "none";
zone.classList.add("has-file");
var wrap = document.createElement("div");
wrap.className = "preview-wrap";
var ext = file.name.split(".").pop().toLowerCase();
if (ext === "pdf") {
var icon = document.createElement("div");
icon.className = "pdf-icon";
icon.innerHTML = "<span>PDF</span>";
wrap.appendChild(icon);
} else {
var img = document.createElement("img");
img.className = "preview-thumb";
img.src = URL.createObjectURL(file);
wrap.appendChild(img);
}
var info = document.createElement("div");
info.className = "preview-info";
info.innerHTML = '<div class="preview-name">' + file.name + '</div><div class="preview-size">' + formatBytes(file.size) + "</div>";
wrap.appendChild(info);
var removeBtn = document.createElement("button");
removeBtn.type = "button";
removeBtn.className = "preview-remove";
removeBtn.textContent = "Remove";
removeBtn.addEventListener("click", function() {
if (side === "front") { frontFile = null; fileFront.value = ""; }
else { backFile = null; fileBack.value = ""; }
previewEl.style.display = "none";
previewEl.innerHTML = "";
zone.querySelector(".placeholder").style.display = "";
zone.classList.remove("has-file");
updateSubmitState();
});
wrap.appendChild(removeBtn);
previewEl.appendChild(wrap);
}
function updateSubmitState() {
btnUpload.disabled = !(frontFile && backFile);
}
function handleFileSelect(input, zone, previewEl, side) {
var file = input.files[0];
if (!file) return;
var err = validateFile(file);
if (err) {
alert(err);
input.value = "";
return;
}
if (side === "front") frontFile = file;
else backFile = file;
renderPreview(file, previewEl, zone, side);
updateSubmitState();
}
fileFront.addEventListener("change", function() {
handleFileSelect(fileFront, zoneFront, previewFront, "front");
});
fileBack.addEventListener("change", function() {
handleFileSelect(fileBack, zoneBack, previewBack, "back");
});
// Submit
btnUpload.addEventListener("click", function() {
if (!frontFile || !backFile) return;
btnUpload.disabled = true;
btnUpload.textContent = "Uploading...";
uploadStatus.style.display = "none";
var formData = new FormData();
formData.append("front", frontFile);
formData.append("back", backFile);
fetch(API + "/api/v1/id-upload/" + encodeURIComponent(token), {
method: "POST",
body: formData,
})
.then(function(res) { return res.json().then(function(data) { return { ok: res.ok, data: data }; }); })
.then(function(result) {
if (result.ok && result.data.success) {
showState("success");
} else {
throw new Error(result.data.error || "Upload failed. Please try again.");
}
})
.catch(function(err) {
uploadStatus.style.display = "";
uploadStatus.className = "status-msg error";
uploadStatus.textContent = err.message || "Something went wrong. Please try again.";
btnUpload.disabled = false;
btnUpload.textContent = "Upload ID";
});
});
// Validate token on load
if (!token) {
errorMessage.textContent = "No upload token provided. Please use the link from your order form.";
showState("error");
return;
}
fetch(API + "/api/v1/id-upload/" + encodeURIComponent(token) + "/status")
.then(function(res) { return res.json().then(function(data) { return { ok: res.ok, data: data }; }); })
.then(function(result) {
if (result.ok && result.data.valid) {
if (result.data.uploaded) {
// Already uploaded
showState("success");
} else {
showState("form");
}
} else {
errorMessage.textContent = result.data.error || "This upload link is invalid or has expired.";
showState("error");
}
})
.catch(function() {
errorMessage.textContent = "Could not validate this upload link. Please check your connection and try again.";
showState("error");
});
})();
</script> </body> </html>