الملفات
mapscreper/frontend/index.html
Abdelsabour 905ad8f612 first commit
2025-09-03 03:03:42 +03:00

284 أسطر
17 KiB
HTML

<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>مستخرج بيانات Google Maps الاحترافي</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Cairo', sans-serif;
background-color: #f8fafc;
}
.icon-wrapper {
display: flex;
align-items: center;
gap: 0.75rem;
color: #4b5563;
}
.icon-wrapper svg {
width: 1.25rem;
height: 1.25rem;
color: #4f46e5;
flex-shrink: 0;
}
.badge {
background-color: #eef2ff;
color: #4338ca;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.875rem;
font-weight: 500;
}
</style>
</head>
<body class="antialiased">
<div class="container mx-auto p-4 md:p-8 max-w-5xl">
<header class="bg-gradient-to-r from-indigo-600 to-purple-600 text-white p-8 rounded-xl shadow-2xl mb-10 text-center">
<h1 class="text-3xl md:text-4xl font-bold">📍 مستخرج بيانات Google Maps الاحترافي</h1>
<p class="mt-3 text-lg opacity-90">استخراج شامل لكل التفاصيل: الخدمات، المراجعات، الصور والمزيد</p>
</header>
<main>
<div class="bg-white p-6 rounded-xl shadow-lg mb-8 border border-gray-200">
<form id="scrape-form" class="flex flex-col sm:flex-row gap-4 items-center">
<input type="url" id="url-input" placeholder="الصق رابط خرائط جوجل هنا..." class="flex-grow w-full p-4 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:outline-none transition" required>
<button type="submit" class="bg-indigo-600 text-white font-bold py-4 px-8 rounded-lg hover:bg-indigo-700 transition-colors duration-300 shadow-md flex items-center justify-center w-full sm:w-auto">
<svg id="search-icon" class="w-5 h-5 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
<svg id="loading-spinner" class="animate-spin h-5 w-5 text-white hidden ml-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span id="button-text">استخراج</span>
</button>
</form>
</div>
<div id="results-container">
<div class="text-center p-10 bg-white rounded-xl shadow-md border border-gray-200">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path vector-effect="non-scaling-stroke" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h14a2 2 0 012 2v10a2 2 0 01-2 2H4a2 2 0 01-2-2z" />
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">لا توجد بيانات بعد</h3>
<p class="mt-1 text-sm text-gray-500">ابدأ بلصق رابط في الحقل أعلاه.</p>
</div>
</div>
</main>
<footer class="text-center text-gray-500 mt-12">
<p>&copy; 2025 مستخرج بيانات Google Maps. Web Scraping باستخدام Node.js</p>
</footer>
</div>
<script>
const form = document.getElementById('scrape-form');
const urlInput = document.getElementById('url-input');
const resultsContainer = document.getElementById('results-container');
const searchIcon = document.getElementById('search-icon');
const loadingSpinner = document.getElementById('loading-spinner');
const submitButton = form.querySelector('button');
const buttonText = document.getElementById('button-text');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const url = urlInput.value;
resultsContainer.innerHTML = '';
searchIcon.classList.add('hidden');
loadingSpinner.classList.remove('hidden');
submitButton.disabled = true;
buttonText.textContent = 'جاري العمل...';
try {
const response = await fetch('/scrape', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url }),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const data = await response.json();
displayCard(data);
} catch (error) {
displayError(error.message);
} finally {
searchIcon.classList.remove('hidden');
loadingSpinner.classList.add('hidden');
submitButton.disabled = false;
buttonText.textContent = 'استخراج';
}
});
function displayCard(data) {
const mainImage = Array.isArray(data.images) && data.images.length > 0 ? data.images[0] : 'https://placehold.co/800x400/e2e8f0/4a5568?text=No+Image';
let galleryHTML = '';
if (Array.isArray(data.images) && data.images.length > 1) {
galleryHTML = `
<div class="mt-6">
<h4 class="font-semibold text-gray-700 mb-3">معرض الصور:</h4>
<div class="grid grid-cols-3 sm:grid-cols-5 gap-3">
${data.images.slice(0, 10).map(img => `
<img src="${img}" alt="صورة مصغرة" class="w-full h-24 object-cover rounded-lg cursor-pointer hover:opacity-80 transition-opacity shadow" onclick="document.getElementById('main-image').src='${img}'">
`).join('')}
</div>
</div>
`;
}
let aboutHTML = '';
if (Array.isArray(data.about) && data.about.length > 0) {
aboutHTML = data.about.map(section => `
<div class="mb-4">
<h4 class="font-semibold text-gray-800 mb-2">${section.title}</h4>
<div class="flex flex-wrap gap-2">
${section.items.map(item => `<span class="badge">${item}</span>`).join('')}
</div>
</div>
`).join('');
}
let webResultsHTML = '';
if(Array.isArray(data.webResults) && data.webResults.length > 0) {
webResultsHTML = `
<div class="mt-8">
<h3 class="text-xl font-bold text-gray-800 mb-4 border-b pb-2">نتائج الويب</h3>
<div class="space-y-4">
${data.webResults.map(result => generateWebsiteRow(result.link, result.title)).join('')}
</div>
</div>
`;
}
let reviewSummaryHTML = '';
if(Array.isArray(data.reviewSummary) && data.reviewSummary.length > 0) {
reviewSummaryHTML = `
<div class="mt-8">
<h3 class="text-xl font-bold text-gray-800 mb-4 border-b pb-2">ملخص التقييمات</h3>
<div class="space-y-2">
${data.reviewSummary.map(summary => `
<div class="flex items-center">
<span class="text-sm text-gray-600 w-12">${summary.stars} نجوم</span>
<div class="w-full bg-gray-200 rounded-full h-2.5 mx-2">
<div class="bg-yellow-400 h-2.5 rounded-full" style="width: ${summary.percentage || '0%'}"></div>
</div>
</div>
`).join('')}
</div>
</div>
`;
}
let reviewsHTML = '';
if (Array.isArray(data.reviews) && data.reviews.length > 0) {
reviewsHTML = `
<div class="mt-8">
<h3 class="text-xl font-bold text-gray-800 mb-4 border-b pb-2">المراجعات</h3>
<div class="space-y-4">
${data.reviews.map(review => `
<div class="flex items-start space-x-4 space-x-reverse">
<img class="h-10 w-10 rounded-full" src="${review.avatar || 'https://placehold.co/40x40'}" alt="صورة المراجع">
<div class="flex-1">
<div class="flex items-center justify-between">
<p class="text-sm font-semibold text-gray-900">${review.author || 'مستخدم جوجل'}</p>
${review.rating ? `
<div class="flex items-center text-yellow-500">
<span class="text-sm font-bold ml-1">${review.rating}</span>
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
</div>` : ''}
</div>
<p class="mt-1 text-gray-600">${review.text || ''}</p>
</div>
</div>
`).join('')}
</div>
</div>
`;
}
const cardHTML = `
<div class="bg-white rounded-xl shadow-lg overflow-hidden border border-gray-200 animate-fade-in">
<style>.animate-fade-in { animation: fadeIn 0.5s ease-in-out; } @keyframes fadeIn { 0% { opacity: 0; transform: translateY(10px); } 100% { opacity: 1; transform: translateY(0); } }</style>
<img id="main-image" src="${mainImage}" alt="صورة ${data.name || 'المكان'}" class="w-full h-72 object-cover">
<div class="p-6 md:p-8">
<p class="text-indigo-600 font-semibold mb-2">${data.category || 'فئة غير متوفرة'}</p>
<h2 class="text-3xl font-bold text-gray-900 mb-4">${data.name || 'غير متوفر'}</h2>
${data.rating ? `
<div class="flex items-center mb-6">
<div class="flex items-center text-yellow-500">
<span class="text-xl font-bold ml-2">${data.rating}</span>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
</div>
<span class="text-gray-500 text-sm mr-3">(${data.reviewsCount || '0'} مراجعة)</span>
</div>` : ''}
<div class="grid md:grid-cols-2 gap-x-8 gap-y-4 border-t border-b py-6 my-6">
${generateDetailRow('M15 10.5a3 3 0 11-6 0 3 3 0 016 0z M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z', data.address)}
${generateDetailRow('M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 002.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106c-.44-.11-.902.055-1.173.417l-.97 1.293c-.282.376-.769.542-1.21.38a12.035 12.035 0 01-7.143-7.143c-.162-.441.004-.928.38-1.21l1.293-.97c.363-.271.527-.734.417-1.173L6.963 3.102a1.125 1.125 0 00-1.091-.852H4.5A2.25 2.25 0 002.25 4.5v2.25z', data.phone)}
${generateWebsiteRow(data.website, 'زيارة الموقع الرسمي')}
${generateCoordinatesRow(data.coordinates)}
${generateDetailRow('M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z', data.hours)}
</div>
${aboutHTML}
${webResultsHTML}
${reviewSummaryHTML}
${reviewsHTML}
${galleryHTML}
</div>
</div>
`;
resultsContainer.innerHTML = cardHTML;
}
function generateDetailRow(svgPath, text) {
if (!text) return '';
return `<div class="icon-wrapper">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="${svgPath}" /></svg>
<span>${text}</span>
</div>`;
}
function generateWebsiteRow(website, title) {
if (!website) return '';
const svgPath = "M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A11.953 11.953 0 0112 16.5c-2.998 0-5.74-1.1-7.843-2.918m15.686-5.834A8.959 8.959 0 0021 12c0 .778-.099 1.533-.284 2.253m0 0a11.953 11.953 0 00-2.284 2.253m-13.402 0a11.953 11.953 0 00-2.284-2.253";
return `<div class="icon-wrapper">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="${svgPath}" /></svg>
<a href="${website}" target="_blank" class="text-indigo-600 hover:underline">${title || website}</a>
</div>`;
}
function generateCoordinatesRow(coords) {
if (!coords || !coords.latitude) return '';
const text = `${coords.latitude}, ${coords.longitude}`;
const gmapsUrl = `https://www.google.com/maps?q=${coords.latitude},${coords.longitude}`;
const svgPath = "M15 10.5a3 3 0 11-6 0 3 3 0 016 0z M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z";
return `<div class="icon-wrapper">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="${svgPath}" /></svg>
<span>${text} <a href="${gmapsUrl}" target="_blank" class="text-indigo-600 hover:underline mr-1">(فتح في الخريطة)</a></span>
</div>`;
}
function displayError(message) {
const errorHTML = `
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded-lg" role="alert">
<p class="font-bold">حدث خطأ!</p>
<p>${message}</p>
</div>
`;
resultsContainer.innerHTML = errorHTML;
}
</script>
</body>
</html>