Fix Boostrap y Try Cache

This commit is contained in:
2025-09-10 14:20:44 -03:00
parent 6309003536
commit 153c0f92da
14 changed files with 276 additions and 142 deletions

View File

@@ -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);

View File

@@ -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>
</>
);

View File

@@ -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 (

View File

@@ -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
};
}