Reverse-Face-Search / script.js
ReverseFaceSearch's picture
Update script.js
953b496 verified
Raw
History Blame Contribute Delete
8.43 kB
(() => {
"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);
});
})();