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:
2026-03-19 15:33:16 -04:00
parent 951a1f64ac
commit 5680954094
2 changed files with 1296 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View 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();
}
}