Spaces:
Runtime error
Runtime error
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>InstaSpy - Bulk Scraper SaaS</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/lucide@latest"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <script> | |
| tailwind.config = { | |
| theme: { | |
| extend: { | |
| fontFamily: { sans: ['Inter', 'sans-serif'] }, | |
| animation: { 'fade-in': 'fadeIn 0.5s ease-out forwards' }, | |
| keyframes: { | |
| fadeIn: { | |
| '0%': { opacity: '0', transform: 'translateY(10px)' }, | |
| '100%': { opacity: '1', transform: 'translateY(0)' }, | |
| } | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| body { background-color: #020617; color: #e2e8f0; } | |
| .glass-panel { | |
| background: rgba(15, 23, 42, 0.6); | |
| backdrop-filter: blur(12px); | |
| border: 1px solid rgba(148, 163, 184, 0.1); | |
| } | |
| .gradient-text { | |
| background: linear-gradient(135deg, #f472b6 0%, #a855f7 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| </style> | |
| </head> | |
| <body class="min-h-screen flex flex-col font-sans selection:bg-pink-500 selection:text-white"> | |
| <nav class="border-b border-slate-800 bg-slate-950/80 sticky top-0 z-50 backdrop-blur-md"> | |
| <div class="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between"> | |
| <div class="flex items-center gap-2"> | |
| <div class="bg-gradient-to-br from-pink-500 to-purple-600 p-1.5 rounded-lg shadow-lg shadow-purple-500/20"> | |
| <i data-lucide="instagram" class="w-5 h-5 text-white"></i> | |
| </div> | |
| <span class="font-bold text-xl tracking-tight text-white">InstaSpy<span class="text-pink-500">.io</span></span> | |
| </div> | |
| </div> | |
| </nav> | |
| <main class="flex-grow max-w-7xl mx-auto px-6 py-12 w-full space-y-12"> | |
| <div class="text-center space-y-4 animate-fade-in"> | |
| <h1 class="text-4xl md:text-5xl font-extrabold tracking-tight text-white"> | |
| Instagram <span class="gradient-text">Bulk Data Scraper</span> | |
| </h1> | |
| <p class="text-slate-400 max-w-2xl mx-auto text-lg leading-relaxed"> | |
| Extract <span class="text-white font-medium">Views, Likes, and Followers</span> from Reels, Posts, and Profiles instantly. | |
| </p> | |
| </div> | |
| <div class="glass-panel rounded-2xl p-1.5 shadow-2xl shadow-purple-900/10 animate-fade-in" style="animation-delay: 0.1s;"> | |
| <div class="bg-[#0f172a] rounded-xl p-4"> | |
| <div class="flex justify-between mb-2"> | |
| <label class="text-xs font-semibold uppercase tracking-wider text-slate-500">Target URLs</label> | |
| <button class="text-xs text-pink-400 hover:text-pink-300 transition" onclick="pasteExample()">Paste Example Links</button> | |
| </div> | |
| <textarea id="urlInput" placeholder="Paste Instagram links here (separated by commas or new lines)..." class="w-full h-48 bg-transparent text-slate-300 placeholder:text-slate-600 focus:outline-none resize-none font-mono text-sm leading-6 custom-scrollbar"></textarea> | |
| </div> | |
| <div class="flex justify-between items-center px-4 py-3 bg-slate-900/50 rounded-b-xl border-t border-slate-800"> | |
| <span id="linkCount" class="text-xs text-slate-500 font-mono">0 Links detected</span> | |
| <button id="scrapeBtn" onclick="startScrape()" class="flex items-center gap-2 px-6 py-2.5 rounded-lg font-semibold text-sm transition-all bg-gradient-to-r from-pink-600 to-purple-600 hover:from-pink-500 hover:to-purple-500 text-white shadow-lg shadow-pink-900/20 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"> | |
| <i data-lucide="play" class="w-4 h-4 fill-current"></i> Start Extraction | |
| </button> | |
| </div> | |
| </div> | |
| <div id="resultsArea" class="hidden space-y-6 animate-fade-in"> | |
| <div class="border border-slate-800 rounded-xl overflow-hidden bg-slate-900/40 backdrop-blur-sm"> | |
| <div class="flex items-center justify-between p-4 border-b border-slate-800 bg-slate-900/60"> | |
| <h3 class="font-semibold text-white flex items-center gap-2 text-sm"> | |
| <i data-lucide="database" class="w-4 h-4 text-slate-400"></i> Extracted Data | |
| </h3> | |
| <button onclick="downloadCSV()" class="flex items-center gap-2 px-3 py-1.5 bg-slate-800 hover:bg-slate-700 hover:text-white text-xs font-medium rounded-lg text-slate-300 transition border border-slate-700"> | |
| <i data-lucide="download" class="w-3.5 h-3.5"></i> Export Excel | |
| </button> | |
| </div> | |
| <div class="overflow-x-auto"> | |
| <table class="w-full text-sm text-left"> | |
| <thead class="text-xs text-slate-400 uppercase bg-slate-900/80 border-b border-slate-800"> | |
| <tr> | |
| <th class="px-6 py-3 font-medium">Type</th> | |
| <th class="px-6 py-3 font-medium">Source URL</th> | |
| <th class="px-6 py-3 font-medium">Author</th> | |
| <th class="px-6 py-3 font-medium text-right">Followers</th> | |
| <th class="px-6 py-3 font-medium text-right">Likes</th> | |
| <th class="px-6 py-3 font-medium text-right">Views</th> | |
| <th class="px-6 py-3 font-medium text-center">Status</th> | |
| </tr> | |
| </thead> | |
| <tbody id="tableBody" class="divide-y divide-slate-800/50"></tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <script> | |
| lucide.createIcons(); | |
| const urlInput = document.getElementById('urlInput'); | |
| const scrapeBtn = document.getElementById('scrapeBtn'); | |
| const resultsArea = document.getElementById('resultsArea'); | |
| const tableBody = document.getElementById('tableBody'); | |
| urlInput.addEventListener('input', () => { | |
| const lines = urlInput.value.split('\n').filter(line => line.trim() !== ''); | |
| document.getElementById('linkCount').innerText = `${lines.length} Links detected`; | |
| }); | |
| function pasteExample() { | |
| urlInput.value = `https://www.instagram.com/reel/DTQEBbwEs_K/\nhttps://www.instagram.com/devs_clicks12/\nhttps://www.instagram.com/reel/DTSetANkglg/`; | |
| urlInput.dispatchEvent(new Event('input')); | |
| } | |
| async function startScrape() { | |
| const rawInput = urlInput.value.trim(); | |
| // Split by comma OR newline, then filter empty strings | |
| const urls = rawInput.split(/[\n,]+/).map(u => u.trim()).filter(u => u !== ''); | |
| if (urls.length === 0) { alert("Please enter at least one URL"); return; } | |
| scrapeBtn.disabled = true; | |
| scrapeBtn.innerHTML = `<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i> Processing...`; | |
| lucide.createIcons(); | |
| try { | |
| // CALL PYTHON BACKEND | |
| const response = await fetch('/api/scrape', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ urls: urls }) | |
| }); | |
| const data = await response.json(); | |
| renderTable(data); | |
| resultsArea.classList.remove('hidden'); | |
| resultsArea.scrollIntoView({ behavior: 'smooth' }); | |
| } catch (error) { | |
| alert("Error connecting to server. Is app.py running?"); | |
| console.error(error); | |
| } | |
| scrapeBtn.disabled = false; | |
| scrapeBtn.innerHTML = `<i data-lucide="play" class="w-4 h-4 fill-current"></i> Start Extraction`; | |
| lucide.createIcons(); | |
| } | |
| function renderTable(data) { | |
| tableBody.innerHTML = ''; | |
| data.forEach(row => { | |
| const tr = document.createElement('tr'); | |
| tr.className = "hover:bg-slate-800/40 transition-colors group"; | |
| const typeColor = row.type === 'REEL' ? 'text-pink-400 bg-pink-400/10 border-pink-400/20' | |
| : row.type === 'PROFILE' ? 'text-purple-400 bg-purple-400/10 border-purple-400/20' | |
| : 'text-blue-400 bg-blue-400/10 border-blue-400/20'; | |
| const statusColor = row.status === 'Success' ? 'text-emerald-400 bg-emerald-400/10' : 'text-amber-400 bg-amber-400/10'; | |
| // Truncate URL for display | |
| const shortUrl = row.url.length > 30 ? row.url.substring(0, 30) + "..." : row.url; | |
| tr.innerHTML = ` | |
| <td class="px-6 py-4"><span class="px-2 py-1 rounded text-[10px] font-bold border uppercase ${typeColor}">${row.type}</span></td> | |
| <td class="px-6 py-4"> | |
| <a href="${row.url}" target="_blank" class="text-xs text-slate-500 hover:text-pink-400 hover:underline transition font-mono flex items-center gap-1" title="${row.url}"> | |
| ${shortUrl} | |
| <i data-lucide="external-link" class="w-3 h-3"></i> | |
| </a> | |
| </td> | |
| <td class="px-6 py-4 font-medium text-white">@${row.author || 'N/A'}</td> | |
| <td class="px-6 py-4 text-right text-slate-300 font-mono">${row.followers}</td> | |
| <td class="px-6 py-4 text-right text-slate-300 font-mono">${row.likes}</td> | |
| <td class="px-6 py-4 text-right text-white font-bold font-mono">${row.views}</td> | |
| <td class="px-6 py-4 text-center"> | |
| <span class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium ${statusColor}"> | |
| <span class="w-1.5 h-1.5 rounded-full ${row.status === 'Success' ? 'bg-emerald-400' : 'bg-amber-400'}"></span> | |
| ${row.status} | |
| </span> | |
| </td> | |
| `; | |
| tableBody.appendChild(tr); | |
| }); | |
| lucide.createIcons(); | |
| } | |
| function downloadCSV() { | |
| const rows = Array.from(document.querySelectorAll("table tr")); | |
| const csvContent = rows.map(row => { | |
| const cells = Array.from(row.querySelectorAll("th, td")); | |
| return cells.map(cell => { | |
| // Check if cell has a link (for the URL column) | |
| const link = cell.querySelector("a"); | |
| const text = link ? link.getAttribute("href") : cell.innerText; | |
| return `"${text.replace(/(\r\n|\n|\r)/gm, " ").trim()}"`; | |
| }).join(","); | |
| }).join("\n"); | |
| const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); | |
| const url = URL.createObjectURL(blob); | |
| const link = document.createElement("a"); | |
| link.href = url; | |
| link.download = "instagram_export.csv"; | |
| link.click(); | |
| } | |
| </script> | |
| </body> | |
| </html> |