feat: Implementar markers nativos de Mapbox con popups
- Reemplazar Arenarium por markers nativos de Mapbox - Agregar popups con info del POI al hacer clic - Hover effect con sombra (sin escalar) - Remover ícono de búsqueda en MapPointsTab - Estilos CSS para botón de cerrar popup Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
1081
src/components/admin/MapPointsTab.tsx
Normal file
1081
src/components/admin/MapPointsTab.tsx
Normal file
File diff suppressed because it is too large
Load Diff
215
src/lib/map/ArenariumMarkers.ts
Normal file
215
src/lib/map/ArenariumMarkers.ts
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
// src/lib/map/ArenariumMarkers.ts
|
||||||
|
// Markers usando Mapbox nativo (sin Arenarium para mejor control)
|
||||||
|
|
||||||
|
import mapboxgl from 'mapbox-gl';
|
||||||
|
import { getPOIMarkerIcon } from './POIMarkerIcons';
|
||||||
|
|
||||||
|
// CSS para el popup
|
||||||
|
const popupStyles = `
|
||||||
|
.mapboxgl-popup-close-button {
|
||||||
|
right: 10px !important;
|
||||||
|
top: 5px !important;
|
||||||
|
font-size: 18px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Inyectar estilos una sola vez
|
||||||
|
let stylesInjected = false;
|
||||||
|
function injectPopupStyles() {
|
||||||
|
if (stylesInjected) return;
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = popupStyles;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
stylesInjected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArenariumPOI {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
category: string;
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
address?: string;
|
||||||
|
description?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ArenariumMarkerManager {
|
||||||
|
private map: mapboxgl.Map;
|
||||||
|
private markers: Map<string, mapboxgl.Marker> = new Map();
|
||||||
|
private pois: Map<string, ArenariumPOI> = new Map();
|
||||||
|
private activePopup: mapboxgl.Popup | null = null;
|
||||||
|
|
||||||
|
constructor(map: mapboxgl.Map) {
|
||||||
|
this.map = map;
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize(): Promise<void> {
|
||||||
|
injectPopupStyles();
|
||||||
|
console.log('[Markers] Initialized with native Mapbox markers');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear elemento HTML para el pin
|
||||||
|
private createPinElement(poi: ArenariumPOI): HTMLElement {
|
||||||
|
const iconInfo = getPOIMarkerIcon(poi.category);
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'mapbox-pin';
|
||||||
|
el.style.cssText = `
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: ${iconInfo.color};
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid white;
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
`;
|
||||||
|
|
||||||
|
el.innerHTML = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="width: 18px; height: 18px;">
|
||||||
|
${iconInfo.svg}
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Hover effect - solo cambiar sombra, no escalar
|
||||||
|
el.addEventListener('mouseenter', () => {
|
||||||
|
el.style.boxShadow = '0 4px 12px rgba(0,0,0,0.4)';
|
||||||
|
});
|
||||||
|
el.addEventListener('mouseleave', () => {
|
||||||
|
el.style.boxShadow = '0 2px 6px rgba(0,0,0,0.3)';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click handler
|
||||||
|
el.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
console.log('[Markers] Pin clicked:', poi.id, poi.name);
|
||||||
|
|
||||||
|
// Mostrar popup
|
||||||
|
this.showPopup(poi.id);
|
||||||
|
|
||||||
|
// Ejecutar callback si existe
|
||||||
|
if (poi.onClick) {
|
||||||
|
poi.onClick();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear popup HTML
|
||||||
|
private createPopupContent(poi: ArenariumPOI): string {
|
||||||
|
const iconInfo = getPOIMarkerIcon(poi.category);
|
||||||
|
return `
|
||||||
|
<div style="font-family: system-ui, -apple-system, sans-serif; min-width: 180px;">
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
||||||
|
<div style="
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: ${iconInfo.color}20;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="${iconInfo.color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width: 16px; height: 16px;">
|
||||||
|
${iconInfo.svg}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style="font-weight: 600; font-size: 14px;">${poi.name}</div>
|
||||||
|
<div style="font-size: 11px; color: #888;">${iconInfo.label}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${poi.address ? `<div style="font-size: 12px; color: #666; margin-bottom: 6px;">${poi.address}</div>` : ''}
|
||||||
|
${poi.description ? `<div style="font-size: 12px; color: #444;">${poi.description}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Agregar un POI
|
||||||
|
addMarker(poi: ArenariumPOI): void {
|
||||||
|
this.pois.set(poi.id, poi);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remover un POI
|
||||||
|
removeMarker(id: string): void {
|
||||||
|
const marker = this.markers.get(id);
|
||||||
|
if (marker) {
|
||||||
|
marker.remove();
|
||||||
|
this.markers.delete(id);
|
||||||
|
}
|
||||||
|
this.pois.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limpiar todos los markers
|
||||||
|
clearMarkers(): void {
|
||||||
|
this.markers.forEach(marker => marker.remove());
|
||||||
|
this.markers.clear();
|
||||||
|
this.pois.clear();
|
||||||
|
this.hidePopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualizar markers en el mapa
|
||||||
|
async updateMarkers(): Promise<void> {
|
||||||
|
// Remover markers existentes que ya no están en pois
|
||||||
|
const currentIds = new Set(this.pois.keys());
|
||||||
|
this.markers.forEach((marker, id) => {
|
||||||
|
if (!currentIds.has(id)) {
|
||||||
|
marker.remove();
|
||||||
|
this.markers.delete(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Agregar o actualizar markers
|
||||||
|
this.pois.forEach((poi, id) => {
|
||||||
|
if (!this.markers.has(id)) {
|
||||||
|
const el = this.createPinElement(poi);
|
||||||
|
const marker = new mapboxgl.Marker({ element: el })
|
||||||
|
.setLngLat([poi.lng, poi.lat])
|
||||||
|
.addTo(this.map);
|
||||||
|
|
||||||
|
this.markers.set(id, marker);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[Markers] Updated ${this.markers.size} markers`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mostrar popup de un marker
|
||||||
|
showPopup(id: string): void {
|
||||||
|
const poi = this.pois.get(id);
|
||||||
|
if (!poi) return;
|
||||||
|
|
||||||
|
// Cerrar popup anterior
|
||||||
|
this.hidePopup();
|
||||||
|
|
||||||
|
// Crear nuevo popup
|
||||||
|
this.activePopup = new mapboxgl.Popup({
|
||||||
|
closeButton: true,
|
||||||
|
closeOnClick: true,
|
||||||
|
maxWidth: '300px',
|
||||||
|
offset: [0, -20]
|
||||||
|
})
|
||||||
|
.setLngLat([poi.lng, poi.lat])
|
||||||
|
.setHTML(this.createPopupContent(poi))
|
||||||
|
.addTo(this.map);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ocultar popup
|
||||||
|
hidePopup(): void {
|
||||||
|
if (this.activePopup) {
|
||||||
|
this.activePopup.remove();
|
||||||
|
this.activePopup = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destruir manager
|
||||||
|
destroy(): void {
|
||||||
|
this.clearMarkers();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user