Fix Boostrap y Try Cache
This commit is contained in:
@@ -5,19 +5,6 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
|
||||
@@ -26,25 +26,61 @@ export const DevApp = () => {
|
||||
<h1 style={{ textAlign: 'center', fontFamily: 'sans-serif' }}>
|
||||
Showcase de Widgets - Elecciones 2025
|
||||
</h1>
|
||||
<main>
|
||||
<DipSenTickerWidget />
|
||||
<ResumenGeneralWidget />
|
||||
<main>
|
||||
<DipSenTickerWidget />
|
||||
<ResumenGeneralWidget />
|
||||
<SenadoresWidget />
|
||||
<DiputadosWidget />
|
||||
<ConcejalesWidget />
|
||||
<SenadoresTickerWidget />
|
||||
<DiputadosTickerWidget />
|
||||
<ConcejalesTickerWidget />
|
||||
<DiputadosPorSeccionWidget />
|
||||
<SenadoresPorSeccionWidget />
|
||||
<ConcejalesPorSeccionWidget />
|
||||
<CongresoWidget />
|
||||
<BancasWidget />
|
||||
<MapaBsAs />
|
||||
<MapaBsAsSecciones />
|
||||
<TelegramaWidget />
|
||||
<ResultadosTablaDetalladaWidget />
|
||||
<ResultadosRankingMunicipioWidget />
|
||||
<ConcejalesWidget />
|
||||
<SenadoresTickerWidget />
|
||||
<DiputadosTickerWidget />
|
||||
<ConcejalesTickerWidget />
|
||||
<DiputadosPorSeccionWidget />
|
||||
<SenadoresPorSeccionWidget />
|
||||
<ConcejalesPorSeccionWidget />
|
||||
<CongresoWidget />
|
||||
<BancasWidget />
|
||||
<MapaBsAs />
|
||||
<MapaBsAsSecciones />
|
||||
<TelegramaWidget />
|
||||
<ResultadosTablaDetalladaWidget />
|
||||
<ResultadosRankingMunicipioWidget />
|
||||
|
||||
<hr className="border-gray-300 my-8" />
|
||||
|
||||
<h2>Mapa - Vista General (Por Defecto)</h2>
|
||||
<p>Carga la vista provincial completa para Diputados.</p>
|
||||
<MapaBsAs />
|
||||
|
||||
<hr className="border-gray-300 my-8" />
|
||||
|
||||
<h2>Mapa - Foco en La Plata (Diputados por defecto)</h2>
|
||||
<p>Carga el mapa y automáticamente hace zoom en La Plata.</p>
|
||||
<MapaBsAs focoMunicipio="LA PLATA" />
|
||||
|
||||
<hr className="border-gray-300 my-8" />
|
||||
|
||||
<h2>Mapa - Foco en Campana</h2>
|
||||
<p>Carga el mapa y automáticamente hace zoom en Campana.</p>
|
||||
<MapaBsAs focoMunicipio="CAMPANA" focoCategoria="senadores" />
|
||||
|
||||
<hr className="border-gray-300 my-8" />
|
||||
|
||||
<h2>Mapa - Vista General de Senadores</h2>
|
||||
<p>Carga la vista provincial completa para la categoría Senadores.</p>
|
||||
<MapaBsAs focoCategoria="senadores" />
|
||||
|
||||
<hr className="border-gray-300 my-8" />
|
||||
|
||||
<h2>Mapa - Foco en Bahía Blanca para Concejales</h2>
|
||||
<p>Carga el mapa enfocado en Bahía Blanca y con la categoría Concejales seleccionada.</p>
|
||||
<MapaBsAs focoMunicipio="BAHIA BLANCA" focoCategoria="concejales" />
|
||||
|
||||
<hr className="border-gray-300 my-8" />
|
||||
|
||||
<h2>Mapa - Foco Inválido (La Plata para Senadores)</h2>
|
||||
<p>Debería mostrar la vista provincial de Senadores y un warning en la consola del navegador.</p>
|
||||
<MapaBsAs focoMunicipio="LA PLATA" focoCategoria="senadores" />
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simp
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import axios from 'axios';
|
||||
import { feature } from 'topojson-client';
|
||||
import type { Feature, Geometry } from 'geojson';
|
||||
import { geoCentroid } from 'd3-geo';
|
||||
import { API_BASE_URL, assetBaseUrl } from '../apiService';
|
||||
@@ -12,6 +13,11 @@ import './MapaBsAs.css';
|
||||
// --- Interfaces y Tipos ---
|
||||
type PointTuple = [number, number];
|
||||
|
||||
interface MapaBsAsProps {
|
||||
focoMunicipio?: string;
|
||||
focoCategoria?: string;
|
||||
}
|
||||
|
||||
interface ResultadoMapa {
|
||||
ambitoId: number;
|
||||
departamentoNombre: string;
|
||||
@@ -33,15 +39,8 @@ interface Agrupacion {
|
||||
nombre: string;
|
||||
}
|
||||
|
||||
interface Categoria {
|
||||
id: number;
|
||||
nombre: string;
|
||||
}
|
||||
|
||||
interface PartidoProperties {
|
||||
departamento: string;
|
||||
}
|
||||
|
||||
interface Categoria { id: number; nombre: string; }
|
||||
interface PartidoProperties { departamento: string; }
|
||||
type PartidoGeography = Feature<Geometry, PartidoProperties> & { rsmKey: string };
|
||||
|
||||
// --- Constantes ---
|
||||
@@ -52,19 +51,43 @@ const INITIAL_POSITION = { center: [-60.5, -37.2] as PointTuple, zoom: MIN_ZOOM
|
||||
const DEFAULT_MAP_COLOR = '#E0E0E0';
|
||||
|
||||
const CATEGORIAS: Categoria[] = [
|
||||
{ id: 5, nombre: 'Senadores' },
|
||||
{ id: 6, nombre: 'Diputados' },
|
||||
{ id: 5, nombre: 'Senadores' },
|
||||
{ id: 7, nombre: 'Concejales' }
|
||||
];
|
||||
|
||||
// --- Helper de Normalización ---
|
||||
const normalizarTexto = (texto: string = ''): string => {
|
||||
return texto
|
||||
.trim()
|
||||
.toUpperCase()
|
||||
.normalize("NFD") // Separa los acentos de las letras
|
||||
.replace(/[\u0300-\u036f]/g, ""); // Elimina los acentos
|
||||
};
|
||||
|
||||
// --- Componente Principal ---
|
||||
const MapaBsAs = () => {
|
||||
const MapaBsAs = ({ focoMunicipio, focoCategoria }: MapaBsAsProps) => {
|
||||
// --- LÓGICA DE ESTADO SIMPLIFICADA ---
|
||||
const categoriaInicial = useMemo(() => {
|
||||
const catNorm = focoCategoria?.toLowerCase();
|
||||
if (catNorm === 'senadores') return 5;
|
||||
if (catNorm === 'concejales') return 7;
|
||||
return 6;
|
||||
}, [focoCategoria]);
|
||||
|
||||
const [position, setPosition] = useState(INITIAL_POSITION);
|
||||
const [selectedAmbitoId, setSelectedAmbitoId] = useState<number | null>(null);
|
||||
const [selectedCategoriaId, setSelectedCategoriaId] = useState<number>(6);
|
||||
const [selectedCategoriaId, setSelectedCategoriaId] = useState<number>(categoriaInicial);
|
||||
const [tooltipContent, setTooltipContent] = useState('');
|
||||
const [isPanning, setIsPanning] = useState(false);
|
||||
|
||||
// Sincroniza el estado si la prop cambia. Esto es para cuando el widget ya está montado
|
||||
// y recibe nuevas props (no ocurrirá en tu caso actual, pero es buena práctica).
|
||||
useEffect(() => {
|
||||
setSelectedCategoriaId(categoriaInicial);
|
||||
}, [categoriaInicial]);
|
||||
|
||||
// --- QUERIES ---
|
||||
const { data: resultadosData, isLoading: isLoadingResultados } = useQuery<ResultadoMapa[]>({
|
||||
queryKey: ['mapaResultadosPorMunicipio', selectedCategoriaId],
|
||||
queryFn: async () => (await axios.get(`${API_BASE_URL}/Resultados/mapa-por-municipio?categoriaId=${selectedCategoriaId}`)).data,
|
||||
@@ -80,21 +103,16 @@ const MapaBsAs = () => {
|
||||
queryFn: async () => (await axios.get(`${API_BASE_URL}/Catalogos/agrupaciones`)).data,
|
||||
});
|
||||
|
||||
const { nombresAgrupaciones, resultadosPorDepartamento } = useMemo<{
|
||||
nombresAgrupaciones: Map<string, string>;
|
||||
resultadosPorDepartamento: Map<string, ResultadoMapa>;
|
||||
}>(() => {
|
||||
const { nombresAgrupaciones, resultadosPorDepartamento } = useMemo(() => {
|
||||
const nombresMap = new Map<string, string>();
|
||||
const resultadosMap = new Map<string, ResultadoMapa>();
|
||||
if (agrupacionesData) {
|
||||
agrupacionesData.forEach((agrupacion) => {
|
||||
nombresMap.set(agrupacion.id, agrupacion.nombre);
|
||||
});
|
||||
agrupacionesData.forEach((a) => nombresMap.set(a.id, a.nombre));
|
||||
}
|
||||
if (resultadosData) {
|
||||
resultadosData.forEach(r => {
|
||||
if (r.departamentoNombre) {
|
||||
resultadosMap.set(r.departamentoNombre.toUpperCase(), r)
|
||||
resultadosMap.set(normalizarTexto(r.departamentoNombre), r);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -108,20 +126,105 @@ const MapaBsAs = () => {
|
||||
setPosition(INITIAL_POSITION);
|
||||
}, []);
|
||||
|
||||
// --- LÓGICA DE CLIC Y FOCO ---
|
||||
const handleGeographyClick = useCallback((geo: PartidoGeography) => {
|
||||
const departamentoNombre = geo.properties.departamento.toUpperCase();
|
||||
const resultado = resultadosPorDepartamento.get(departamentoNombre);
|
||||
const departamentoNombreNormalizado = normalizarTexto(geo.properties.departamento);
|
||||
const resultado = resultadosPorDepartamento.get(departamentoNombreNormalizado);
|
||||
if (!resultado) return;
|
||||
const ambitoIdParaSeleccionar = resultado.ambitoId;
|
||||
if (selectedAmbitoId === ambitoIdParaSeleccionar) {
|
||||
|
||||
if (selectedAmbitoId === resultado.ambitoId) {
|
||||
handleReset();
|
||||
} else {
|
||||
const centroid = geoCentroid(geo) as PointTuple;
|
||||
setPosition({ center: centroid, zoom: 5 });
|
||||
setSelectedAmbitoId(ambitoIdParaSeleccionar);
|
||||
setSelectedAmbitoId(resultado.ambitoId);
|
||||
}
|
||||
}, [selectedAmbitoId, handleReset, resultadosPorDepartamento]);
|
||||
|
||||
// --- useEffect DE INICIALIZACIÓN (Ligeramente refinado) ---
|
||||
useEffect(() => {
|
||||
if (isLoading || !focoMunicipio || selectedAmbitoId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const geometries = geoData?.objects?.['departamentos-buenos_aires']?.geometries;
|
||||
if (!geometries) return;
|
||||
|
||||
const nombreFocoNormalizado = normalizarTexto(focoMunicipio);
|
||||
const geoTargetTopo = geometries.find(
|
||||
(g: any) => normalizarTexto(g.properties.departamento) === nombreFocoNormalizado
|
||||
);
|
||||
|
||||
if (geoTargetTopo) {
|
||||
if (resultadosPorDepartamento.has(nombreFocoNormalizado)) {
|
||||
const geoTargetGeoJSON = feature(geoData, geoTargetTopo);
|
||||
handleGeographyClick(geoTargetGeoJSON as unknown as PartidoGeography);
|
||||
}
|
||||
}
|
||||
// Deshabilitamos la regla para que solo se ejecute cuando isLoading cambia a false.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (categoriaInicial !== selectedCategoriaId) {
|
||||
setSelectedCategoriaId(categoriaInicial);
|
||||
handleReset();
|
||||
}
|
||||
}, [categoriaInicial, selectedCategoriaId, handleReset]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => e.key === 'Escape' && handleReset();
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [handleReset]);
|
||||
|
||||
const renderGeography = useCallback((geo: PartidoGeography, isSelectedGeo: boolean = false) => {
|
||||
const departamentoNombreNormalizado = normalizarTexto(geo.properties.departamento);
|
||||
const resultado = resultadosPorDepartamento.get(departamentoNombreNormalizado);
|
||||
const isClickable = !!resultado;
|
||||
const isSelected = isSelectedGeo || (selectedAmbitoId !== null && selectedAmbitoId === resultado?.ambitoId);
|
||||
const isFaded = !isSelectedGeo && selectedAmbitoId !== null && !isSelected;
|
||||
|
||||
const nombreAgrupacionGanadora = resultado
|
||||
? nombresAgrupaciones.get(resultado.agrupacionGanadoraId)
|
||||
: 'Sin datos';
|
||||
|
||||
return (
|
||||
<Geography
|
||||
key={geo.rsmKey + (isSelectedGeo ? '-selected' : '')}
|
||||
geography={geo}
|
||||
className={`rsm-geography ${isSelected ? 'selected' : ''} ${isFaded ? 'faded' : ''} ${!isClickable ? 'no-results' : ''}`}
|
||||
fill={resultado?.colorGanador || DEFAULT_MAP_COLOR}
|
||||
onClick={isClickable ? () => handleGeographyClick(geo) : undefined}
|
||||
onMouseEnter={() => {
|
||||
if (isClickable) {
|
||||
setTooltipContent(`${geo.properties.departamento}: ${nombreAgrupacionGanadora}`);
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => setTooltipContent("")}
|
||||
/>
|
||||
);
|
||||
}, [resultadosPorDepartamento, selectedAmbitoId, nombresAgrupaciones, handleGeographyClick]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading || !focoMunicipio || selectedAmbitoId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const geometries = geoData?.objects?.['departamentos-buenos_aires']?.geometries;
|
||||
if (!geometries) return;
|
||||
|
||||
const nombreFocoNormalizado = normalizarTexto(focoMunicipio);
|
||||
const geoTargetTopo = geometries.find(
|
||||
(g: any) => normalizarTexto(g.properties.departamento) === nombreFocoNormalizado
|
||||
);
|
||||
|
||||
if (geoTargetTopo && resultadosPorDepartamento.has(nombreFocoNormalizado)) {
|
||||
const geoTargetGeoJSON = feature(geoData, geoTargetTopo);
|
||||
handleGeographyClick(geoTargetGeoJSON as unknown as PartidoGeography);
|
||||
}
|
||||
}, [isLoading, focoMunicipio, selectedCategoriaId]);
|
||||
|
||||
const handleMoveEnd = (newPosition: { coordinates: PointTuple; zoom: number }) => {
|
||||
if (newPosition.zoom <= MIN_ZOOM) {
|
||||
if (position.zoom > MIN_ZOOM || selectedAmbitoId !== null) {
|
||||
@@ -147,35 +250,6 @@ const MapaBsAs = () => {
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [handleReset]);
|
||||
|
||||
const getPartyFillColor = (departamentoNombre: string) => {
|
||||
const resultado = resultadosPorDepartamento.get(departamentoNombre.toUpperCase());
|
||||
return resultado?.colorGanador || DEFAULT_MAP_COLOR;
|
||||
};
|
||||
|
||||
// --- Helper de Renderizado ---
|
||||
const renderGeography = (geo: PartidoGeography, isSelectedGeo: boolean = false) => {
|
||||
const departamentoNombre = geo.properties.departamento.toUpperCase();
|
||||
const resultado = resultadosPorDepartamento.get(departamentoNombre);
|
||||
const isClickable = !!resultado;
|
||||
const isSelected = isSelectedGeo || (selectedAmbitoId !== null && selectedAmbitoId === resultado?.ambitoId);
|
||||
const isFaded = !isSelectedGeo && selectedAmbitoId !== null && !isSelected;
|
||||
const nombreAgrupacionGanadora = resultado ? nombresAgrupaciones.get(resultado.agrupacionGanadoraId) : 'Sin datos';
|
||||
|
||||
return (
|
||||
<Geography
|
||||
key={geo.rsmKey + (isSelectedGeo ? '-selected' : '')}
|
||||
geography={geo}
|
||||
data-tooltip-id="partido-tooltip"
|
||||
data-tooltip-content={`${geo.properties.departamento}: ${nombreAgrupacionGanadora}`}
|
||||
className={`rsm-geography ${isSelected ? 'selected' : ''} ${isFaded ? 'faded' : ''} ${!isClickable ? 'no-results' : ''}`}
|
||||
fill={getPartyFillColor(geo.properties.departamento)}
|
||||
onClick={isClickable ? () => handleGeographyClick(geo) : undefined}
|
||||
onMouseEnter={() => setTooltipContent(`${geo.properties.departamento}: ${nombreAgrupacionGanadora}`)}
|
||||
onMouseLeave={() => setTooltipContent("")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mapa-wrapper">
|
||||
<div className="mapa-container">
|
||||
@@ -213,7 +287,7 @@ const MapaBsAs = () => {
|
||||
{({ geographies }: { geographies: PartidoGeography[] }) => {
|
||||
const selectedGeo = selectedAmbitoId
|
||||
? geographies.find(geo => {
|
||||
const resultado = resultadosPorDepartamento.get(geo.properties.departamento.toUpperCase());
|
||||
const resultado = resultadosPorDepartamento.get(normalizarTexto(geo.properties.departamento));
|
||||
return resultado?.ambitoId === selectedAmbitoId;
|
||||
})
|
||||
: null;
|
||||
@@ -239,14 +313,15 @@ const MapaBsAs = () => {
|
||||
className="mapa-categoria-combobox"
|
||||
value={selectedCategoriaId}
|
||||
onChange={(e) => {
|
||||
// --- LÓGICA DE CAMBIO DE CATEGORÍA ---
|
||||
// Limpiamos el foco de municipio al cambiar de categoría
|
||||
setSelectedAmbitoId(null);
|
||||
setPosition(INITIAL_POSITION);
|
||||
setSelectedCategoriaId(Number(e.target.value));
|
||||
handleReset();
|
||||
}}
|
||||
>
|
||||
{CATEGORIAS.map(cat => (
|
||||
<option key={cat.id} value={cat.id}>
|
||||
{cat.nombre}
|
||||
</option>
|
||||
<option key={cat.id} value={cat.id}>{cat.nombre}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
@@ -259,6 +334,7 @@ const MapaBsAs = () => {
|
||||
|
||||
// --- Sub-componentes ---
|
||||
const ControlesMapa = ({ onReset }: { onReset: () => void }) => (
|
||||
|
||||
<div className="map-controls">
|
||||
<button onClick={onReset}>← VOLVER</button>
|
||||
</div>
|
||||
@@ -299,10 +375,10 @@ const DetalleMunicipio = ({ ambitoId, onReset, categoriaId }: { ambitoId: number
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</li>
|
||||
</li >
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</ul >
|
||||
</div >
|
||||
);
|
||||
};
|
||||
|
||||
@@ -321,6 +397,7 @@ const Legend = ({ resultados, nombresAgrupaciones }: { resultados: Map<string, R
|
||||
});
|
||||
|
||||
return Array.from(ganadoresUnicos.values());
|
||||
|
||||
}, [resultados, nombresAgrupaciones]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -63,33 +63,30 @@ if (import.meta.env.DEV) {
|
||||
} else {
|
||||
// --- MODO PRODUCCIÓN ---
|
||||
// Exponemos la función de renderizado para el bootstrap.js
|
||||
const renderWidgets = () => {
|
||||
const widgetContainers = document.querySelectorAll('[data-elecciones-widget]');
|
||||
|
||||
if (widgetContainers.length === 0) {
|
||||
console.warn('React: ADVERTENCIA - No se encontró ningún elemento en el DOM para renderizar un widget. Verifica que el HTML contenga <div data-elecciones-widget="...">.');
|
||||
}
|
||||
|
||||
widgetContainers.forEach(container => {
|
||||
const widgetName = (container as HTMLElement).dataset.eleccionesWidget;
|
||||
// La función de renderizado acepta el contenedor y las props
|
||||
const renderWidgets = (container: HTMLElement, props: DOMStringMap) => {
|
||||
const widgetName = props.eleccionesWidget;
|
||||
|
||||
if (widgetName && WIDGET_MAP[widgetName]) {
|
||||
const WidgetComponent = WIDGET_MAP[widgetName];
|
||||
const root = ReactDOM.createRoot(container);
|
||||
|
||||
if (widgetName && WIDGET_MAP[widgetName]) {
|
||||
const WidgetComponent = WIDGET_MAP[widgetName];
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<WidgetComponent />
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
} else {
|
||||
console.error(`React: ERROR - No se encontró un componente para el nombre de widget: "${widgetName}"`);
|
||||
}
|
||||
});
|
||||
// Pasamos todas las props (ej. { eleccionesWidget: '...', focoMunicipio: '...' })
|
||||
// al componente que se va a renderizar.
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<WidgetComponent {...props} />
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
} else {
|
||||
console.error(`React: ERROR - No se encontró un componente para el nombre de widget: "${widgetName}"`);
|
||||
}
|
||||
};
|
||||
|
||||
// La función expuesta ahora se llamará por cada widget, no una sola vez.
|
||||
(window as any).EleccionesWidgets = {
|
||||
render: renderWidgets
|
||||
render: renderWidgets
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user