scraping / index.html
samihalawa's picture
Update index.html
7a8a1e0 verified
<!DOCTYPE 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><!DOCTYPE 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>