| (() => { |
| "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 |
| ); |
| }); |
|
|
| |
| |
| 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); |
| }); |
| })(); |
|
|