api/public/js/search.js

488 lines
No EOL
18 KiB
JavaScript

class MapSearch {
constructor(map) {
this.map = map;
this.searchResults = [];
this.currentMarkers = [];
this.userLocation = null;
this.init();
}
init() {
this.createSearchUI();
this.bindEvents();
this.getUserLocation();
}
createSearchUI() {
// ایجاد پنل جستجو
const searchPanel = document.createElement('div');
searchPanel.className = 'search-panel';
searchPanel.innerHTML = `
<div class="search-header">
<h3>جستجو در نقشه</h3>
<button class="close-btn" onclick="this.parentElement.parentElement.style.display='none'">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
</svg>
</button>
</div>
<div class="search-input-container">
<div class="search-input-wrapper">
<input type="text" id="searchInput" placeholder="جستجو در مکان‌ها، خیابان‌ها..." />
<button id="searchBtn" class="search-icon-btn" title="جستجو">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
</svg>
</button>
</div>
</div>
<div class="search-filters">
<select id="searchType" class="search-select">
<option value="all">همه</option>
<option value="points">مکان‌ها</option>
<option value="streets">خیابان‌ها</option>
<option value="areas">مناطق</option>
</select>
<button id="locationBtn" class="location-btn" title="استفاده از موقعیت من">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/>
</svg>
</button>
</div>
<div id="searchResults" class="search-results"></div>
`;
// اضافه کردن استایل
const style = document.createElement('style');
style.textContent = `
.search-panel {
position: absolute;
top: 10px;
right: 10px;
width: 380px;
background: white;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.12);
z-index: 1000;
font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
direction: rtl;
display: none;
border: 1px solid rgba(0,0,0,0.08);
overflow: hidden;
transform: translateZ(0);
}
.search-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.search-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.close-btn {
background: rgba(255,255,255,0.2);
border: none;
border-radius: 50%;
width: 28px;
height: 28px;
cursor: pointer;
color: white;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.close-btn:hover {
background: rgba(255,255,255,0.3);
transform: scale(1.05);
}
.search-input-container {
padding: 16px 20px;
}
.search-input-wrapper {
position: relative;
display: flex;
align-items: center;
}
#searchInput {
flex: 1;
padding: 12px 16px;
padding-left: 48px;
border: 2px solid #e1e5e9;
border-radius: 8px;
font-size: 14px;
font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
transition: all 0.2s ease;
background: #f8f9fa;
}
#searchInput:focus {
outline: none;
border-color: #667eea;
background: white;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.search-icon-btn {
position: absolute;
left: 12px;
background: none;
border: none;
color: #667eea;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: all 0.2s ease;
}
.search-icon-btn:hover {
background: rgba(102, 126, 234, 0.1);
transform: scale(1.05);
}
.search-filters {
padding: 0 20px 16px;
display: flex;
gap: 12px;
align-items: center;
}
.search-select {
flex: 1;
padding: 10px 12px;
border: 2px solid #e1e5e9;
border-radius: 8px;
font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
font-size: 13px;
background: #f8f9fa;
transition: all 0.2s ease;
}
.search-select:focus {
outline: none;
border-color: #667eea;
background: white;
}
.location-btn {
padding: 10px;
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.location-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3);
}
.search-results {
max-height: 300px;
overflow-y: auto;
overflow-x: hidden;
border-top: 1px solid #f0f0f0;
}
.search-result-item {
padding: 12px 20px;
border-bottom: 1px solid #f8f9fa;
cursor: pointer;
transition: padding-right 0.2s ease, background-color 0.2s ease;
}
.search-result-item:hover {
background: #f8f9fa;
padding-right: 22px;
}
.search-result-item:last-child {
border-bottom: none;
}
.result-name {
font-weight: 600;
color: #333;
margin-bottom: 4px;
font-size: 14px;
}
.result-category {
color: #666;
font-size: 12px;
font-weight: 400;
}
.result-city {
color: #28a745;
font-size: 12px;
font-weight: 500;
margin-top: 2px;
}
.result-distance {
color: #007bff;
font-size: 12px;
margin-top: 4px;
font-weight: 500;
}
.no-results {
padding: 24px 20px;
text-align: center;
color: #666;
font-weight: 400;
}
.loading {
padding: 24px 20px;
text-align: center;
color: #666;
font-weight: 400;
}
.loading::after {
content: '';
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #667eea;
border-radius: 50%;
border-top-color: transparent;
animation: spin 1s linear infinite;
margin-right: 8px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
`;
document.head.appendChild(style);
document.body.appendChild(searchPanel);
}
bindEvents() {
const searchInput = document.getElementById('searchInput');
const searchBtn = document.getElementById('searchBtn');
const locationBtn = document.getElementById('locationBtn');
// جستجو با Enter
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.performSearch();
}
});
// جستجو با کلیک
searchBtn.addEventListener('click', () => {
this.performSearch();
});
// استفاده از موقعیت کاربر
locationBtn.addEventListener('click', () => {
this.getUserLocation();
});
// Autocomplete
let timeout;
searchInput.addEventListener('input', () => {
clearTimeout(timeout);
timeout = setTimeout(() => {
if (searchInput.value.length >= 2) {
this.performSearch();
}
}, 500);
});
}
async performSearch() {
const query = document.getElementById('searchInput').value.trim();
const type = document.getElementById('searchType').value;
if (!query) {
this.showResults([]);
return;
}
this.showLoading();
try {
const params = new URLSearchParams({
q: query,
type: type,
limit: 20
});
// اضافه کردن موقعیت کاربر اگر موجود باشد
if (this.userLocation) {
params.append('lat', this.userLocation.lat);
params.append('lng', this.userLocation.lng);
}
const response = await fetch(`/api/search/places?${params}`);
const data = await response.json();
if (data.success) {
this.searchResults = data.results;
this.showResults(data.results);
this.addMarkersToMap(data.results);
} else {
this.showError(data.error || 'خطا در جستجو');
}
} catch (error) {
console.error('Search error:', error);
this.showError('خطا در اتصال به سرور');
}
}
showResults(results) {
const resultsContainer = document.getElementById('searchResults');
if (results.length === 0) {
resultsContainer.innerHTML = '<div class="no-results">نتیجه‌ای یافت نشد</div>';
return;
}
const resultsHtml = results.map(result => `
<div class="search-result-item" onclick="mapSearch.navigateToResult(${result.lat}, ${result.lng}, '${result.name}')">
<div class="result-name">${this.escapeHtml(result.name)}</div>
<div class="result-category">${this.getCategoryText(result.category)}</div>
${result.city && result.city !== 'نامشخص' ? `<div class="result-city">${this.escapeHtml(result.city)}</div>` : ''}
${result.distance ? `<div class="result-distance">${result.distance} متر</div>` : ''}
</div>
`).join('');
resultsContainer.innerHTML = resultsHtml;
}
showLoading() {
const resultsContainer = document.getElementById('searchResults');
resultsContainer.innerHTML = '<div class="loading">در حال جستجو...</div>';
}
showError(message) {
const resultsContainer = document.getElementById('searchResults');
resultsContainer.innerHTML = `<div class="no-results" style="color: #dc3545;">${message}</div>`;
}
addMarkersToMap(results) {
// حذف مارکرهای قبلی
this.clearMarkers();
results.forEach(result => {
const marker = L.marker([result.lat, result.lng], {
icon: this.getMarkerIcon(result.category)
});
const popupContent = `
<div style="text-align: right; direction: rtl; font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; padding-right: 20px;">
<h4 style="margin: 0 0 6px 0; font-weight: 600; color: #333; font-size: 14px; line-height: 1.3;">${this.escapeHtml(result.name)}</h4>
<p style="margin: 3px 0; color: #666; font-size: 12px;">${this.getCategoryText(result.category)}</p>
${result.city && result.city !== 'نامشخص' ? `<p style="margin: 3px 0; color: #28a745; font-size: 11px; font-weight: 500;">شهر: ${this.escapeHtml(result.city)}</p>` : ''}
${result.distance ? `<p style="margin: 3px 0; color: #007bff; font-size: 11px; font-weight: 500;">فاصله: ${result.distance} متر</p>` : ''}
</div>
`;
marker.bindPopup(popupContent);
marker.addTo(this.map);
this.currentMarkers.push(marker);
});
// تنظیم زوم و مرکز نقشه اگر نتیجه‌ای وجود دارد
if (results.length > 0) {
const group = new L.featureGroup(this.currentMarkers);
this.map.fitBounds(group.getBounds().pad(0.1));
}
}
clearMarkers() {
this.currentMarkers.forEach(marker => {
this.map.removeLayer(marker);
});
this.currentMarkers = [];
}
navigateToResult(lat, lng, name) {
this.map.setView([lat, lng], 16);
// نمایش پاپ‌آپ برای مارکر
this.currentMarkers.forEach(marker => {
const markerLatLng = marker.getLatLng();
if (markerLatLng.lat === lat && markerLatLng.lng === lng) {
marker.openPopup();
}
});
}
getUserLocation() {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
this.userLocation = {
lat: position.coords.latitude,
lng: position.coords.longitude
};
// اضافه کردن مارکر موقعیت کاربر
this.addUserLocationMarker();
console.log('User location:', this.userLocation);
},
(error) => {
console.log('Geolocation error:', error);
}
);
}
}
addUserLocationMarker() {
if (this.userLocationMarker) {
this.map.removeLayer(this.userLocationMarker);
}
this.userLocationMarker = L.marker([this.userLocation.lat, this.userLocation.lng], {
icon: L.divIcon({
className: 'user-location-marker',
html: '📍',
iconSize: [30, 30],
iconAnchor: [15, 15]
})
});
this.userLocationMarker.bindPopup('موقعیت شما').addTo(this.map);
}
getMarkerIcon(category) {
const iconColors = {
'رستوران': '#ff6b6b',
'کافه': '#4ecdc4',
'بانک': '#45b7d1',
'داروخانه': '#96ceb4',
'بیمارستان': '#feca57',
'مدرسه': '#ff9ff3',
'دانشگاه': '#54a0ff',
'فروشگاه': '#5f27cd',
'خیابان': '#00d2d3',
'پارک': '#10ac84',
'مکان مذهبی': '#ff6348',
'تسهیلات': '#747d8c',
'مکان': '#747d8c'
};
const color = iconColors[category] || '#747d8c';
return L.divIcon({
className: 'custom-marker',
html: `<div style="background-color: ${color}; width: 20px; height: 20px; border-radius: 50%; border: 2px solid white; box-shadow: 0 2px 4px rgba(0,0,0,0.3);"></div>`,
iconSize: [20, 20],
iconAnchor: [10, 10]
});
}
getCategoryText(category) {
return category || 'مکان';
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
// متغیر سراسری برای دسترسی از HTML
let mapSearch;