Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Multi-Platform Product Scraper</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| } | |
| header { | |
| text-align: center; | |
| color: white; | |
| margin-bottom: 30px; | |
| } | |
| h1 { | |
| font-size: 2.5em; | |
| margin-bottom: 10px; | |
| text-shadow: 2px 2px 4px rgba(0,0,0,0.3); | |
| } | |
| .search-section { | |
| background: white; | |
| padding: 30px; | |
| border-radius: 10px; | |
| box-shadow: 0 8px 32px rgba(0,0,0,0.1); | |
| margin-bottom: 30px; | |
| } | |
| .input-group { | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 20px; | |
| flex-wrap: wrap; | |
| align-items: center; | |
| } | |
| input[type="text"], | |
| input[type="number"], | |
| select { | |
| padding: 12px 15px; | |
| border: 2px solid #e0e0e0; | |
| border-radius: 5px; | |
| font-size: 1em; | |
| } | |
| input[type="text"]:focus, | |
| input[type="number"]:focus, | |
| select:focus { | |
| outline: none; | |
| border-color: #667eea; | |
| box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); | |
| } | |
| input[type="text"] { | |
| flex: 1; | |
| min-width: 250px; | |
| } | |
| input[type="number"] { | |
| width: 120px; | |
| } | |
| button { | |
| padding: 12px 30px; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| border: none; | |
| border-radius: 5px; | |
| font-size: 1em; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| } | |
| button:hover:not(:disabled) { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4); | |
| } | |
| button:disabled { | |
| opacity: 0.6; | |
| cursor: not-allowed; | |
| } | |
| .platform-selector { | |
| display: flex; | |
| gap: 15px; | |
| margin-top: 20px; | |
| flex-wrap: wrap; | |
| } | |
| .platform-checkbox { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 10px 15px; | |
| background: #f5f5f5; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| } | |
| .platform-checkbox input { | |
| cursor: pointer; | |
| width: 18px; | |
| height: 18px; | |
| accent-color: #667eea; | |
| } | |
| .loading { | |
| text-align: center; | |
| padding: 40px; | |
| background: white; | |
| border-radius: 10px; | |
| box-shadow: 0 8px 32px rgba(0,0,0,0.1); | |
| display: none; | |
| margin-top: 30px; | |
| } | |
| .loading.active { | |
| display: block; | |
| } | |
| .spinner { | |
| border: 4px solid #f3f3f3; | |
| border-top: 4px solid #667eea; | |
| border-radius: 50%; | |
| width: 50px; | |
| height: 50px; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 20px; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .alert { | |
| padding: 15px; | |
| border-radius: 5px; | |
| margin-top: 20px; | |
| display: none; | |
| border-left: 4px solid; | |
| } | |
| .alert.active { | |
| display: block; | |
| } | |
| .alert.error { | |
| background: #fee; | |
| color: #c00; | |
| border-color: #c00; | |
| } | |
| .alert.success { | |
| background: #d4edda; | |
| color: #155724; | |
| border-color: #c3e6cb; | |
| } | |
| .no-results { | |
| text-align: center; | |
| padding: 40px; | |
| background: white; | |
| border-radius: 10px; | |
| color: #999; | |
| font-size: 1.1em; | |
| margin-top: 30px; | |
| display: none; | |
| } | |
| .no-results.active { | |
| display: block; | |
| } | |
| .stats { | |
| background: white; | |
| padding: 20px; | |
| border-radius: 10px; | |
| margin-top: 20px; | |
| display: none; | |
| gap: 30px; | |
| flex-wrap: wrap; | |
| } | |
| .stats.active { | |
| display: flex; | |
| } | |
| .stat { | |
| flex: 1; | |
| min-width: 150px; | |
| } | |
| .stat-value { | |
| font-size: 2em; | |
| font-weight: bold; | |
| color: #667eea; | |
| } | |
| .stat-label { | |
| color: #999; | |
| font-size: 0.9em; | |
| margin-top: 5px; | |
| } | |
| .results { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); | |
| gap: 20px; | |
| margin-top: 30px; | |
| } | |
| .result-card { | |
| background: white; | |
| border-radius: 10px; | |
| overflow: hidden; | |
| box-shadow: 0 8px 32px rgba(0,0,0,0.1); | |
| transition: all 0.3s; | |
| } | |
| .result-card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 12px 40px rgba(0,0,0,0.15); | |
| } | |
| .card-header { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 15px; | |
| font-weight: 600; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .card-title { | |
| flex: 1; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| font-size: 0.95em; | |
| } | |
| .platform-badge { | |
| background: rgba(255,255,255,0.3); | |
| padding: 4px 12px; | |
| border-radius: 15px; | |
| font-size: 0.75em; | |
| white-space: nowrap; | |
| } | |
| .card-image { | |
| width: 100%; | |
| height: 200px; | |
| object-fit: cover; | |
| background: #f0f0f0; | |
| display: block; | |
| } | |
| .card-content { | |
| padding: 20px; | |
| } | |
| .product-info { | |
| margin-bottom: 12px; | |
| } | |
| .label { | |
| font-weight: 600; | |
| color: #333; | |
| margin-bottom: 5px; | |
| font-size: 0.85em; | |
| } | |
| .value { | |
| color: #666; | |
| word-break: break-word; | |
| font-size: 0.9em; | |
| line-height: 1.4; | |
| } | |
| .price { | |
| font-size: 1.8em; | |
| color: #28a745; | |
| font-weight: bold; | |
| margin: 10px 0; | |
| } | |
| .link { | |
| display: inline-block; | |
| color: white; | |
| text-decoration: none; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| padding: 8px 15px; | |
| border-radius: 4px; | |
| font-weight: 600; | |
| margin-top: 10px; | |
| font-size: 0.9em; | |
| } | |
| .link:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); | |
| } | |
| @media (max-width: 768px) { | |
| .input-group { | |
| flex-direction: column; | |
| } | |
| input[type="text"] { | |
| min-width: auto; | |
| } | |
| input[type="number"] { | |
| width: 100%; | |
| } | |
| .results { | |
| grid-template-columns: 1fr; | |
| } | |
| h1 { | |
| font-size: 1.8em; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <h1>π Multi-Platform Product Scraper</h1> | |
| <p>Search CEX, MediaMarkt, and Backmarket</p> | |
| </header> | |
| <div class="search-section"> | |
| <div class="input-group"> | |
| <input | |
| type="text" | |
| id="apiKeyInput" | |
| placeholder="Enter your Apify API Key (apify_api_...)" | |
| autocomplete="off" | |
| style="flex: 1;" | |
| > | |
| <button onclick="saveApiKey()" style="min-width: 150px;">πΎ Save Key</button> | |
| </div> | |
| <div class="input-group" style="margin-top: 15px;"> | |
| <input | |
| type="text" | |
| id="searchInput" | |
| placeholder="Enter product name (e.g., iPhone 13)" | |
| autocomplete="off" | |
| > | |
| <input | |
| type="number" | |
| id="maxItems" | |
| value="5" | |
| min="1" | |
| max="50" | |
| > | |
| <button id="searchBtn" onclick="searchProducts()">π Search</button> | |
| </div> | |
| <div class="platform-selector"> | |
| <label class="platform-checkbox"> | |
| <input type="checkbox" id="cex" checked> π³ CEX | |
| </label> | |
| <label class="platform-checkbox"> | |
| <input type="checkbox" id="mediamarkt" checked> π MediaMarkt | |
| </label> | |
| <label class="platform-checkbox"> | |
| <input type="checkbox" id="backmarket" checked> β»οΈ Backmarket | |
| </label> | |
| </div> | |
| </div> | |
| <div class="alert error" id="errorAlert"> | |
| <span id="errorMessage"></span> | |
| </div> | |
| <div class="alert success" id="successAlert"> | |
| <span id="successMessage"></span> | |
| </div> | |
| <div class="loading" id="loading"> | |
| <div class="spinner"></div> | |
| <div id="loadingText">Searching platforms...</div> | |
| </div> | |
| <div id="stats" class="stats"> | |
| <div class="stat"> | |
| <div class="stat-value" id="totalResults">0</div> | |
| <div class="stat-label">Total Results</div> | |
| </div> | |
| <div class="stat"> | |
| <div class="stat-value" id="platformsUsed">0</div> | |
| <div class="stat-label">Platforms</div> | |
| </div> | |
| <div class="stat"> | |
| <div class="stat-value" id="searchTime">0s</div> | |
| <div class="stat-label">Time</div> | |
| </div> | |
| </div> | |
| <div class="no-results" id="noResults"> | |
| <p>π No products found. Try different keywords.</p> | |
| </div> | |
| <div class="results" id="results"></div> | |
| </div> | |
| <script> | |
| // ============================================ | |
| // GLOBAL STATE | |
| // ============================================ | |
| let API_KEY = localStorage.getItem('apifyApiKey') || ''; | |
| const ACTORS = { | |
| cex: { | |
| id: 'sync-network~cex-product-scraper-uk-webuy-com', | |
| endpoint: '/run-sync-get-dataset-items' | |
| }, | |
| mediamarkt: { | |
| id: 'scrapegoats~MediaMarkt-Scraper', | |
| endpoint: '/run-sync-get-dataset-items' | |
| }, | |
| backmarket: { | |
| id: 'scrapegoats~Backmarket-Scraper', | |
| endpoint: '/run-sync-get-dataset-items' | |
| } | |
| }; | |
| const REQUEST_TIMEOUT = 300000; | |
| // ============================================ | |
| // INITIALIZATION | |
| // ============================================ | |
| window.addEventListener('load', function() { | |
| if (API_KEY) { | |
| document.getElementById('apiKeyInput').value = 'ββββββββ' + API_KEY.slice(-4); | |
| } | |
| }); | |
| // ============================================ | |
| // API KEY | |
| // ============================================ | |
| function saveApiKey() { | |
| let keyInput = document.getElementById('apiKeyInput').value.trim(); | |
| if (!keyInput || keyInput.startsWith('β')) { | |
| showAlert('error', 'β Please enter a valid API key'); | |
| return; | |
| } | |
| if (!keyInput.startsWith('apify_api_')) { | |
| showAlert('error', 'β Must start with "apify_api_"'); | |
| return; | |
| } | |
| API_KEY = keyInput; | |
| localStorage.setItem('apifyApiKey', API_KEY); | |
| document.getElementById('apiKeyInput').value = 'ββββββββ' + API_KEY.slice(-4); | |
| showAlert('success', 'β API key saved'); | |
| } | |
| // ============================================ | |
| // ALERTS | |
| // ============================================ | |
| function showAlert(type, message) { | |
| const alertId = type + 'Alert'; | |
| const messageId = type + 'Message'; | |
| document.getElementById(messageId).textContent = message; | |
| document.getElementById(alertId).classList.add('active'); | |
| setTimeout(() => { | |
| document.getElementById(alertId).classList.remove('active'); | |
| }, 5000); | |
| } | |
| // ============================================ | |
| // UTILITIES | |
| // ============================================ | |
| function escapeHtml(text) { | |
| if (!text) return ''; | |
| const div = document.createElement('div'); | |
| div.textContent = String(text); | |
| return div.innerHTML; | |
| } | |
| function setLoading(active) { | |
| document.getElementById('loading').classList.toggle('active', active); | |
| } | |
| // ============================================ | |
| // PRICE PARSING - CRITICAL FIX | |
| // ============================================ | |
| function extractPrice(item) { | |
| // CEX specific | |
| if (item.buy_price !== undefined) { | |
| const price = parseFloat(String(item.buy_price).replace(/[^0-9.]/g, '')); | |
| if (!isNaN(price)) return price; | |
| } | |
| // Backmarket specific | |
| if (item.price !== undefined) { | |
| if (typeof item.price === 'number') return item.price; | |
| const price = parseFloat(String(item.price).replace(/[^0-9.]/g, '')); | |
| if (!isNaN(price)) return price; | |
| } | |
| // MediaMarkt specific | |
| if (item.priceValue !== undefined) { | |
| const price = parseFloat(String(item.priceValue).replace(/[^0-9.]/g, '')); | |
| if (!isNaN(price)) return price; | |
| } | |
| // Generic fallbacks | |
| if (item.price_value !== undefined) { | |
| const price = parseFloat(String(item.price_value).replace(/[^0-9.]/g, '')); | |
| if (!isNaN(price)) return price; | |
| } | |
| if (item.listing_price !== undefined) { | |
| const price = parseFloat(String(item.listing_price).replace(/[^0-9.]/g, '')); | |
| if (!isNaN(price)) return price; | |
| } | |
| if (item.salePrice !== undefined) { | |
| const price = parseFloat(String(item.salePrice).replace(/[^0-9.]/g, '')); | |
| if (!isNaN(price)) return price; | |
| } | |
| return null; | |
| } | |
| function formatPrice(price) { | |
| if (price === null || price === undefined) return 'N/A'; | |
| if (typeof price === 'number') { | |
| return '$' + price.toFixed(2); | |
| } | |
| return String(price); | |
| } | |
| // ============================================ | |
| // MAIN SEARCH | |
| // ============================================ | |
| async function searchProducts() { | |
| if (!API_KEY) { | |
| showAlert('error', 'β Save your API key first'); | |
| return; | |
| } | |
| const searchInput = document.getElementById('searchInput').value.trim(); | |
| if (!searchInput) { | |
| showAlert('error', 'β Enter a product name'); | |
| return; | |
| } | |
| const platforms = []; | |
| if (document.getElementById('cex').checked) platforms.push('cex'); | |
| if (document.getElementById('mediamarkt').checked) platforms.push('mediamarkt'); | |
| if (document.getElementById('backmarket').checked) platforms.push('backmarket'); | |
| if (platforms.length === 0) { | |
| showAlert('error', 'β Select at least one platform'); | |
| return; | |
| } | |
| document.getElementById('searchBtn').disabled = true; | |
| document.getElementById('results').innerHTML = ''; | |
| document.getElementById('noResults').classList.remove('active'); | |
| document.getElementById('stats').classList.remove('active'); | |
| setLoading(true); | |
| const startTime = Date.now(); | |
| let allResults = []; | |
| let successCount = 0; | |
| try { | |
| const promises = platforms.map(p => searchPlatform(p, searchInput)); | |
| const results = await Promise.allSettled(promises); | |
| results.forEach((result, idx) => { | |
| if (result.status === 'fulfilled' && result.value && result.value.length > 0) { | |
| allResults = allResults.concat(result.value); | |
| successCount++; | |
| } | |
| }); | |
| const timeElapsed = ((Date.now() - startTime) / 1000).toFixed(2); | |
| if (allResults.length === 0) { | |
| document.getElementById('noResults').classList.add('active'); | |
| } else { | |
| displayResults(allResults, successCount, timeElapsed); | |
| showAlert('success', `β Found ${allResults.length} products`); | |
| } | |
| } catch (error) { | |
| showAlert('error', 'β Error: ' + error.message); | |
| } finally { | |
| setLoading(false); | |
| document.getElementById('searchBtn').disabled = false; | |
| } | |
| } | |
| // ============================================ | |
| // PLATFORM SEARCH | |
| // ============================================ | |
| async function searchPlatform(platform, query) { | |
| const actor = ACTORS[platform]; | |
| const url = `https://api.apify.com/v2/acts/${actor.id}${actor.endpoint}?token=${API_KEY}`; | |
| let payload = {}; | |
| if (platform === 'cex') { | |
| payload = { | |
| search_input: query, | |
| max_items: parseInt(document.getElementById('maxItems').value) || 5, | |
| include_images: true, | |
| include_trade_values: true, | |
| throttle: 2 | |
| }; | |
| } else if (platform === 'backmarket') { | |
| payload = { | |
| function: 'country_and_keywords', | |
| searchs: [query], | |
| country_input: 'United States', | |
| limit_per_search: parseInt(document.getElementById('maxItems').value) || 5 | |
| }; | |
| } else if (platform === 'mediamarkt') { | |
| payload = { | |
| search_input: query, | |
| max_items: parseInt(document.getElementById('maxItems').value) || 5 | |
| }; | |
| } | |
| try { | |
| const controller = new AbortController(); | |
| const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT); | |
| const response = await fetch(url, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload), | |
| signal: controller.signal | |
| }); | |
| clearTimeout(timeout); | |
| if (!response.ok) { | |
| throw new Error(`HTTP ${response.status}`); | |
| } | |
| const items = await response.json(); | |
| if (!Array.isArray(items)) { | |
| return []; | |
| } | |
| return items.map(item => ({ | |
| ...item, | |
| source: platform.charAt(0).toUpperCase() + platform.slice(1), | |
| _platform: platform | |
| })); | |
| } catch (error) { | |
| console.error(`${platform} error:`, error); | |
| return []; | |
| } | |
| } | |
| // ============================================ | |
| // DISPLAY RESULTS | |
| // ============================================ | |
| function displayResults(results, platformCount, timeElapsed) { | |
| const resultsDiv = document.getElementById('results'); | |
| const statsDiv = document.getElementById('stats'); | |
| statsDiv.classList.add('active'); | |
| document.getElementById('totalResults').textContent = results.length; | |
| document.getElementById('platformsUsed').textContent = platformCount; | |
| document.getElementById('searchTime').textContent = timeElapsed + 's'; | |
| results.forEach(item => { | |
| const card = createCard(item); | |
| resultsDiv.appendChild(card); | |
| }); | |
| } | |
| function createCard(item) { | |
| const card = document.createElement('div'); | |
| card.className = 'result-card'; | |
| const title = item.title || item.name || item.product_title || 'No title'; | |
| const price = extractPrice(item); | |
| const formattedPrice = formatPrice(price); | |
| const url = item.url || item.link || item.product_url || '#'; | |
| const image = item.image_url || item.imageUrl || item.image || null; | |
| const description = item.description || item.condition || ''; | |
| let html = ` | |
| <div class="card-header"> | |
| <div class="card-title" title="${escapeHtml(title)}">${escapeHtml(title.substring(0, 45))}</div> | |
| <span class="platform-badge">${item.source}</span> | |
| </div> | |
| <div class="card-content"> | |
| `; | |
| if (image) { | |
| html += `<img src="${escapeHtml(image)}" class="card-image" onerror="this.style.display='none'">`; | |
| } | |
| html += ` | |
| <div class="product-info"> | |
| <div class="label">π° Price</div> | |
| <div class="price">${escapeHtml(formattedPrice)}</div> | |
| </div> | |
| `; | |
| if (description) { | |
| html += ` | |
| <div class="product-info"> | |
| <div class="label">π Details</div> | |
| <div class="value">${escapeHtml(description.substring(0, 100))}</div> | |
| </div> | |
| `; | |
| } | |
| if (url && url !== '#') { | |
| html += ` | |
| <div class="product-info"> | |
| <a href="${escapeHtml(url)}" target="_blank" class="link">π View</a> | |
| </div> | |
| `; | |
| } | |
| html += '</div>'; | |
| card.innerHTML = html; | |
| return card; | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Multi-Platform Product Scraper</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| } | |
| header { | |
| text-align: center; | |
| color: white; | |
| margin-bottom: 30px; | |
| } | |
| h1 { | |
| font-size: 2.5em; | |
| margin-bottom: 10px; | |
| text-shadow: 2px 2px 4px rgba(0,0,0,0.3); | |
| } | |
| .search-section { | |
| background: white; | |
| padding: 30px; | |
| border-radius: 10px; | |
| box-shadow: 0 8px 32px rgba(0,0,0,0.1); | |
| margin-bottom: 30px; | |
| } | |
| .input-group { | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 20px; | |
| flex-wrap: wrap; | |
| align-items: center; | |
| } | |
| input[type="text"], | |
| input[type="number"], | |
| select { | |
| padding: 12px 15px; | |
| border: 2px solid #e0e0e0; | |
| border-radius: 5px; | |
| font-size: 1em; | |
| } | |
| input[type="text"]:focus, | |
| input[type="number"]:focus, | |
| select:focus { | |
| outline: none; | |
| border-color: #667eea; | |
| box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); | |
| } | |
| input[type="text"] { | |
| flex: 1; | |
| min-width: 250px; | |
| } | |
| input[type="number"] { | |
| width: 120px; | |
| } | |
| button { | |
| padding: 12px 30px; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| border: none; | |
| border-radius: 5px; | |
| font-size: 1em; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| } | |
| button:hover:not(:disabled) { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4); | |
| } | |
| button:disabled { | |
| opacity: 0.6; | |
| cursor: not-allowed; | |
| } | |
| .platform-selector { | |
| display: flex; | |
| gap: 15px; | |
| margin-top: 20px; | |
| flex-wrap: wrap; | |
| } | |
| .platform-checkbox { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 10px 15px; | |
| background: #f5f5f5; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| } | |
| .platform-checkbox input { | |
| cursor: pointer; | |
| width: 18px; | |
| height: 18px; | |
| accent-color: #667eea; | |
| } | |
| .loading { | |
| text-align: center; | |
| padding: 40px; | |
| background: white; | |
| border-radius: 10px; | |
| box-shadow: 0 8px 32px rgba(0,0,0,0.1); | |
| display: none; | |
| margin-top: 30px; | |
| } | |
| .loading.active { | |
| display: block; | |
| } | |
| .spinner { | |
| border: 4px solid #f3f3f3; | |
| border-top: 4px solid #667eea; | |
| border-radius: 50%; | |
| width: 50px; | |
| height: 50px; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 20px; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .alert { | |
| padding: 15px; | |
| border-radius: 5px; | |
| margin-top: 20px; | |
| display: none; | |
| border-left: 4px solid; | |
| } | |
| .alert.active { | |
| display: block; | |
| } | |
| .alert.error { | |
| background: #fee; | |
| color: #c00; | |
| border-color: #c00; | |
| } | |
| .alert.success { | |
| background: #d4edda; | |
| color: #155724; | |
| border-color: #c3e6cb; | |
| } | |
| .no-results { | |
| text-align: center; | |
| padding: 40px; | |
| background: white; | |
| border-radius: 10px; | |
| color: #999; | |
| font-size: 1.1em; | |
| margin-top: 30px; | |
| display: none; | |
| } | |
| .no-results.active { | |
| display: block; | |
| } | |
| .stats { | |
| background: white; | |
| padding: 20px; | |
| border-radius: 10px; | |
| margin-top: 20px; | |
| display: none; | |
| gap: 30px; | |
| flex-wrap: wrap; | |
| } | |
| .stats.active { | |
| display: flex; | |
| } | |
| .stat { | |
| flex: 1; | |
| min-width: 150px; | |
| } | |
| .stat-value { | |
| font-size: 2em; | |
| font-weight: bold; | |
| color: #667eea; | |
| } | |
| .stat-label { | |
| color: #999; | |
| font-size: 0.9em; | |
| margin-top: 5px; | |
| } | |
| .results { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); | |
| gap: 20px; | |
| margin-top: 30px; | |
| } | |
| .result-card { | |
| background: white; | |
| border-radius: 10px; | |
| overflow: hidden; | |
| box-shadow: 0 8px 32px rgba(0,0,0,0.1); | |
| transition: all 0.3s; | |
| } | |
| .result-card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 12px 40px rgba(0,0,0,0.15); | |
| } | |
| .card-header { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 15px; | |
| font-weight: 600; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .card-title { | |
| flex: 1; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| font-size: 0.95em; | |
| } | |
| .platform-badge { | |
| background: rgba(255,255,255,0.3); | |
| padding: 4px 12px; | |
| border-radius: 15px; | |
| font-size: 0.75em; | |
| white-space: nowrap; | |
| } | |
| .card-image { | |
| width: 100%; | |
| height: 200px; | |
| object-fit: cover; | |
| background: #f0f0f0; | |
| display: block; | |
| } | |
| .card-content { | |
| padding: 20px; | |
| } | |
| .product-info { | |
| margin-bottom: 12px; | |
| } | |
| .label { | |
| font-weight: 600; | |
| color: #333; | |
| margin-bottom: 5px; | |
| font-size: 0.85em; | |
| } | |
| .value { | |
| color: #666; | |
| word-break: break-word; | |
| font-size: 0.9em; | |
| line-height: 1.4; | |
| } | |
| .price { | |
| font-size: 1.8em; | |
| color: #28a745; | |
| font-weight: bold; | |
| margin: 10px 0; | |
| } | |
| .link { | |
| display: inline-block; | |
| color: white; | |
| text-decoration: none; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| padding: 8px 15px; | |
| border-radius: 4px; | |
| font-weight: 600; | |
| margin-top: 10px; | |
| font-size: 0.9em; | |
| } | |
| .link:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); | |
| } | |
| @media (max-width: 768px) { | |
| .input-group { | |
| flex-direction: column; | |
| } | |
| input[type="text"] { | |
| min-width: auto; | |
| } | |
| input[type="number"] { | |
| width: 100%; | |
| } | |
| .results { | |
| grid-template-columns: 1fr; | |
| } | |
| h1 { | |
| font-size: 1.8em; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <h1>π Multi-Platform Product Scraper</h1> | |
| <p>Search CEX, MediaMarkt, and Backmarket</p> | |
| </header> | |
| <div class="search-section"> | |
| <div class="input-group"> | |
| <input | |
| type="text" | |
| id="apiKeyInput" | |
| placeholder="Enter your Apify API Key (apify_api_...)" | |
| autocomplete="off" | |
| style="flex: 1;" | |
| > | |
| <button onclick="saveApiKey()" style="min-width: 150px;">πΎ Save Key</button> | |
| </div> | |
| <div class="input-group" style="margin-top: 15px;"> | |
| <input | |
| type="text" | |
| id="searchInput" | |
| placeholder="Enter product name (e.g., iPhone 13)" | |
| autocomplete="off" | |
| > | |
| <input | |
| type="number" | |
| id="maxItems" | |
| value="5" | |
| min="1" | |
| max="50" | |
| > | |
| <button id="searchBtn" onclick="searchProducts()">π Search</button> | |
| </div> | |
| <div class="platform-selector"> | |
| <label class="platform-checkbox"> | |
| <input type="checkbox" id="cex" checked> π³ CEX | |
| </label> | |
| <label class="platform-checkbox"> | |
| <input type="checkbox" id="mediamarkt" checked> π MediaMarkt | |
| </label> | |
| <label class="platform-checkbox"> | |
| <input type="checkbox" id="backmarket" checked> β»οΈ Backmarket | |
| </label> | |
| </div> | |
| </div> | |
| <div class="alert error" id="errorAlert"> | |
| <span id="errorMessage"></span> | |
| </div> | |
| <div class="alert success" id="successAlert"> | |
| <span id="successMessage"></span> | |
| </div> | |
| <div class="loading" id="loading"> | |
| <div class="spinner"></div> | |
| <div id="loadingText">Searching platforms...</div> | |
| </div> | |
| <div id="stats" class="stats"> | |
| <div class="stat"> | |
| <div class="stat-value" id="totalResults">0</div> | |
| <div class="stat-label">Total Results</div> | |
| </div> | |
| <div class="stat"> | |
| <div class="stat-value" id="platformsUsed">0</div> | |
| <div class="stat-label">Platforms</div> | |
| </div> | |
| <div class="stat"> | |
| <div class="stat-value" id="searchTime">0s</div> | |
| <div class="stat-label">Time</div> | |
| </div> | |
| </div> | |
| <div class="no-results" id="noResults"> | |
| <p>π No products found. Try different keywords.</p> | |
| </div> | |
| <div class="results" id="results"></div> | |
| </div> | |
| <script> | |
| // ============================================ | |
| // GLOBAL STATE | |
| // ============================================ | |
| let API_KEY = localStorage.getItem('apifyApiKey') || ''; | |
| const ACTORS = { | |
| cex: { | |
| id: 'sync-network~cex-product-scraper-uk-webuy-com', | |
| endpoint: '/run-sync-get-dataset-items' | |
| }, | |
| mediamarkt: { | |
| id: 'scrapegoats~MediaMarkt-Scraper', | |
| endpoint: '/run-sync-get-dataset-items' | |
| }, | |
| backmarket: { | |
| id: 'scrapegoats~Backmarket-Scraper', | |
| endpoint: '/run-sync-get-dataset-items' | |
| } | |
| }; | |
| const REQUEST_TIMEOUT = 300000; | |
| // ============================================ | |
| // INITIALIZATION | |
| // ============================================ | |
| window.addEventListener('load', function() { | |
| if (API_KEY) { | |
| document.getElementById('apiKeyInput').value = 'ββββββββ' + API_KEY.slice(-4); | |
| } | |
| }); | |
| // ============================================ | |
| // API KEY | |
| // ============================================ | |
| function saveApiKey() { | |
| let keyInput = document.getElementById('apiKeyInput').value.trim(); | |
| if (!keyInput || keyInput.startsWith('β')) { | |
| showAlert('error', 'β Please enter a valid API key'); | |
| return; | |
| } | |
| if (!keyInput.startsWith('apify_api_')) { | |
| showAlert('error', 'β Must start with "apify_api_"'); | |
| return; | |
| } | |
| API_KEY = keyInput; | |
| localStorage.setItem('apifyApiKey', API_KEY); | |
| document.getElementById('apiKeyInput').value = 'ββββββββ' + API_KEY.slice(-4); | |
| showAlert('success', 'β API key saved'); | |
| } | |
| // ============================================ | |
| // ALERTS | |
| // ============================================ | |
| function showAlert(type, message) { | |
| const alertId = type + 'Alert'; | |
| const messageId = type + 'Message'; | |
| document.getElementById(messageId).textContent = message; | |
| document.getElementById(alertId).classList.add('active'); | |
| setTimeout(() => { | |
| document.getElementById(alertId).classList.remove('active'); | |
| }, 5000); | |
| } | |
| // ============================================ | |
| // UTILITIES | |
| // ============================================ | |
| function escapeHtml(text) { | |
| if (!text) return ''; | |
| const div = document.createElement('div'); | |
| div.textContent = String(text); | |
| return div.innerHTML; | |
| } | |
| function setLoading(active) { | |
| document.getElementById('loading').classList.toggle('active', active); | |
| } | |
| // ============================================ | |
| // PRICE PARSING - CRITICAL FIX | |
| // ============================================ | |
| function extractPrice(item) { | |
| // CEX specific | |
| if (item.buy_price !== undefined) { | |
| const price = parseFloat(String(item.buy_price).replace(/[^0-9.]/g, '')); | |
| if (!isNaN(price)) return price; | |
| } | |
| // Backmarket specific | |
| if (item.price !== undefined) { | |
| if (typeof item.price === 'number') return item.price; | |
| const price = parseFloat(String(item.price).replace(/[^0-9.]/g, '')); | |
| if (!isNaN(price)) return price; | |
| } | |
| // MediaMarkt specific | |
| if (item.priceValue !== undefined) { | |
| const price = parseFloat(String(item.priceValue).replace(/[^0-9.]/g, '')); | |
| if (!isNaN(price)) return price; | |
| } | |
| // Generic fallbacks | |
| if (item.price_value !== undefined) { | |
| const price = parseFloat(String(item.price_value).replace(/[^0-9.]/g, '')); | |
| if (!isNaN(price)) return price; | |
| } | |
| if (item.listing_price !== undefined) { | |
| const price = parseFloat(String(item.listing_price).replace(/[^0-9.]/g, '')); | |
| if (!isNaN(price)) return price; | |
| } | |
| if (item.salePrice !== undefined) { | |
| const price = parseFloat(String(item.salePrice).replace(/[^0-9.]/g, '')); | |
| if (!isNaN(price)) return price; | |
| } | |
| return null; | |
| } | |
| function formatPrice(price) { | |
| if (price === null || price === undefined) return 'N/A'; | |
| if (typeof price === 'number') { | |
| return '$' + price.toFixed(2); | |
| } | |
| return String(price); | |
| } | |
| // ============================================ | |
| // MAIN SEARCH | |
| // ============================================ | |
| async function searchProducts() { | |
| if (!API_KEY) { | |
| showAlert('error', 'β Save your API key first'); | |
| return; | |
| } | |
| const searchInput = document.getElementById('searchInput').value.trim(); | |
| if (!searchInput) { | |
| showAlert('error', 'β Enter a product name'); | |
| return; | |
| } | |
| const platforms = []; | |
| if (document.getElementById('cex').checked) platforms.push('cex'); | |
| if (document.getElementById('mediamarkt').checked) platforms.push('mediamarkt'); | |
| if (document.getElementById('backmarket').checked) platforms.push('backmarket'); | |
| if (platforms.length === 0) { | |
| showAlert('error', 'β Select at least one platform'); | |
| return; | |
| } | |
| document.getElementById('searchBtn').disabled = true; | |
| document.getElementById('results').innerHTML = ''; | |
| document.getElementById('noResults').classList.remove('active'); | |
| document.getElementById('stats').classList.remove('active'); | |
| setLoading(true); | |
| const startTime = Date.now(); | |
| let allResults = []; | |
| let successCount = 0; | |
| try { | |
| const promises = platforms.map(p => searchPlatform(p, searchInput)); | |
| const results = await Promise.allSettled(promises); | |
| results.forEach((result, idx) => { | |
| if (result.status === 'fulfilled' && result.value && result.value.length > 0) { | |
| allResults = allResults.concat(result.value); | |
| successCount++; | |
| } | |
| }); | |
| const timeElapsed = ((Date.now() - startTime) / 1000).toFixed(2); | |
| if (allResults.length === 0) { | |
| document.getElementById('noResults').classList.add('active'); | |
| } else { | |
| displayResults(allResults, successCount, timeElapsed); | |
| showAlert('success', `β Found ${allResults.length} products`); | |
| } | |
| } catch (error) { | |
| showAlert('error', 'β Error: ' + error.message); | |
| } finally { | |
| setLoading(false); | |
| document.getElementById('searchBtn').disabled = false; | |
| } | |
| } | |
| // ============================================ | |
| // PLATFORM SEARCH | |
| // ============================================ | |
| async function searchPlatform(platform, query) { | |
| const actor = ACTORS[platform]; | |
| const url = `https://api.apify.com/v2/acts/${actor.id}${actor.endpoint}?token=${API_KEY}`; | |
| let payload = {}; | |
| if (platform === 'cex') { | |
| payload = { | |
| search_input: query, | |
| max_items: parseInt(document.getElementById('maxItems').value) || 5, | |
| include_images: true, | |
| include_trade_values: true, | |
| throttle: 2 | |
| }; | |
| } else if (platform === 'backmarket') { | |
| payload = { | |
| function: 'country_and_keywords', | |
| searchs: [query], | |
| country_input: 'United States', | |
| limit_per_search: parseInt(document.getElementById('maxItems').value) || 5 | |
| }; | |
| } else if (platform === 'mediamarkt') { | |
| payload = { | |
| search_input: query, | |
| max_items: parseInt(document.getElementById('maxItems').value) || 5 | |
| }; | |
| } | |
| try { | |
| const controller = new AbortController(); | |
| const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT); | |
| const response = await fetch(url, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload), | |
| signal: controller.signal | |
| }); | |
| clearTimeout(timeout); | |
| if (!response.ok) { | |
| throw new Error(`HTTP ${response.status}`); | |
| } | |
| const items = await response.json(); | |
| if (!Array.isArray(items)) { | |
| return []; | |
| } | |
| return items.map(item => ({ | |
| ...item, | |
| source: platform.charAt(0).toUpperCase() + platform.slice(1), | |
| _platform: platform | |
| })); | |
| } catch (error) { | |
| console.error(`${platform} error:`, error); | |
| return []; | |
| } | |
| } | |
| // ============================================ | |
| // DISPLAY RESULTS | |
| // ============================================ | |
| function displayResults(results, platformCount, timeElapsed) { | |
| const resultsDiv = document.getElementById('results'); | |
| const statsDiv = document.getElementById('stats'); | |
| statsDiv.classList.add('active'); | |
| document.getElementById('totalResults').textContent = results.length; | |
| document.getElementById('platformsUsed').textContent = platformCount; | |
| document.getElementById('searchTime').textContent = timeElapsed + 's'; | |
| results.forEach(item => { | |
| const card = createCard(item); | |
| resultsDiv.appendChild(card); | |
| }); | |
| } | |
| function createCard(item) { | |
| const card = document.createElement('div'); | |
| card.className = 'result-card'; | |
| const title = item.title || item.name || item.product_title || 'No title'; | |
| const price = extractPrice(item); | |
| const formattedPrice = formatPrice(price); | |
| const url = item.url || item.link || item.product_url || '#'; | |
| const image = item.image_url || item.imageUrl || item.image || null; | |
| const description = item.description || item.condition || ''; | |
| let html = ` | |
| <div class="card-header"> | |
| <div class="card-title" title="${escapeHtml(title)}">${escapeHtml(title.substring(0, 45))}</div> | |
| <span class="platform-badge">${item.source}</span> | |
| </div> | |
| <div class="card-content"> | |
| `; | |
| if (image) { | |
| html += `<img src="${escapeHtml(image)}" class="card-image" onerror="this.style.display='none'">`; | |
| } | |
| html += ` | |
| <div class="product-info"> | |
| <div class="label">π° Price</div> | |
| <div class="price">${escapeHtml(formattedPrice)}</div> | |
| </div> | |
| `; | |
| if (description) { | |
| html += ` | |
| <div class="product-info"> | |
| <div class="label">π Details</div> | |
| <div class="value">${escapeHtml(description.substring(0, 100))}</div> | |
| </div> | |
| `; | |
| } | |
| if (url && url !== '#') { | |
| html += ` | |
| <div class="product-info"> | |
| <a href="${escapeHtml(url)}" target="_blank" class="link">π View</a> | |
| </div> | |
| `; | |
| } | |
| html += '</div>'; | |
| card.innerHTML = html; | |
| return card; | |
| } | |
| </script> | |
| </body> | |
| </html> |