Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Ethical Google Maps Data Extractor</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| .progress-bar { | |
| transition: width 0.3s ease; | |
| } | |
| .fade-in { | |
| animation: fadeIn 0.5s; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; } | |
| to { opacity: 1; } | |
| } | |
| .scrollable-container { | |
| scrollbar-width: thin; | |
| scrollbar-color: #3b82f6 #e5e7eb; | |
| } | |
| .scrollable-container::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| .scrollable-container::-webkit-scrollbar-track { | |
| background: #e5e7eb; | |
| } | |
| .scrollable-container::-webkit-scrollbar-thumb { | |
| background-color: #3b82f6; | |
| border-radius: 3px; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50 min-h-screen"> | |
| <div class="container mx-auto px-4 py-8"> | |
| <!-- Header --> | |
| <header class="mb-8"> | |
| <div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4"> | |
| <div> | |
| <h1 class="text-3xl font-bold text-gray-800">Ethical Maps Extractor</h1> | |
| <p class="text-gray-600">Responsible data extraction from Google Maps</p> | |
| </div> | |
| <div class="flex items-center gap-2 bg-blue-50 px-4 py-2 rounded-lg"> | |
| <i class="fas fa-shield-alt text-blue-500"></i> | |
| <span class="text-sm font-medium text-blue-700">Ethical Mode: Active</span> | |
| </div> | |
| </div> | |
| </header> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> | |
| <!-- Left Panel - Configuration --> | |
| <div class="lg:col-span-1 bg-white rounded-xl shadow-md p-6 h-fit"> | |
| <h2 class="text-xl font-semibold text-gray-800 mb-4">Extraction Settings</h2> | |
| <div class="space-y-5"> | |
| <!-- Search Term --> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Search Term</label> | |
| <div class="relative"> | |
| <input type="text" id="searchTerm" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="e.g. Restaurants in New York"> | |
| <div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none"> | |
| <i class="fas fa-search text-gray-400"></i> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Location --> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Location</label> | |
| <div class="relative"> | |
| <input type="text" id="location" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="City, State or Country"> | |
| <div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none"> | |
| <i class="fas fa-map-marker-alt text-gray-400"></i> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Google Maps API Key --> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Google Maps API Key</label> | |
| <div class="relative"> | |
| <input type="text" id="apiKey" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="Your Google Maps API Key"> | |
| <div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none"> | |
| <i class="fas fa-key text-gray-400"></i> | |
| </div> | |
| </div> | |
| <p class="text-xs text-gray-500 mt-1">Required for Google Maps API access</p> | |
| </div> | |
| <!-- Result Limit --> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Result Limit</label> | |
| <div class="flex items-center gap-2"> | |
| <input type="range" id="resultLimit" min="1" max="1000" value="10" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> | |
| <span id="limitValue" class="text-sm font-medium text-gray-700 w-10 text-center">10</span> | |
| </div> | |
| <p class="text-xs text-gray-500 mt-1">Ethical limit: 1000 results per day</p> | |
| </div> | |
| <!-- Extraction Depth --> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Data Depth</label> | |
| <div class="grid grid-cols-2 gap-2"> | |
| <div> | |
| <input type="checkbox" id="basicInfo" class="hidden peer" checked> | |
| <label for="basicInfo" class="flex items-center justify-between p-2 border border-gray-300 rounded-lg cursor-pointer peer-checked:border-blue-500 peer-checked:bg-blue-50"> | |
| <span class="text-sm text-gray-700">Basic Info</span> | |
| <i class="fas fa-check-circle text-blue-500"></i> | |
| </label> | |
| </div> | |
| <div> | |
| <input type="checkbox" id="contactInfo" class="hidden peer" checked> | |
| <label for="contactInfo" class="flex items-center justify-between p-2 border border-gray-300 rounded-lg cursor-pointer peer-checked:border-blue-500 peer-checked:bg-blue-50"> | |
| <span class="text-sm text-gray-700">Contact Info</span> | |
| <i class="fas fa-check-circle text-blue-500"></i> | |
| </label> | |
| </div> | |
| <div> | |
| <input type="checkbox" id="reviews" class="hidden peer"> | |
| <label for="reviews" class="flex items-center justify-between p-2 border border-gray-300 rounded-lg cursor-pointer peer-checked:border-blue-500 peer-checked:bg-blue-50"> | |
| <span class="text-sm text-gray-700">Reviews</span> | |
| <i class="fas fa-check-circle text-blue-500"></i> | |
| </label> | |
| </div> | |
| <div> | |
| <input type="checkbox" id="hours" class="hidden peer"> | |
| <label for="hours" class="flex items-center justify-between p-2 border border-gray-300 rounded-lg cursor-pointer peer-checked:border-blue-500 peer-checked:bg-blue-50"> | |
| <span class="text-sm text-gray-700">Hours</span> | |
| <i class="fas fa-check-circle text-blue-500"></i> | |
| </label> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Rate Limiting --> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Request Delay</label> | |
| <select id="requestDelay" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> | |
| <option value="5">5 seconds (recommended)</option> | |
| <option value="7">7 seconds (safer)</option> | |
| <option value="10">10 seconds (most reliable)</option> | |
| </select> | |
| </div> | |
| <!-- Start Button --> | |
| <button id="startExtraction" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition duration-200 flex items-center justify-center gap-2"> | |
| <i class="fas fa-play"></i> | |
| Start Extraction | |
| </button> | |
| <!-- Ethical Notice --> | |
| <div class="p-3 bg-yellow-50 border border-yellow-200 rounded-lg"> | |
| <div class="flex items-start gap-2"> | |
| <i class="fas fa-exclamation-triangle text-yellow-500 mt-0.5"></i> | |
| <div> | |
| <p class="text-sm text-yellow-800 font-medium">Ethical Notice</p> | |
| <p class="text-xs text-yellow-700">This tool follows strict ethical guidelines. Usage is limited to 50 results per day to respect Google's services.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Right Panel - Results and Logs --> | |
| <div class="lg:col-span-2 space-y-6"> | |
| <!-- Progress Card --> | |
| <div id="progressCard" class="bg-white rounded-xl shadow-md p-6 hidden"> | |
| <div class="flex justify-between items-center mb-3"> | |
| <h2 class="text-lg font-semibold text-gray-800">Extraction Progress</h2> | |
| <div class="flex items-center gap-2"> | |
| <span id="progressText" class="text-sm font-medium text-gray-700">0%</span> | |
| <button id="pauseExtraction" class="text-gray-500 hover:text-gray-700"> | |
| <i class="fas fa-pause"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="w-full bg-gray-200 rounded-full h-2.5"> | |
| <div id="progressBar" class="progress-bar bg-blue-600 h-2.5 rounded-full" style="width: 0%"></div> | |
| </div> | |
| <div class="mt-4 grid grid-cols-3 gap-4 text-center"> | |
| <div class="bg-blue-50 p-2 rounded-lg"> | |
| <p class="text-xs text-blue-700 font-medium">Extracted</p> | |
| <p id="extractedCount" class="text-lg font-bold text-blue-800">0</p> | |
| </div> | |
| <div class="bg-gray-50 p-2 rounded-lg"> | |
| <p class="text-xs text-gray-700 font-medium">Remaining</p> | |
| <p id="remainingCount" class="text-lg font-bold text-gray-800">0</p> | |
| </div> | |
| <div class="bg-green-50 p-2 rounded-lg"> | |
| <p class="text-xs text-green-700 font-medium">Success Rate</p> | |
| <p id="successRate" class="text-lg font-bold text-green-800">100%</p> | |
| </div> | |
| </div> | |
| <div class="mt-4"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <span class="text-sm font-medium text-gray-700">Estimated Time Remaining</span> | |
| <span id="timeRemaining" class="text-sm font-medium text-gray-700">Calculating...</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Results Card --> | |
| <div class="bg-white rounded-xl shadow-md overflow-hidden"> | |
| <div class="border-b border-gray-200 px-6 py-4"> | |
| <div class="flex justify-between items-center"> | |
| <h2 class="text-lg font-semibold text-gray-800">Extracted Data</h2> | |
| <div class="flex gap-2"> | |
| <button id="exportCSV" class="text-sm bg-green-50 hover:bg-green-100 text-green-700 font-medium py-1 px-3 rounded-lg flex items-center gap-1"> | |
| <i class="fas fa-file-csv"></i> | |
| Export CSV | |
| </button> | |
| <button id="clearResults" class="text-sm bg-gray-50 hover:bg-gray-100 text-gray-700 font-medium py-1 px-3 rounded-lg flex items-center gap-1"> | |
| <i class="fas fa-trash-alt"></i> | |
| Clear | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="scrollable-container max-h-96 overflow-y-auto"> | |
| <table class="min-w-full divide-y divide-gray-200"> | |
| <thead class="bg-gray-50"> | |
| <tr> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Address</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Phone</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Rating</th> | |
| </tr> | |
| </thead> | |
| <tbody id="resultsTable" class="bg-white divide-y divide-gray-200"> | |
| <!-- Results will be inserted here --> | |
| <tr class="text-center py-4"> | |
| <td colspan="4" class="px-6 py-12 text-gray-500"> | |
| <i class="fas fa-database text-4xl text-gray-300 mb-3"></i> | |
| <p>No data extracted yet</p> | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <!-- Logs Card --> | |
| <div class="bg-white rounded-xl shadow-md overflow-hidden"> | |
| <div class="border-b border-gray-200 px-6 py-4"> | |
| <div class="flex justify-between items-center"> | |
| <h2 class="text-lg font-semibold text-gray-800">Extraction Logs</h2> | |
| <div class="flex gap-2"> | |
| <button id="toggleLogs" class="text-sm bg-gray-50 hover:bg-gray-100 text-gray-700 font-medium py-1 px-3 rounded-lg flex items-center gap-1"> | |
| <i class="fas fa-chevron-up"></i> | |
| Collapse | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="logsContainer" class="scrollable-container max-h-60 overflow-y-auto px-6 py-4 space-y-2"> | |
| <div class="text-sm text-gray-500 italic">Waiting for extraction to begin...</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Modal for Ethical Agreement --> | |
| <div id="ethicalModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 hidden"> | |
| <div class="bg-white rounded-xl shadow-xl max-w-md w-full"> | |
| <div class="p-6"> | |
| <div class="flex items-center gap-3 mb-4"> | |
| <i class="fas fa-shield-alt text-2xl text-blue-500"></i> | |
| <h3 class="text-xl font-bold text-gray-800">Ethical Usage Agreement</h3> | |
| </div> | |
| <div class="mb-6 text-sm text-gray-600 space-y-3"> | |
| <p>This tool is designed for ethical data extraction from Google Maps with strict limitations:</p> | |
| <ul class="list-disc pl-5 space-y-1"> | |
| <li>Maximum 50 results per day</li> | |
| <li>Minimum 5-second delay between requests</li> | |
| <li>No bypassing of CAPTCHAs or other security measures</li> | |
| <li>Data must not be used for spamming or harassment</li> | |
| </ul> | |
| <p class="mt-3 font-medium">By using this tool, you agree to these terms and Google's Terms of Service.</p> | |
| </div> | |
| <div class="flex justify-end gap-3"> | |
| <button id="declineAgreement" class="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 font-medium hover:bg-gray-50">Decline</button> | |
| <button id="acceptAgreement" class="px-4 py-2 bg-blue-600 rounded-lg text-white font-medium hover:bg-blue-700">I Accept</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // DOM Elements | |
| const searchTermInput = document.getElementById('searchTerm'); | |
| const locationInput = document.getElementById('location'); | |
| const resultLimitInput = document.getElementById('resultLimit'); | |
| const limitValue = document.getElementById('limitValue'); | |
| const startExtractionBtn = document.getElementById('startExtraction'); | |
| const progressCard = document.getElementById('progressCard'); | |
| const progressBar = document.getElementById('progressBar'); | |
| const progressText = document.getElementById('progressText'); | |
| const extractedCount = document.getElementById('extractedCount'); | |
| const remainingCount = document.getElementById('remainingCount'); | |
| const successRate = document.getElementById('successRate'); | |
| const timeRemaining = document.getElementById('timeRemaining'); | |
| const resultsTable = document.getElementById('resultsTable'); | |
| const logsContainer = document.getElementById('logsContainer'); | |
| const exportCSVBtn = document.getElementById('exportCSV'); | |
| const clearResultsBtn = document.getElementById('clearResults'); | |
| const toggleLogsBtn = document.getElementById('toggleLogs'); | |
| const logsContainerDiv = document.getElementById('logsContainer'); | |
| const ethicalModal = document.getElementById('ethicalModal'); | |
| const acceptAgreementBtn = document.getElementById('acceptAgreement'); | |
| const declineAgreementBtn = document.getElementById('declineAgreement'); | |
| const pauseExtractionBtn = document.getElementById('pauseExtraction'); | |
| // Variables | |
| let isExtracting = false; | |
| let isPaused = false; | |
| let extractedItems = 0; | |
| let totalItems = 0; | |
| let successfulExtractions = 0; | |
| let extractionStartTime = null; | |
| let extractionInterval = null; | |
| // Event Listeners | |
| resultLimitInput.addEventListener('input', updateLimitValue); | |
| startExtractionBtn.addEventListener('click', startExtraction); | |
| exportCSVBtn.addEventListener('click', exportToCSV); | |
| clearResultsBtn.addEventListener('click', clearResults); | |
| toggleLogsBtn.addEventListener('click', toggleLogs); | |
| acceptAgreementBtn.addEventListener('click', acceptAgreement); | |
| declineAgreementBtn.addEventListener('click', declineAgreement); | |
| pauseExtractionBtn.addEventListener('click', togglePause); | |
| // Functions | |
| function updateLimitValue() { | |
| limitValue.textContent = resultLimitInput.value; | |
| } | |
| function startExtraction() { | |
| const apiKey = document.getElementById('apiKey').value.trim(); | |
| const searchTerm = searchTermInput.value.trim(); | |
| const location = locationInput.value.trim(); | |
| const limit = parseInt(resultLimitInput.value); | |
| if (!apiKey) { | |
| addLog("AIzaSyDQAh_kANmTKL5pm9q4MDFfe4cKvEGffrM", "error"); | |
| document.getElementById('AIzaSyDQAh_kANmTKL5pm9q4MDFfe4cKvEGffrM').focus(); | |
| return; | |
| } | |
| if (!searchTerm) { | |
| addLog("Please enter a search term", "error"); | |
| searchTermInput.focus(); | |
| return; | |
| } | |
| if (limit > 1000) { | |
| addLog("Ethical limit is 1000 results per day", "error"); | |
| return; | |
| } | |
| // Show ethical agreement modal | |
| ethicalModal.classList.remove('hidden'); | |
| } | |
| function acceptAgreement() { | |
| ethicalModal.classList.add('hidden'); | |
| beginExtraction(); | |
| } | |
| function declineAgreement() { | |
| ethicalModal.classList.add('hidden'); | |
| addLog("Extraction canceled - you must accept the ethical agreement", "error"); | |
| } | |
| function beginExtraction() { | |
| const searchTerm = searchTermInput.value.trim(); | |
| const location = locationInput.value.trim(); | |
| const limit = parseInt(resultLimitInput.value); | |
| const delay = parseInt(document.getElementById('requestDelay').value); | |
| // Reset variables | |
| extractedItems = 0; | |
| totalItems = limit; | |
| successfulExtractions = 0; | |
| extractionStartTime = new Date(); | |
| isExtracting = true; | |
| isPaused = false; | |
| // Update UI | |
| startExtractionBtn.disabled = true; | |
| startExtractionBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Extracting...'; | |
| progressCard.classList.remove('hidden'); | |
| updateProgress(); | |
| clearResults(); | |
| addLog(`Starting extraction for "${searchTerm}" in "${location || 'any location'}"`, "info"); | |
| addLog(`Configured with ${delay} second delay between requests`, "info"); | |
| // Simulate extraction process | |
| extractionInterval = setInterval(() => { | |
| if (!isPaused) { | |
| simulateExtractionStep(); | |
| } | |
| }, delay * 1000); | |
| } | |
| function simulateExtractionStep() { | |
| if (extractedItems >= totalItems) { | |
| finishExtraction(); | |
| return; | |
| } | |
| extractedItems++; | |
| const isSuccess = Math.random() > 0.1; // 90% success rate simulation | |
| if (isSuccess) successfulExtractions++; | |
| // Update progress | |
| updateProgress(); | |
| // Add log entry | |
| const logMessage = isSuccess | |
| ? `Successfully extracted data for business #${extractedItems}` | |
| : `Failed to extract data for business #${extractedItems} (retrying...)`; | |
| addLog(logMessage, isSuccess ? "success" : "warning"); | |
| // Randomly simulate a CAPTCHA | |
| if (Math.random() < 0.05) { | |
| addLog("CAPTCHA detected - extraction paused for 2 minutes", "error"); | |
| isPaused = true; | |
| setTimeout(() => { | |
| isPaused = false; | |
| addLog("CAPTCHA resolved - resuming extraction", "success"); | |
| }, 120000); | |
| } | |
| } | |
| function updateProgress() { | |
| const progress = Math.round((extractedItems / totalItems) * 100); | |
| progressBar.style.width = `${progress}%`; | |
| progressText.textContent = `${progress}%`; | |
| extractedCount.textContent = extractedItems; | |
| remainingCount.textContent = totalItems - extractedItems; | |
| const successPercentage = extractedItems > 0 | |
| ? Math.round((successfulExtractions / extractedItems) * 100) | |
| : 100; | |
| successRate.textContent = `${successPercentage}%`; | |
| // Update time remaining | |
| if (extractedItems > 0) { | |
| const timeElapsed = (new Date() - extractionStartTime) / 1000; // in seconds | |
| const timePerItem = timeElapsed / extractedItems; | |
| const remainingTime = Math.round((totalItems - extractedItems) * timePerItem); | |
| if (remainingTime < 60) { | |
| timeRemaining.textContent = `${remainingTime} seconds`; | |
| } else { | |
| const minutes = Math.floor(remainingTime / 60); | |
| const seconds = remainingTime % 60; | |
| timeRemaining.textContent = `${minutes}m ${seconds}s`; | |
| } | |
| } | |
| } | |
| function finishExtraction() { | |
| clearInterval(extractionInterval); | |
| isExtracting = false; | |
| // Update UI | |
| startExtractionBtn.disabled = false; | |
| startExtractionBtn.innerHTML = '<i class="fas fa-play"></i> Start Extraction'; | |
| addLog(`Extraction completed - ${successfulExtractions} of ${totalItems} items successfully extracted`, "success"); | |
| // Show completion message | |
| setTimeout(() => { | |
| addLog("Remember: Daily limit resets in 24 hours", "info"); | |
| }, 1000); | |
| } | |
| function addLog(message, type) { | |
| const now = new Date(); | |
| const timeString = now.toLocaleTimeString(); | |
| const colors = { | |
| info: 'text-blue-600', | |
| success: 'text-green-600', | |
| warning: 'text-yellow-600', | |
| error: 'text-red-600' | |
| }; | |
| const logEntry = document.createElement('div'); | |
| logEntry.className = `text-sm ${colors[type] || 'text-gray-600'} fade-in`; | |
| logEntry.innerHTML = `<span class="font-medium">[${timeString}]</span> ${message}`; | |
| logsContainer.insertBefore(logEntry, logsContainer.firstChild); | |
| // Auto-scroll if not paused | |
| if (!isPaused) { | |
| logsContainer.scrollTop = 0; | |
| } | |
| } | |
| function addResultToTable(data) { | |
| // Remove the placeholder if it exists | |
| if (resultsTable.querySelector('.text-center')) { | |
| resultsTable.innerHTML = ''; | |
| } | |
| const row = document.createElement('tr'); | |
| row.className = 'fade-in'; | |
| row.innerHTML = ` | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <div class="font-medium text-gray-900">${data.name}</div> | |
| <div class="text-xs text-gray-500">${data.website}</div> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${data.address}</td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${data.phone}</td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <div class="flex items-center"> | |
| <span class="bg-blue-100 text-blue-800 text-xs font-semibold px-2 py-0.5 rounded">${data.rating}</span> | |
| <span class="ml-2 text-xs text-gray-500">(${data.reviews} reviews)</span> | |
| </div> | |
| </td> | |
| `; | |
| resultsTable.appendChild(row); | |
| } | |
| function exportToCSV() { | |
| if (resultsTable.querySelector('.text-center')) { | |
| addLog("No data to export", "warning"); | |
| return; | |
| } | |
| addLog("Preparing CSV export...", "info"); | |
| // In a real implementation, this would generate a proper CSV file | |
| setTimeout(() => { | |
| addLog("CSV file downloaded successfully", "success"); | |
| }, 1500); | |
| } | |
| function clearResults() { | |
| resultsTable.innerHTML = ` | |
| <tr class="text-center py-4"> | |
| <td colspan="4" class="px-6 py-12 text-gray-500"> | |
| <i class="fas fa-database text-4xl text-gray-300 mb-3"></i> | |
| <p>No data extracted yet</p> | |
| </td> | |
| </tr> | |
| `; | |
| addLog("Results cleared", "info"); | |
| } | |
| function toggleLogs() { | |
| const isCollapsed = logsContainerDiv.classList.contains('hidden'); | |
| if (isCollapsed) { | |
| logsContainerDiv.classList.remove('hidden'); | |
| toggleLogsBtn.innerHTML = '<i class="fas fa-chevron-up"></i> Collapse'; | |
| } else { | |
| logsContainerDiv.classList.add('hidden'); | |
| toggleLogsBtn.innerHTML = '<i class="fas fa-chevron-down"></i> Expand'; | |
| } | |
| } | |
| function togglePause() { | |
| isPaused = !isPaused; | |
| if (isPaused) { | |
| pauseExtractionBtn.innerHTML = '<i class="fas fa-play"></i>'; | |
| addLog("Extraction paused", "warning"); | |
| } else { | |
| pauseExtractionBtn.innerHTML = '<i class="fas fa-pause"></i>'; | |
| addLog("Extraction resumed", "success"); | |
| } | |
| } | |
| // Initialize | |
| updateLimitValue(); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Bencolliss/mapsscrapper" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |