/* ──────────────────────────── Three.js 3D PDF icon ──── */ (function () { const container = document.getElementById('three-header'); if (!container || typeof THREE === "undefined") return; const W = 120, H = 120; const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(W, H); container.appendChild(renderer.domElement); const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(38, W / H, 0.1, 100); camera.position.set(0, 0, 5); scene.add(new THREE.AmbientLight(0xffffff, 0.6)); const dirLight = new THREE.DirectionalLight(0xffffff, 0.9); dirLight.position.set(4, 6, 4); scene.add(dirLight); const bodyGeo = new THREE.BoxGeometry(1.4, 1.8, 0.12); const bodyMat = new THREE.MeshStandardMaterial({ color: 0xffffff }); const body = new THREE.Mesh(bodyGeo, bodyMat); scene.add(body); const cornerShape = new THREE.Shape(); cornerShape.moveTo(0, 0); cornerShape.lineTo(0.38, 0); cornerShape.lineTo(0, -0.38); cornerShape.closePath(); const cornerGeo = new THREE.ExtrudeGeometry(cornerShape, { depth: 0.13, bevelEnabled: false }); const cornerMat = new THREE.MeshStandardMaterial({ color: 0x5048c8 }); const corner = new THREE.Mesh(cornerGeo, cornerMat); corner.position.set(0.7, 0.9, 0); scene.add(corner); const stripeGeo = new THREE.BoxGeometry(0.9, 0.06, 0.14); const stripeMat = new THREE.MeshStandardMaterial({ color: 0xe24b4a }); [-0.2, 0.1, 0.4].forEach(y => { const s = new THREE.Mesh(stripeGeo, stripeMat); s.position.set(-0.12, y, 0); scene.add(s); }); const planeGeo = new THREE.PlaneGeometry(1.45, 1.85); const planeMat = new THREE.MeshStandardMaterial({ color: 0x5048c8 }); const plane = new THREE.Mesh(planeGeo, planeMat); plane.position.set(0.10, -0.10, -0.20); scene.add(plane); function animate() { requestAnimationFrame(animate); body.rotation.y += 0.012; corner.rotation.y = body.rotation.y; plane.rotation.y = body.rotation.y; scene.children.forEach(c => { if (c.isMesh) c.rotation.y = body.rotation.y; }); renderer.render(scene, camera); } animate(); })(); /* ──────────────────────────── App state ───────────────── */ const FREE_LIMIT = 15; let selectedFiles = []; const dropZone = document.getElementById('drop-zone'); const fileInput = document.getElementById('file-input'); const previewGrid = document.getElementById('preview-grid'); const counterBar = document.getElementById('counter-bar'); const countNum = document.getElementById('count-num'); const countDot = document.getElementById('count-dot'); const countLimit = document.getElementById('count-limit-text'); const convertBtn = document.getElementById('convert-btn'); const btnLabel = document.getElementById('btn-label'); const loadingBar = document.getElementById('loading-bar'); const clearBtn = document.getElementById('clear-btn'); const overlay = document.getElementById('overlay'); const toast = document.getElementById('toast'); if (dropZone && fileInput) { dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag-over'); }); dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over')); dropZone.addEventListener('drop', e => { e.preventDefault(); dropZone.classList.remove('drag-over'); addFiles(e.dataTransfer.files); }); fileInput.addEventListener('change', () => { addFiles(fileInput.files); fileInput.value = ''; }); } function addFiles(fileList) { const incoming = Array.from(fileList).filter(f => f.type.startsWith('image/')); const newTotal = selectedFiles.length + incoming.length; if (newTotal > FREE_LIMIT) { const allowed = incoming.slice(0, FREE_LIMIT - selectedFiles.length); allowed.forEach(pushFile); updateUI(); showPremiumDialog(); return; } incoming.forEach(pushFile); updateUI(); } function pushFile(file) { const url = URL.createObjectURL(file); selectedFiles.push({ file, url }); } function removeFile(index) { URL.revokeObjectURL(selectedFiles[index].url); selectedFiles.splice(index, 1); updateUI(); } function clearAll() { selectedFiles.forEach(f => URL.revokeObjectURL(f.url)); selectedFiles = []; updateUI(); } function updateUI() { const n = selectedFiles.length; if (n > 0) { counterBar.classList.add('visible'); countNum.textContent = n; countLimit.textContent = `${n} / ${FREE_LIMIT} free slots used`; if (countDot) { countDot.className = 'count-dot' + (n >= FREE_LIMIT ? ' danger' : n >= 10 ? ' warn' : ''); } } else { counterBar.classList.remove('visible'); } previewGrid.innerHTML = ''; selectedFiles.forEach(({ url }, i) => { const item = document.createElement('div'); item.className = 'preview-item'; item.innerHTML = ` preview ${i + 1} ${i + 1} `; previewGrid.appendChild(item); }); convertBtn.disabled = n === 0; clearBtn.style.display = n > 0 ? 'block' : 'none'; } async function convertToPDF() { if (selectedFiles.length === 0) { showToast("Please select at least one image."); return; } if (selectedFiles.length > FREE_LIMIT) { showPremiumDialog(); return; } setLoading(true); const formData = new FormData(); selectedFiles.forEach(({ file }) => { formData.append('images', file); }); try { const res = await fetch('/convert', { method: 'POST', body: formData }); const contentType = res.headers.get("content-type") || ""; if (!res.ok) { let errorMessage = "Conversion failed."; if (contentType.includes("application/json")) { const json = await res.json(); if (json.error === "limit_exceeded") { showPremiumDialog(); errorMessage = json.message || "Free limit reached."; } else { errorMessage = json.error || json.message || errorMessage; } } showToast(errorMessage); return; } const blob = await res.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'converted.pdf'; document.body.appendChild(a); a.click(); a.remove(); window.URL.revokeObjectURL(url); showToast('PDF downloaded successfully!'); } catch (err) { console.error(err); showToast('Server error. Please try again.'); } finally { setLoading(false); } } function setLoading(on) { convertBtn.disabled = on; btnLabel.style.display = on ? 'none' : 'inline'; loadingBar.style.display = on ? 'block' : 'none'; } function showPremiumDialog() { overlay.classList.add('show'); } function closeDialog(e) { if (!e || e.target === overlay) { overlay.classList.remove('show'); } } function showToast(msg) { toast.textContent = msg; toast.classList.add('show'); setTimeout(() => toast.classList.remove('show'), 3000); } window.removeFile = removeFile; window.clearAll = clearAll; window.convertToPDF = convertToPDF; window.closeDialog = closeDialog; window.showToast = showToast;