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>
202 lines
No EOL
12 KiB
HTML
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 — 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 — 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 — 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. — 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> |