(function () { const root = document.getElementById("photoforge-app"); const config = JSON.parse(document.getElementById("photoforge-config")?.textContent || "{}"); const assets = config.assets || {}; const VIEWS = { LANDING: "landing", AUTH: "auth", IDLE: "idle", TEXT: "text_input", ANALYZING: "analyzing", DESCRIPTION: "description_ready", GENERATING: "generating", COMPLETE: "complete", }; const LEGAL_CONTENT = { contact: { title: "Contact Us", content: `

Ask us anything, or share your feedback. We usually answer within the same business day.

support@rekognize.io

`, }, privacy: { title: "Privacy Policy", content: `

We only collect the content you upload, your email (if provided), and basic technical data needed to operate the site. We use this information solely to run and improve photoforge.io and do not sell your data. Limited third-party services may process data on our behalf. You can request access or deletion of your data anytime through our Contact Us page. This site is not intended for children under 13. We may update this policy, and the latest version will always be posted here.

`, }, terms: { title: "Terms of Service", content: `

By using PhotoForge, you agree to use the site lawfully and not misuse, disrupt, or copy the service. All site content is owned by Rekognize OU. The service is provided as is, without warranties, and we are not liable for any indirect or consequential damages. We may change these terms or suspend access at any time. Estonian law applies. For questions, visit our Contact Us page.

`, }, cookies: { title: "Cookie Policy", content: `

PhotoForge uses essential cookies to operate the site. Optional analytics cookies may be used only with your consent. You can disable cookies in your browser, but some features may not work. Third-party services we use may set their own cookies. For questions, see our Contact Us page.

`, }, }; const state = { view: VIEWS.LANDING, isSubmitting: false, inputImage: null, photoUuid: null, description: "", outputImages: [], progress: 0, error: null, userEmail: config.user?.email || null, theme: localStorage.getItem("photoforge-theme") || "light", count: 1, imageStyle: "natural", imageQuality: "standard", gallery: [], uploadedImages: [], showGallery: false, showPurchase: false, selectedPurchaseTier: "sm", credits: Number.isFinite(config.user?.credits) ? config.user.credits : 0, activeLegalPage: null, authSent: false, purchaseMode: "select", clientSecret: null, stripePublicKey: null, }; let progressTimer = null; let generationTarget = 0; let checkoutInstance = null; let mountedClientSecret = null; let stripeInstance = null; function html(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function icon(name, classes = "h-5 w-5") { return ``; } function tooltip(label, content) { return ` ${content} ${html(label)} `; } function getCookie(name) { return document.cookie .split(";") .map((cookie) => cookie.trim()) .find((cookie) => cookie.startsWith(`${name}=`)) ?.slice(name.length + 1) || ""; } function csrfHeaders() { const token = getCookie("csrftoken") || document.querySelector('meta[name="csrf-token"]')?.content || ""; return token ? { "X-CSRFToken": decodeURIComponent(token) } : {}; } async function readJsonResponse(response) { const contentType = response.headers.get("content-type") || ""; const payload = contentType.includes("application/json") ? await response.json() : {}; if (!response.ok) { const message = payload.error || payload.detail || response.statusText || "Request failed"; const error = new Error(message); error.payload = payload; error.status = response.status; throw error; } return payload; } function setView(view) { if (![VIEWS.ANALYZING, VIEWS.GENERATING].includes(view)) { clearProgressTimer(); } state.view = view; enforceAuthGuard(); render(); } function enforceAuthGuard() { if (!state.userEmail && ![VIEWS.LANDING, VIEWS.AUTH].includes(state.view)) { state.view = VIEWS.LANDING; } } function resetFlow() { clearProgressTimer(); state.view = VIEWS.IDLE; state.inputImage = null; state.photoUuid = null; state.description = ""; state.outputImages = []; state.progress = 0; state.count = 1; state.imageStyle = "natural"; state.imageQuality = "standard"; render(); } function showError(message, returnView = VIEWS.LANDING) { state.error = message; render(); window.setTimeout(() => { state.error = null; if (returnView) { state.view = returnView; if (returnView === VIEWS.LANDING) { state.inputImage = null; } } render(); }, 3000); } function updateProgressUi() { const circle = root.querySelector("[data-progress-circle]"); if (circle) { const circumference = 283; circle.style.strokeDashoffset = String(circumference - (circumference * state.progress) / 100); } const bar = root.querySelector("[data-progress-bar]"); if (bar) { bar.style.width = `${Math.max(0, Math.min(100, state.progress))}%`; } } function clearProgressTimer() { if (progressTimer) { window.clearInterval(progressTimer); progressTimer = null; } } function startProgressTimer(kind) { clearProgressTimer(); progressTimer = window.setInterval(() => { const target = kind === VIEWS.ANALYZING ? 95 : generationTarget; const diff = target - state.progress; if (diff > 0.1) { state.progress += diff * 0.01; updateProgressUi(); } }, 100); } async function checkAuth() { try { const response = await fetch("/api/profiles/me", { credentials: "same-origin" }); if (!response.ok) { state.userEmail = null; state.credits = 0; enforceAuthGuard(); render(); return; } const data = await response.json(); state.userEmail = data.email; state.credits = data.credits ?? state.credits; render(); await fetchGallery(); } catch (_error) { render(); } } async function fetchGallery() { if (!state.userEmail) { state.gallery = []; render(); return; } try { const response = await fetch("/api/photos/gallery", { credentials: "same-origin" }); const data = await readJsonResponse(response); state.gallery = data.map((item) => ({ id: String(item.id), imageUrl: item.photo_url || "", description: item.description || "", sourceImage: item.source_photo_url || "", style: item.params?.style || "natural", quality: item.params?.quality || "standard", createdAt: new Date(item.created).getTime(), })); render(); } catch (error) { console.error("Gallery fetch failed", error); } } async function handleLogout() { try { await fetch("/api/profiles/auth/logout", { method: "POST", credentials: "same-origin", headers: { ...csrfHeaders(), "Content-Type": "application/json" }, }); } catch (error) { console.error(error); } state.userEmail = null; state.credits = 0; state.gallery = []; state.view = VIEWS.LANDING; render(); } async function requestLogin(email) { state.isSubmitting = true; render(); try { const response = await fetch("/api/profiles/auth/email", { method: "POST", credentials: "same-origin", headers: { ...csrfHeaders(), "Content-Type": "application/json" }, body: JSON.stringify({ email }), }); await readJsonResponse(response); state.authSent = true; } catch (error) { window.alert("Failed to send login link. Please try again."); } finally { state.isSubmitting = false; render(); } } async function processSelectedFile(file, previewUrl = null) { if (!file) return; state.inputImage = previewUrl || URL.createObjectURL(file); state.photoUuid = null; state.description = ""; state.outputImages = []; state.progress = 0; state.view = VIEWS.ANALYZING; render(); startProgressTimer(VIEWS.ANALYZING); try { const formData = new FormData(); formData.append("photo", file); const uploadResponse = await fetch("/api/photos/upload", { method: "POST", credentials: "same-origin", headers: csrfHeaders(), body: formData, }); const uploadData = await readJsonResponse(uploadResponse); state.photoUuid = uploadData.uuid; const uploaded = { id: uploadData.uuid, url: state.inputImage, createdAt: Date.now(), description: "", }; state.uploadedImages = [uploaded, ...state.uploadedImages.filter((item) => item.id !== uploaded.id)].slice(0, 10); const describeResponse = await fetch(`/api/photos/describe/${uploadData.uuid}`, { method: "POST", credentials: "same-origin", headers: csrfHeaders(), }); const describeData = await readJsonResponse(describeResponse); state.description = describeData.description || ""; state.uploadedImages = state.uploadedImages.map((item) => item.id === uploadData.uuid ? { ...item, description: state.description } : item, ); state.progress = 100; clearProgressTimer(); state.view = VIEWS.DESCRIPTION; render(); } catch (error) { console.error("Processing failed", error); clearProgressTimer(); const code = error.payload?.code; if (code === "auth_required") { state.view = VIEWS.AUTH; state.error = "Please sign in before uploading an image."; render(); return; } showError(error.message || "Failed to process image."); } } async function processImageUrl(url) { state.showGallery = false; state.inputImage = url; state.photoUuid = null; state.description = ""; state.outputImages = []; state.progress = 0; state.view = VIEWS.ANALYZING; render(); startProgressTimer(VIEWS.ANALYZING); try { const response = await fetch(url, { credentials: "same-origin" }); if (!response.ok) throw new Error("Could not load source image."); const blob = await response.blob(); const extension = blob.type.split("/")[1] || "png"; await processSelectedFile(new File([blob], `source.${extension}`, { type: blob.type }), url); } catch (error) { console.error(error); clearProgressTimer(); showError("Could not reuse this image. Please upload it again.", VIEWS.IDLE); } } async function beginGeneration() { if (!state.description.trim()) return; if (!state.photoUuid) { window.alert("Please upload an image first."); setView(VIEWS.IDLE); return; } const generationCount = Number(state.count); const cost = state.imageQuality === "hd" ? 2 : 1; const images = []; state.outputImages = []; state.progress = 0; state.view = VIEWS.GENERATING; generationTarget = 5; render(); startProgressTimer(VIEWS.GENERATING); try { for (let index = 0; index < generationCount; index += 1) { const stepSize = 100 / generationCount; generationTarget = (index + 0.95) * stepSize; const response = await fetch(`/api/photos/forge/${state.photoUuid}`, { method: "POST", credentials: "same-origin", headers: { ...csrfHeaders(), "Content-Type": "application/json" }, body: JSON.stringify({ style: state.imageStyle, quality: state.imageQuality, }), }); const forged = await readJsonResponse(response); images.push(forged.photo_url || forged.url || ""); state.credits = Math.max(0, state.credits - cost); state.progress = (index + 1) * stepSize; updateProgressUi(); } generationTarget = 100; state.progress = 100; updateProgressUi(); await new Promise((resolve) => window.setTimeout(resolve, 400)); clearProgressTimer(); state.outputImages = images.filter(Boolean); state.gallery = [ ...state.outputImages.map((imageUrl) => ({ id: Math.random().toString(36).slice(2), imageUrl, sourceImage: state.inputImage || "", description: state.description || "Generated Image", style: state.imageStyle, quality: state.imageQuality, createdAt: Date.now(), })), ...state.gallery, ]; state.view = state.outputImages.length ? VIEWS.COMPLETE : VIEWS.DESCRIPTION; render(); checkAuth(); fetchGallery(); } catch (error) { console.error("Generation error", error); clearProgressTimer(); if (error.payload?.code === "no_credit") { window.alert("Not enough credits to continue generation."); } else { window.alert("Generation failed. Please try again."); } state.view = VIEWS.DESCRIPTION; render(); checkAuth(); } } async function ensureStripePublicKey() { if (state.stripePublicKey) return state.stripePublicKey; const response = await fetch("/payment/config", { credentials: "same-origin" }); const data = await readJsonResponse(response); state.stripePublicKey = data.publicKey; return state.stripePublicKey; } function destroyCheckout() { if (checkoutInstance) { try { checkoutInstance.destroy(); } catch (_error) { // Stripe may already have torn down the iframe after a redirect. } } checkoutInstance = null; mountedClientSecret = null; } async function waitForStripe() { for (let attempt = 0; attempt < 50; attempt += 1) { if (window.Stripe) return; await new Promise((resolve) => window.setTimeout(resolve, 100)); } throw new Error("Stripe.js could not be loaded."); } async function mountCheckout() { if (!state.clientSecret || mountedClientSecret === state.clientSecret) return; const checkoutMount = document.getElementById("checkout"); if (!checkoutMount) return; try { await waitForStripe(); const publicKey = await ensureStripePublicKey(); stripeInstance = stripeInstance || window.Stripe(publicKey); destroyCheckout(); checkoutInstance = await stripeInstance.initEmbeddedCheckout({ clientSecret: state.clientSecret }); checkoutInstance.mount("#checkout"); mountedClientSecret = state.clientSecret; } catch (error) { console.error(error); checkoutMount.innerHTML = `

${html(error.message || "Could not load checkout.")}

`; } } async function openPurchase(tier = null) { if (!state.userEmail) { setView(VIEWS.AUTH); return; } destroyCheckout(); state.showPurchase = true; state.selectedPurchaseTier = tier || state.selectedPurchaseTier || "sm"; state.purchaseMode = tier ? "checkout" : "select"; state.clientSecret = null; render(); if (!tier) return; try { await ensureStripePublicKey(); const response = await fetch(`/payment/session?tier=${encodeURIComponent(tier)}`, { method: "POST", credentials: "same-origin", headers: { ...csrfHeaders(), "Content-Type": "application/json" }, }); const data = await readJsonResponse(response); state.clientSecret = data.clientSecret; render(); } catch (error) { console.error("Stripe session error", error); showError("Could not prepare checkout.", null); } } function closePurchase() { destroyCheckout(); state.showPurchase = false; state.purchaseMode = "select"; state.clientSecret = null; render(); } function downloadUrl(url, filename) { const link = document.createElement("a"); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); link.remove(); } function formatDate(timestamp) { if (!timestamp) return ""; return new Date(timestamp).toLocaleDateString(); } function formatTime(timestamp) { if (!timestamp) return ""; return new Date(timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); } function navTemplate() { const showReset = ![VIEWS.IDLE, VIEWS.LANDING, VIEWS.AUTH].includes(state.view); return ` `; } function landingTemplate() { const cards = [ ["shield-check", "Generate Royalty-Free Photos", "Create unique alternatives to stock photos. Safe for commercial use without licensing fees.", assets.stock, "Royalty Free"], ["eye-off", "Anonymize People", "Protect privacy by realistically altering faces while preserving the original context and emotion.", assets.anon, "Anonymize"], ["palette", "Creative Modifications", "Transform ordinary photos into artistic masterpieces. Change seasons, styles, or entire environments.", assets.fun, "Creative"], ["pen-tool", "Enhance Sketches", "Turn rough drawings and concepts into polished, high-fidelity illustrations and renders.", assets.sketch, "Sketches"], ]; return `

image-to-image transformation tool

${processStep("image", "Source", "bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400")} ${icon("arrow-right", "h-6 w-6 text-zinc-300 dark:text-zinc-700")} ${processStep("file-text", "Description", "border-2 border-dashed border-zinc-200 bg-zinc-50 text-zinc-400 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-600")} ${icon("arrow-right", "h-6 w-6 text-zinc-300 dark:text-zinc-700")} ${processStep("sparkles", "Result", "bg-zinc-900 text-white shadow-xl shadow-zinc-200/50 dark:bg-white dark:text-black dark:shadow-none")}

Upload an image. PhotoForge analyzes and describes it in detailed text. Then, it uses this description to generate a new version of the original image.

${cards.map(([iconName, title, copy, src, alt]) => `
${html(alt)}
${icon(iconName, "h-5 w-5")}

${html(title)}

${html(copy)}

`).join("")}
`; } function processStep(iconName, label, classes) { return `
${icon(iconName, "h-8 w-8")}
${html(label)}
`; } function authTemplate() { return `

Sign in

Enter your email to continue. We'll send you a secure login link.

${ state.authSent ? `
${icon("check", "h-8 w-8 text-green-600 dark:text-green-400")}

Check your email

We've sent a secure login link to your inbox. Click the link to sign in.

` : `
${icon("mail", "h-5 w-5 text-zinc-400")}
` }
By clicking continue, you agree to our Terms of Service and Privacy Policy.
`; } function idleTemplate() { return `

Transform Reality.

Advanced image-to-image synthesis for the creative mind.

Or
${state.uploadedImages.length ? `

Previously Uploaded

${state.uploadedImages.map((image) => ` `).join("")}
` : ""}
`; } function optionsTemplate() { const countButtons = [1, 2, 4].map((number) => optionButton(number, String(number), state.count === number, "count")).join(""); const styleButtons = ["natural", "vivid"].map((style) => optionButton(style, style, state.imageStyle === style, "style", true)).join(""); const qualityButtons = ["standard", "hd"].map((quality) => { const label = `${quality}${quality === "hd" ? '2 Credits' : ""}`; return optionButton(quality, label, state.imageQuality === quality, "quality"); }).join(""); return `
${optionGroup("Variations", countButtons)} ${optionGroup("Style", styleButtons)}
${optionGroupInner("Quality", qualityButtons)}
`; } function optionGroup(label, buttons) { return `
${optionGroupInner(label, buttons)}
`; } function optionGroupInner(label, buttons) { return ` ${html(label)}
${buttons}
`; } function optionButton(value, label, active, type, capitalize = false) { return ` `; } function textInputTemplate() { return `

Define Your Image

${optionsTemplate()}
`; } function analyzingTemplate() { const circumference = 283; const offset = circumference - (circumference * state.progress) / 100; return `
${state.inputImage ? `
Analyzing
` : icon("image", "h-8 w-8 text-zinc-400 dark:text-zinc-500")}

Analyzing Composition

Extracting semantic features and depth maps...

`; } function descriptionTemplate() { return `
Analysis Result Editable
${optionsTemplate()}
`; } function generatingTemplate() { return `

Synthesizing ${state.count} Variant${state.count > 1 ? "s" : ""}

`; } function completeTemplate() { return `
${state.inputImage ? splitResultTemplate() : textOnlyResultTemplate()}
`; } function splitResultTemplate() { const resultGrid = state.count === 1 ? "grid-cols-1" : state.count === 2 ? "grid grid-cols-1" : "grid grid-cols-2"; return `
Source
Source
${state.outputImages.map((imageUrl, index) => resultTile(imageUrl, index, true)).join("")}
`; } function textOnlyResultTemplate() { const grid = state.count === 1 ? "grid-cols-1" : state.count === 2 ? "grid-cols-2" : "grid-cols-2 grid-rows-2"; return `
${state.outputImages.map((imageUrl, index) => resultTile(imageUrl, index, false)).join("")}
`; } function resultTile(imageUrl, index, compact) { const wrapper = compact ? `relative h-full w-full overflow-hidden border-b border-zinc-200 last:border-b-0 dark:border-zinc-800 ${state.count === 2 ? "h-1/2" : ""} ${state.count === 4 ? "h-1/2 border-r even:border-r-0" : ""}` : "group/image relative h-full w-full overflow-hidden bg-white dark:bg-zinc-900"; return `
Result ${index + 1}
${tooltip("Download", ``)} ${tooltip(compact ? "Use as Source" : "Reimagine This", ``)}
${compact ? `V${index + 1}` : `Variant ${index + 1}`}
`; } function purchaseModalTemplate() { if (!state.showPurchase) return ""; return `
${state.purchaseMode === "checkout" ? `` : ""}

${state.purchaseMode === "checkout" ? "Checkout" : "Purchase Credits"}

Secure payment via StripeCredits never expire

${state.purchaseMode === "checkout" ? checkoutTemplate() : pricingTemplate()}
`; } function checkoutTemplate() { return `
${!state.clientSecret ? `
${icon("refresh-cw", "h-8 w-8 animate-spin text-zinc-400")}

Preparing checkout...

` : '
'}
`; } function pricingTemplate() { const tiers = [ ["sm", "25", "$5", "$0.20 / credit", false], ["md", "100", "$15", "$0.15 / credit (Save 25%)", true], ["lg", "500", "$50", "$0.10 / credit (Save 50%)", false], ]; return `
${tiers.map(([key, credits, price, detail, popular]) => `
${popular ? '
Most Popular
' : ""}
${credits}
Credits
${price}USD
${detail}
`).join("")}
${icon("shield-check", "h-4 w-4")} Secure SSL Payment
stripe
VISA
Mastercard
`; } function galleryModalTemplate() { if (!state.showGallery) return ""; return `
${icon("grid-2x2")}

Gallery

${state.gallery.length} items
${state.gallery.length ? galleryGridTemplate() : `
${icon("grid-2x2", "mb-4 h-16 w-16 opacity-20")}

Your gallery is empty

Generated images will automatically appear here.

`}
`; } function galleryGridTemplate() { return `
${state.gallery.map((item) => `
Saved ${item.sourceImage ? `
Source
Original Source
` : ""}
${tooltip("Copy Description", ``)} ${tooltip("Download Image", ``)} ${item.sourceImage ? tooltip("Reuse Original Source", ``) : ""} ${tooltip("Use Result as Source", ``)} ${tooltip("Remove", ``)}

${html(item.description)}

${formatDate(item.createdAt)}${formatTime(item.createdAt)}
`).join("")}
`; } function legalModalTemplate() { if (!state.activeLegalPage) return ""; const page = LEGAL_CONTENT[state.activeLegalPage]; return `

${html(page.title)}

${page.content}
`; } function toastTemplate() { if (!state.error) return ""; return `
${icon("shield-check")}${html(state.error)}
`; } function viewTemplate() { switch (state.view) { case VIEWS.AUTH: return authTemplate(); case VIEWS.IDLE: return idleTemplate(); case VIEWS.TEXT: return textInputTemplate(); case VIEWS.ANALYZING: return analyzingTemplate(); case VIEWS.DESCRIPTION: return descriptionTemplate(); case VIEWS.GENERATING: return generatingTemplate(); case VIEWS.COMPLETE: return completeTemplate(); case VIEWS.LANDING: default: return landingTemplate(); } } function render() { enforceAuthGuard(); root.innerHTML = `
${navTemplate()}
${viewTemplate()}
${purchaseModalTemplate()} ${galleryModalTemplate()} ${legalModalTemplate()} ${toastTemplate()}
`; if (window.lucide) { window.lucide.createIcons({ attrs: { "stroke-width": 1.8 } }); } if (state.showPurchase && state.purchaseMode === "checkout" && state.clientSecret) { window.setTimeout(mountCheckout, 0); } } root.addEventListener("click", async (event) => { const actionElement = event.target.closest("[data-action]"); if (!actionElement || !root.contains(actionElement)) return; if (actionElement.closest("[data-modal-panel]") && actionElement.dataset.action !== "close-legal") { event.stopPropagation(); } const { action } = actionElement.dataset; switch (action) { case "set-view": setView(actionElement.dataset.view); break; case "start-creating": if (!state.userEmail) setView(VIEWS.AUTH); else if (state.credits > 0) setView(VIEWS.IDLE); else openPurchase(); break; case "toggle-theme": state.theme = state.theme === "light" ? "dark" : "light"; localStorage.setItem("photoforge-theme", state.theme); render(); break; case "open-gallery": state.showGallery = true; render(); break; case "close-gallery": state.showGallery = false; render(); break; case "open-purchase": openPurchase(); break; case "close-purchase": closePurchase(); break; case "purchase-select-view": destroyCheckout(); state.purchaseMode = "select"; state.clientSecret = null; render(); break; case "purchase-tier": openPurchase(actionElement.dataset.tier); break; case "logout": handleLogout(); break; case "reset": resetFlow(); break; case "option": if (actionElement.dataset.option === "count") state.count = Number(actionElement.dataset.value); if (actionElement.dataset.option === "style") state.imageStyle = actionElement.dataset.value; if (actionElement.dataset.option === "quality") state.imageQuality = actionElement.dataset.value; render(); break; case "generate": beginGeneration(); break; case "reuse-upload": { const uploaded = state.uploadedImages.find((item) => item.id === actionElement.dataset.id); if (uploaded) { state.inputImage = uploaded.url; state.photoUuid = uploaded.id; state.description = uploaded.description || state.description; state.outputImages = []; state.progress = 0; state.view = state.description ? VIEWS.DESCRIPTION : VIEWS.IDLE; render(); } break; } case "download": downloadUrl(actionElement.dataset.url, actionElement.dataset.filename || "photo-forge.jpg"); break; case "copy-description": { const item = state.gallery.find((galleryItem) => galleryItem.id === actionElement.dataset.id); if (item) navigator.clipboard?.writeText(item.description); break; } case "remove-gallery": state.gallery = state.gallery.filter((item) => item.id !== actionElement.dataset.id); render(); break; case "use-url-source": processImageUrl(actionElement.dataset.url); break; case "legal": state.activeLegalPage = actionElement.dataset.page; render(); break; case "close-legal": if (event.target.closest("[data-modal-panel]") && !event.target.closest('button[data-action="close-legal"]')) return; state.activeLegalPage = null; render(); break; default: break; } }); root.addEventListener("submit", (event) => { const form = event.target.closest("form[data-form]"); if (!form) return; event.preventDefault(); if (form.dataset.form === "auth") { const formData = new FormData(form); requestLogin(String(formData.get("email") || "")); } }); root.addEventListener("change", (event) => { const input = event.target.closest("[data-file-input]"); if (!input) return; const file = input.files?.[0]; processSelectedFile(file); }); root.addEventListener("input", (event) => { const textarea = event.target.closest("[data-description-input]"); if (!textarea) return; state.description = textarea.value; root.querySelectorAll("[data-requires-description]").forEach((button) => { button.disabled = !state.description.trim(); }); }); document.addEventListener("keydown", (event) => { if (event.key !== "Escape") return; if (state.activeLegalPage) state.activeLegalPage = null; else if (state.showGallery) state.showGallery = false; else if (state.showPurchase) closePurchase(); render(); }); render(); checkAuth(); })();