(() => { "use strict"; const destination = "https://www.socialsleuth.xyz/face-search"; const handoffEndpoint = "https://www.socialsleuth.xyz/api/face-search/handoff"; const maxDimensionPx = 1600; const targetMaxBytes = 1.5 * 1024 * 1024; const backendMaxBytes = 2 * 1024 * 1024; const uploadHardLimitBytes = 50 * 1024 * 1024; const jpegQualitySteps = [0.82, 0.75, 0.68, 0.6, 0.5, 0.42]; const uploadCard = document.getElementById("upload-card"); const photoInput = document.getElementById("photo-input"); const uploadEmpty = document.getElementById("upload-empty"); const uploadPreview = document.getElementById("upload-preview"); const previewImage = document.getElementById("preview-image"); const fileName = document.getElementById("file-name"); const changePhoto = document.getElementById("change-photo"); const uploadError = document.getElementById("upload-error"); const startSearch = document.getElementById("start-search"); let previewUrl = ""; let selectedFile = null; let isSubmitting = false; let finalHandoffUrl = ""; const openTopLevelWindow = (url = "about:blank") => { return window.open(url, "_blank"); }; document .querySelectorAll(".how-it-works .section-button, .final-cta .primary-button") .forEach((link) => { link.addEventListener("click", (event) => { event.preventDefault(); openTopLevelWindow(destination); }); }); const isSupportedImage = (file) => { if (!file) return false; const acceptedTypes = ["image/jpeg", "image/jpg", "image/png"]; if (acceptedTypes.includes(file.type.toLowerCase())) return true; return /\.(jpe?g|png)$/i.test(file.name); }; const openFilePicker = () => { if (isSubmitting) return; photoInput.click(); }; const showError = (message = "Please choose a JPG or PNG image.") => { uploadError.textContent = message; uploadError.hidden = false; }; const clearError = () => { uploadError.hidden = true; }; const displayFile = (file) => { if (!isSupportedImage(file)) { showError(); return; } if (file.size > uploadHardLimitBytes) { showError("That photo is too large to process. Please choose another image."); return; } clearError(); selectedFile = file; finalHandoffUrl = ""; if (previewUrl) URL.revokeObjectURL(previewUrl); previewUrl = URL.createObjectURL(file); previewImage.src = previewUrl; previewImage.alt = `Preview of selected photo: ${file.name}`; fileName.textContent = file.name; uploadEmpty.hidden = true; uploadPreview.hidden = false; startSearch.disabled = false; startSearch.textContent = "START SEARCH"; }; photoInput.addEventListener("change", () => { displayFile(photoInput.files && photoInput.files[0]); photoInput.value = ""; }); uploadCard.addEventListener("click", openFilePicker); uploadCard.addEventListener("keydown", (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); openFilePicker(); } }); ["dragenter", "dragover"].forEach((eventName) => { uploadCard.addEventListener(eventName, (event) => { event.preventDefault(); uploadCard.classList.add("is-dragging"); }); }); ["dragleave", "drop"].forEach((eventName) => { uploadCard.addEventListener(eventName, (event) => { event.preventDefault(); uploadCard.classList.remove("is-dragging"); }); }); uploadCard.addEventListener("drop", (event) => { if (isSubmitting) return; displayFile(event.dataTransfer.files && event.dataTransfer.files[0]); }); changePhoto.addEventListener("click", (event) => { event.stopPropagation(); if (isSubmitting) return; openFilePicker(); }); const loadImage = (file) => new Promise((resolve, reject) => { const url = URL.createObjectURL(file); const image = new Image(); image.onload = () => resolve({ image, url }); image.onerror = () => { URL.revokeObjectURL(url); reject(new Error("IMAGE_DECODE_FAILED")); }; image.src = url; }); const canvasToBlob = (canvas, quality) => new Promise((resolve, reject) => { canvas.toBlob( (blob) => (blob ? resolve(blob) : reject(new Error("CANVAS_ENCODE_FAILED"))), "image/jpeg", quality ); }); // Converted from production's validated imageCompression.js settings so // common phone photos fit the existing 2MB temporary-session limit. const prepareImageForHandoff = async (file) => { if (file.size <= 1024 * 1024) return file; let sourceUrl = ""; try { const loaded = await loadImage(file); const image = loaded.image; sourceUrl = loaded.url; const longEdge = Math.max(image.naturalWidth || 0, image.naturalHeight || 0); const scale = longEdge > maxDimensionPx ? maxDimensionPx / longEdge : 1; const width = Math.max(1, Math.round((image.naturalWidth || maxDimensionPx) * scale)); const height = Math.max(1, Math.round((image.naturalHeight || maxDimensionPx) * scale)); const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; const context = canvas.getContext("2d"); if (!context) return file; context.imageSmoothingEnabled = true; context.imageSmoothingQuality = "high"; context.drawImage(image, 0, 0, width, height); let blob = null; for (const quality of jpegQualitySteps) { blob = await canvasToBlob(canvas, quality); if (blob.size <= targetMaxBytes) break; } if (!blob || blob.size >= file.size) return file; const baseName = String(file.name || "photo").replace(/\.[^./]+$/, "") || "photo"; return new File([blob], `${baseName}.jpg`, { type: "image/jpeg", lastModified: Date.now(), }); } finally { if (sourceUrl) URL.revokeObjectURL(sourceUrl); } }; startSearch.addEventListener("click", async () => { if (startSearch.disabled || isSubmitting) return; if (finalHandoffUrl) { window.open(finalHandoffUrl, "_blank"); return; } if (!selectedFile) return; isSubmitting = true; startSearch.disabled = true; startSearch.textContent = "PREPARING SEARCH…"; clearError(); try { const preparedFile = await prepareImageForHandoff(selectedFile); if (!isSupportedImage(preparedFile) || preparedFile.size > backendMaxBytes) { throw new Error("IMAGE_TOO_LARGE"); } startSearch.textContent = "TRANSFERRING PHOTO…"; const form = new FormData(); form.append("image", preparedFile, preparedFile.name || "photo.jpg"); const response = await fetch(handoffEndpoint, { method: "POST", body: form, credentials: "omit", }); const data = await response.json().catch(() => null); if (!response.ok || !data?.ok || typeof data.handoffToken !== "string") { throw new Error(data?.error || "HANDOFF_FAILED"); } finalHandoffUrl = `${destination}?handoff=${encodeURIComponent(data.handoffToken)}`; isSubmitting = false; startSearch.disabled = false; startSearch.textContent = "CONTINUE TO SEARCH"; } catch (_) { finalHandoffUrl = ""; isSubmitting = false; startSearch.disabled = false; startSearch.textContent = "START SEARCH"; showError("We couldn't transfer that photo. Please try again."); } }); document.querySelectorAll(".faq-question").forEach((button) => { const answer = document.getElementById(button.getAttribute("aria-controls")); const item = button.closest(".faq-item"); button.addEventListener("click", () => { const willOpen = button.getAttribute("aria-expanded") !== "true"; button.setAttribute("aria-expanded", String(willOpen)); item.classList.toggle("is-open", willOpen); if (willOpen) { answer.hidden = false; requestAnimationFrame(() => answer.classList.add("is-open")); } else { answer.classList.remove("is-open"); window.setTimeout(() => { if (button.getAttribute("aria-expanded") === "false") answer.hidden = true; }, 230); } }); }); window.addEventListener("beforeunload", () => { if (previewUrl) URL.revokeObjectURL(previewUrl); }); })();