Refinamiento de Funciones y Estética de Mapa

This commit is contained in:
2025-09-20 22:31:11 -03:00
parent 7d2922aaeb
commit 3750d1a56d
6 changed files with 674 additions and 98 deletions

View File

@@ -5,7 +5,7 @@ import './DevAppStyle.css'
export const DevAppLegislativas = () => { export const DevAppLegislativas = () => {
return ( return (
<div className="container"> <div className="container">
<h1>Il visualizzatore di widget - Elecciones Nacionales 2025</h1> <h1>Visor de Widgets</h1>
{/* Le pasamos el ID de la elección que queremos visualizar. {/* Le pasamos el ID de la elección que queremos visualizar.
Para tus datos de prueba provinciales, este ID es 1. */} Para tus datos de prueba provinciales, este ID es 1. */}

View File

@@ -10,6 +10,12 @@
.panel-header { .panel-header {
padding: 1rem 1.5rem; padding: 1rem 1.5rem;
border-bottom: 1px solid #e0e0e0; border-bottom: 1px solid #e0e0e0;
position: relative;
/* Necesario para que z-index funcione */
z-index: 20;
/* Un número alto para ponerlo al frente */
background-color: white;
/* Asegura que no sea transparente */
} }
/* Contenedor para alinear título y selector */ /* Contenedor para alinear título y selector */
@@ -29,20 +35,89 @@
min-width: 220px; min-width: 220px;
} }
/* El contenedor principal del selector (la parte visible antes de hacer clic) */
.categoria-selector__control {
border-radius: 8px !important;
/* Coincide con el radio de los otros elementos */
border: 1px solid #e0e0e0 !important;
box-shadow: none !important;
/* Quitamos la sombra por defecto */
transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
/* Estilo cuando el selector está enfocado (seleccionado) */
.categoria-selector__control--is-focused {
border-color: #007bff !important;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25) !important;
}
/* El texto del valor seleccionado */
.categoria-selector__single-value {
font-weight: 500;
color: #333;
}
/* El menú desplegable que contiene las opciones */
.categoria-selector__menu {
border-radius: 8px !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important;
border: 1px solid #e0e0e0 !important;
margin-top: 4px !important;
/* Pequeño espacio entre el control y el menú */
}
/* Cada una de las opciones en la lista */
.categoria-selector__option {
cursor: pointer;
transition: background-color 0.2s, color 0.2s;
}
/* Estilo de una opción cuando pasas el mouse por encima (estado 'focused') */
.categoria-selector__option--is-focused {
background-color: #f0f8ff;
/* Un azul muy claro */
color: #333;
}
/* Estilo de la opción que está actualmente seleccionada */
.categoria-selector__option--is-selected {
background-color: #007bff;
color: white;
}
/* La pequeña línea vertical que separa el contenido del indicador (la flecha) */
.categoria-selector__indicator-separator {
display: none;
/* La ocultamos para un look más limpio */
}
/* El indicador (la flecha hacia abajo) */
.categoria-selector__indicator {
color: #a0a0a0;
transition: color 0.2s;
}
.categoria-selector__indicator:hover {
color: #333;
}
/* --- ESTILOS MODERNOS PARA BREADCRUMBS --- */ /* --- ESTILOS MODERNOS PARA BREADCRUMBS --- */
.breadcrumbs-container { .breadcrumbs-container {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; /* Espacio entre elementos */ gap: 0.5rem;
font-size: 0.9rem; /* Espacio entre elementos */
font-size: 1rem;
} }
.breadcrumb-item, .breadcrumb-item-actual { .breadcrumb-item,
.breadcrumb-item-actual {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0.4rem 0.8rem; padding: 0.4rem 0.8rem;
border-radius: 8px; /* Bordes redondeados para efecto píldora */ border-radius: 8px;
/* Bordes redondeados para efecto píldora */
transition: background-color 0.2s ease-in-out; transition: background-color 0.2s ease-in-out;
} }
@@ -62,7 +137,8 @@
.breadcrumb-item-actual { .breadcrumb-item-actual {
background-color: transparent; background-color: transparent;
color: #000; color: #000;
font-weight: 700; /* Más peso para el nivel actual */ font-weight: 700;
/* Más peso para el nivel actual */
} }
.breadcrumb-icon { .breadcrumb-icon {
@@ -71,7 +147,8 @@
} }
.breadcrumb-separator { .breadcrumb-separator {
color: #a0a0a0; /* Color sutil para el separador */ color: #a0a0a0;
/* Color sutil para el separador */
font-size: 1.2rem; font-size: 1.2rem;
} }
@@ -85,18 +162,21 @@
/* Columna del mapa */ /* Columna del mapa */
.mapa-column { .mapa-column {
flex: 2; /* Por defecto, ocupa 2/3 del espacio */ flex: 2;
/* Por defecto, ocupa 2/3 del espacio */
position: relative; position: relative;
transition: flex 0.5s ease-in-out; transition: flex 0.5s ease-in-out;
} }
/* Columna de resultados */ /* Columna de resultados */
.resultados-column { .resultados-column {
flex: 1; /* Por defecto, ocupa 1/3 */ flex: 1;
/* Por defecto, ocupa 1/3 */
overflow-y: auto; overflow-y: auto;
padding: 1.5rem; padding: 1.5rem;
transition: all 0.5s ease-in-out; transition: all 0.5s ease-in-out;
min-width: 320px; /* Un ancho mínimo para que no se comprima demasiado */ min-width: 320px;
/* Un ancho mínimo para que no se comprima demasiado */
} }
/* --- NUEVO LAYOUT PARA TARJETAS DE PARTIDO --- */ /* --- NUEVO LAYOUT PARA TARJETAS DE PARTIDO --- */
@@ -105,8 +185,8 @@
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
padding: 1rem 0; padding: 1rem 0;
border-bottom: 1px solid #f0f0f0; /* Separador sutil */ border-bottom: 1px solid #f0f0f0;
border-left: 5px solid; /* El color se aplica inline */ border-left: 5px solid;
padding-left: 1rem; padding-left: 1rem;
} }
@@ -125,15 +205,22 @@
.partido-main-content { .partido-main-content {
flex-grow: 1; flex-grow: 1;
display: flex; display: grid;
flex-direction: column; /* CAMBIO: De flex a grid */
gap: 0.5rem; /* Espacio entre la fila superior y la barra */ grid-template-columns: 1fr auto;
/* Columna 1 (nombre) flexible, Columna 2 (stats) se ajusta al contenido */
grid-template-rows: auto auto;
/* Dos filas: una para la info, otra para la barra */
align-items: center;
/* Alinea verticalmente el contenido de ambas filas */
gap: 0.25rem 1rem;
/* Espacio entre filas y columnas (0.25rem vertical, 1rem horizontal) */
} }
.partido-top-row { .partido-top-row {
display: flex; /* Hacemos que este contenedor sea "invisible" para el grid,
justify-content: space-between; promoviendo a sus hijos (info y stats) a la cuadrícula principal. */
align-items: flex-start; /* Alinea los elementos al tope */ display: contents;
} }
.partido-info-wrapper { .partido-info-wrapper {
@@ -142,7 +229,7 @@
} }
.partido-nombre { .partido-nombre {
font-weight: 500; font-weight: 800;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -156,7 +243,8 @@
.partido-stats { .partido-stats {
flex-shrink: 0; flex-shrink: 0;
text-align: right; text-align: right;
padding-left: 1rem; /* Espacio para que no se pegue al nombre */ padding-left: 1rem;
/* Ya no necesita ser un contenedor flex, el grid lo posiciona */
} }
.partido-porcentaje { .partido-porcentaje {
@@ -175,6 +263,8 @@
height: 20px; height: 20px;
background-color: #f0f0f0; background-color: #f0f0f0;
border-radius: 4px; border-radius: 4px;
grid-column: 1 / 3;
/* Le indicamos que ocupe ambas columnas (de la línea 1 a la 3) */
} }
.partido-barra-foreground { .partido-barra-foreground {
@@ -182,6 +272,7 @@
border-radius: 4px; border-radius: 4px;
transition: width 0.5s ease-in-out; transition: width 0.5s ease-in-out;
} }
/* ------------------------------------------- */ /* ------------------------------------------- */
.panel-estado-recuento { .panel-estado-recuento {
@@ -212,13 +303,15 @@
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
.mapa-render-area { .mapa-render-area {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.mapa-volver-btn { .mapa-volver-btn {
position: absolute; position: absolute;
top: 10px; top: 10px;
@@ -231,12 +324,15 @@
cursor: pointer; cursor: pointer;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
} }
.rsm-zoomable-group { .rsm-zoomable-group {
transition: transform 0.75s ease-in-out; transition: transform 0.75s ease-in-out;
} }
.panel-main-content.panel-collapsed .mapa-column { .panel-main-content.panel-collapsed .mapa-column {
flex: 1 1 100%; flex: 1 1 100%;
} }
.panel-main-content.panel-collapsed .resultados-column { .panel-main-content.panel-collapsed .resultados-column {
flex-basis: 0; flex-basis: 0;
min-width: 0; min-width: 0;
@@ -244,6 +340,7 @@
padding: 0; padding: 0;
overflow: hidden; overflow: hidden;
} }
.panel-toggle-btn { .panel-toggle-btn {
position: absolute; position: absolute;
top: 50%; top: 50%;
@@ -256,107 +353,435 @@
background-color: white; background-color: white;
border-radius: 4px 0 0 4px; border-radius: 4px 0 0 4px;
cursor: pointer; cursor: pointer;
font-size: 1.5rem; font-size: 1.3rem;
font-weight: bold; font-weight: bold;
color: #555; color: #555;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-shadow: -2px 0 5px rgba(0,0,0,0.1); box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1);
transition: background-color 0.2s; transition: background-color 0.2s;
} }
.panel-toggle-btn:hover { .panel-toggle-btn:hover {
background-color: #f0f0f0; background-color: #f0f0f0;
} }
.rsm-geography { .rsm-geography {
cursor: pointer; cursor: pointer;
stroke: #000000; stroke: #000000;
stroke-width: 0.25px; stroke-width: 0.25px;
outline: none; outline: none;
transition: filter 0.2s ease-in-out; transition: filter 0.2s ease-in-out;
} }
.rsm-geography:not(.selected):hover { .rsm-geography:not(.selected):hover {
filter: brightness(1.15); filter: brightness(1.25); /* Mantenemos el brillo */
stroke: #ffffff; stroke: #ffffff; /* Color del borde a blanco */
stroke-width: 0.25px; stroke-width: 0.25px;
paint-order: stroke; /* Asegura que el borde se dibuje encima del relleno */
} }
.rsm-geography.selected { .rsm-geography.selected {
stroke: #000000; stroke: #000000;
stroke-width: 0.25px; stroke-width: 0.25px;
filter: none; filter: none;
pointer-events: none; pointer-events: none;
} }
.rsm-geography-faded, .rsm-geography-faded,
.rsm-geography-faded-municipality { .rsm-geography-faded-municipality {
opacity: 0.5; opacity: 0.5;
pointer-events: none; pointer-events: none;
} }
.caba-comuna-geography { .caba-comuna-geography {
stroke: #000000; stroke: #000000;
stroke-width: 0.05px; stroke-width: 0.05px;
} }
.caba-comuna-geography:not(.selected):hover { .caba-comuna-geography:not(.selected):hover {
stroke: #000000; stroke: #000000;
stroke-width: 0.055px; stroke-width: 0.055px;
filter: brightness(1.25); filter: brightness(1.25);
} }
.transition-spinner { .transition-spinner {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: rgba(255, 255, 255, 0.5); background-color: rgba(255, 255, 255, 0.5);
z-index: 20; z-index: 20;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.transition-spinner::after { .transition-spinner::after {
content: ''; content: '';
width: 50px; width: 50px;
height: 50px; height: 50px;
border: 5px solid rgba(0, 0, 0, 0.2); border: 5px solid rgba(0, 0, 0, 0.2);
border-top-color: #007bff; border-top-color: #007bff;
border-radius: 50%; border-radius: 50%;
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
} }
@keyframes spin { @keyframes spin {
to { transform: rotate(360deg); } to {
transform: rotate(360deg);
}
} }
.caba-magnifier-container { .caba-magnifier-container {
position: absolute; position: absolute;
height: auto; height: auto;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
pointer-events: none; pointer-events: none;
} }
.caba-lupa-svg { .caba-lupa-svg {
width: 100%; width: 100%;
height: auto; height: auto;
pointer-events: none; pointer-events: none;
} }
.caba-lupa-interactive-area { .caba-lupa-interactive-area {
pointer-events: all; pointer-events: all;
cursor: pointer; cursor: pointer;
filter: drop-shadow(0px 2px 4px rgba(0,0,0,0.25)); filter: drop-shadow(0px 2px 4px rgba(0, 0, 0, 0.25));
transition: transform 0.2s ease-in-out; transition: transform 0.2s ease-in-out;
} }
.caba-lupa-interactive-area:hover { .caba-lupa-interactive-area:hover {
filter: brightness(1.15); filter: brightness(1.15);
stroke: #ffffff; stroke: #ffffff;
stroke-width: 0.25px; stroke-width: 0.25px;
} }
.skeleton-fila div { .skeleton-fila div {
background: #f6f7f8; background: #f6f7f8;
background-image: linear-gradient(to right, #f6f7f8 0%, #edeef1 20%, #f6f7f8 40%, #f6f7f8 100%); background-image: linear-gradient(to right, #f6f7f8 0%, #edeef1 20%, #f6f7f8 40%, #f6f7f8 100%);
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: 800px 104px; background-size: 800px 104px;
animation: shimmer 1s linear infinite; animation: shimmer 1s linear infinite;
border-radius: 4px; border-radius: 4px;
} }
.skeleton-logo { width: 65px; height: 65px; } .skeleton-logo {
.skeleton-text { height: 1em; } width: 65px;
.skeleton-bar { height: 20px; margin-top: 4px; } height: 65px;
}
.skeleton-text {
height: 1em;
}
.skeleton-bar {
height: 20px;
margin-top: 4px;
}
/* --- NUEVOS ESTILOS PARA EL TOGGLE MÓVIL --- */
.mobile-view-toggle {
display: none;
/* Oculto por defecto */
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 100;
background-color: rgba(255, 255, 255, 0.9);
border-radius: 30px;
padding: 5px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
gap: 5px;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
}
.mobile-view-toggle .toggle-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
border: none;
background-color: transparent;
border-radius: 25px;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
color: #555;
transition: all 0.2s ease-in-out;
}
.mobile-view-toggle .toggle-btn.active {
background-color: #007bff;
color: white;
}
/* --- ESTILOS PARA LOS BOTONES DE ZOOM DEL MAPA --- */
.zoom-controls-container {
position: absolute;
top: 5px;
right: 10px;
z-index: 30;
/* Debe ser MAYOR que el z-index del header (20) */
display: flex;
flex-direction: column;
gap: 5px;
}
.zoom-btn {
width: 40px;
height: 40px;
background-color: white;
border: 1px solid #ccc;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
transition: background-color 0.2s;
}
.zoom-icon-wrapper {
/* Contenedor del icono */
display: flex;
/* Necesario para que el SVG interno se alinee */
align-items: center;
justify-content: center;
}
.zoom-icon-wrapper svg {
/* Apunta directamente al SVG del icono */
width: 20px;
height: 20px;
color: #333;
}
.zoom-btn.disabled {
opacity: 0.5;
/* Lo hace semitransparente */
cursor: not-allowed;
/* Muestra el cursor de "no permitido" */
}
.zoom-btn:hover {
background-color: #f0f0f0;
}
/* --- ESTILOS DE CURSOR PARA EL ARRASTRE DEL MAPA --- */
.map-locked .rsm-geography {
cursor: pointer;
/* Cursor normal de clic */
}
.map-pannable .rsm-geography {
cursor: grab;
/* Indica que el mapa se puede arrastrar */
}
.map-pannable .rsm-geography:active {
cursor: grabbing;
/* Indica que se está arrastrando */
}
/* --- MEDIA QUERY PARA RESPONSIVE (ENFOQUE FINAL CON CAPAS) --- */
@media (max-width: 800px) {
/* --- CONFIGURACIÓN GENERAL --- */
html,
body {
width: 100%;
overflow-x: hidden;
}
/* Controles de vista y header (sin cambios) */
.mobile-view-toggle {
display: flex;
}
.panel-toggle-btn {
display: none;
}
.header-top-row {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.categoria-selector {
width: 100%;
}
/* --- NUEVO LAYOUT DE CAPAS SUPERPUESTAS --- */
/* 1. El contenedor principal ahora es un ancla de posicionamiento */
.panel-main-content {
position: relative;
/* Clave para que los hijos se posicionen dentro de él */
height: calc(100vh - 200px);
/* Le damos una altura fija y predecible */
min-height: 450px;
}
/* 2. Ambas columnas son capas que ocupan el 100% del espacio del padre */
.mapa-column,
.resultados-column {
position: absolute;
left: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
transition: opacity 0.2s ease-in-out, visibility 0.2s ease-in-out;
}
/* Le damos un estilo específico a la columna del mapa para subirla */
.mapa-column {
top: -50px;
left: -10px;
z-index: 10;
}
/* Hacemos que la columna de resultados pueda tener su propio scroll... */
.resultados-column {
top: 0;
/* Aseguramos que los resultados se queden en su sitio */
padding: 1rem;
overflow-y: auto;
z-index: 15;
}
/* 3. Lógica de visibilidad: controlamos qué capa está "arriba" */
.panel-main-content.mobile-view-mapa .resultados-column {
opacity: 0;
visibility: hidden;
/* Esta es la propiedad clave que ya tenías, pero es importante verificarla */
pointer-events: none;
/* Asegura que la capa oculta no bloquee el mapa */
}
.panel-main-content.mobile-view-resultados .mapa-column {
opacity: 0;
visibility: hidden;
pointer-events: none;
}
/* Hacemos que la columna de resultados pueda tener su propio scroll si el contenido es largo */
.resultados-column {
padding: 1rem;
overflow-y: auto;
}
/* 4. Estilos de los resultados (ya estaban bien, se mantienen) */
.partido-fila {
display: flex;
align-items: center;
gap: 1rem;
}
.partido-logo {
width: 60px;
height: 60px;
flex-shrink: 0;
}
.partido-main-content {
flex-grow: 1;
min-width: 0;
}
.partido-top-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.partido-info-wrapper {
min-width: 0;
}
.partido-nombre {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.partido-stats {
text-align: right;
flex-shrink: 0;
padding-left: 0.5rem;
}
/* --- AJUSTE DE TAMAÑO DEL CONTENEDOR INTERNO DEL MAPA --- */
.mapa-column .mapa-componente-container,
.mapa-column .mapa-render-area {
height: 100%;
}
/* Margen de seguridad para el último elemento de la lista de resultados */
.panel-partidos-container .partido-fila:last-child {
margin-bottom: 90px;
}
.zoom-controls-container {
top: 55px;
}
.mapa-volver-btn {
top: 55px;
left: 12px;
}
/* --- MEDIA QUERY ADICIONAL PARA MÓVIL EN HORIZONTAL --- */
/* Se activa cuando la pantalla es ancha pero no muy alta, como un teléfono en landscape */
@media (max-width: 900px) and (orientation: landscape) {
/* Layout flexible de dos columnas */
.panel-main-content {
display: flex;
flex-direction: row;
position: static;
height: 85vh;
min-height: 400px;
}
.mapa-column,
.resultados-column {
position: static;
/* Desactivamos el posicionamiento absoluto */
height: auto;
width: auto;
opacity: 1;
visibility: visible;
pointer-events: auto;
flex: 3;
overflow-y: auto;
/* Permitimos que la columna de resultados tenga su propio scroll */
}
.resultados-column {
flex: 2;
min-width: 300px;
/* Un mínimo para que no se comprima */
}
/* 3. Ocultamos los botones de cambio de vista móvil, ya que ambas se ven */
.mobile-view-toggle {
display: none;
}
/* 4. Mostramos de nuevo el botón lateral para colapsar el panel de resultados */
.panel-toggle-btn {
display: flex;
}
}
}

View File

@@ -8,6 +8,8 @@ import { Breadcrumbs } from './components/Breadcrumbs';
import './PanelNacional.css'; import './PanelNacional.css';
import Select from 'react-select'; import Select from 'react-select';
import type { PanelElectoralDto } from '../../../types/types'; import type { PanelElectoralDto } from '../../../types/types';
import { FiMap, FiList } from 'react-icons/fi';
import { useMediaQuery } from './hooks/useMediaQuery';
interface PanelNacionalWidgetProps { interface PanelNacionalWidgetProps {
eleccionId: number; eleccionId: number;
@@ -38,6 +40,9 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) =>
const [ambitoActual, setAmbitoActual] = useState<AmbitoState>({ id: null, nivel: 'pais', nombre: 'Argentina', provinciaDistritoId: null }); const [ambitoActual, setAmbitoActual] = useState<AmbitoState>({ id: null, nivel: 'pais', nombre: 'Argentina', provinciaDistritoId: null });
const [categoriaId, setCategoriaId] = useState<number>(2); const [categoriaId, setCategoriaId] = useState<number>(2);
const [isPanelOpen, setIsPanelOpen] = useState(true); const [isPanelOpen, setIsPanelOpen] = useState(true);
const [mobileView, setMobileView] = useState<'mapa' | 'resultados'>('mapa');
// --- DETECCIÓN DE VISTA MÓVIL ---
const isMobile = useMediaQuery('(max-width: 800px)');
const handleAmbitoSelect = (nuevoAmbitoId: string, nuevoNivel: 'provincia' | 'municipio', nuevoNombre: string) => { const handleAmbitoSelect = (nuevoAmbitoId: string, nuevoNivel: 'provincia' | 'municipio', nuevoNombre: string) => {
setAmbitoActual(prev => ({ setAmbitoActual(prev => ({
@@ -76,12 +81,14 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) =>
<div className="panel-nacional-container"> <div className="panel-nacional-container">
<header className="panel-header"> <header className="panel-header">
<div className="header-top-row"> <div className="header-top-row">
<h1>Resultados elecciones {ambitoActual.nombre}</h1> <h1>Legislativas Argentina 2025</h1>
<Select <Select
options={CATEGORIAS_NACIONALES} options={CATEGORIAS_NACIONALES}
value={selectedCategoria} value={selectedCategoria}
onChange={(option) => option && setCategoriaId(option.value)} onChange={(option) => option && setCategoriaId(option.value)}
className="categoria-selector" className="categoria-selector"
classNamePrefix="categoria-selector"
isSearchable={false}
/> />
</div> </div>
<Breadcrumbs <Breadcrumbs
@@ -92,7 +99,7 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) =>
onVolverProvincia={handleVolverAProvincia} onVolverProvincia={handleVolverAProvincia}
/> />
</header> </header>
<main className={`panel-main-content ${!isPanelOpen ? 'panel-collapsed' : ''}`}> <main className={`panel-main-content ${!isPanelOpen ? 'panel-collapsed' : ''} ${isMobile ? `mobile-view-${mobileView}` : ''}`}>
<div className="mapa-column"> <div className="mapa-column">
<button className="panel-toggle-btn" onClick={() => setIsPanelOpen(!isPanelOpen)} title={isPanelOpen ? "Ocultar panel" : "Mostrar panel"}> <button className="panel-toggle-btn" onClick={() => setIsPanelOpen(!isPanelOpen)} title={isPanelOpen ? "Ocultar panel" : "Mostrar panel"}>
{isPanelOpen ? '' : ''} {isPanelOpen ? '' : ''}
@@ -107,6 +114,7 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) =>
provinciaDistritoId={ambitoActual.provinciaDistritoId ?? null} provinciaDistritoId={ambitoActual.provinciaDistritoId ?? null}
onAmbitoSelect={handleAmbitoSelect} onAmbitoSelect={handleAmbitoSelect}
onVolver={ambitoActual.nivel === 'municipio' ? handleVolverAProvincia : handleResetToPais} onVolver={ambitoActual.nivel === 'municipio' ? handleVolverAProvincia : handleResetToPais}
isMobileView={isMobile}
/> />
</Suspense> </Suspense>
</div> </div>
@@ -120,6 +128,24 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) =>
</Suspense> </Suspense>
</div> </div>
</main> </main>
{/* --- NUEVO CONTROLADOR DE VISTA PARA MÓVIL --- */}
<div className="mobile-view-toggle">
<button
className={`toggle-btn ${mobileView === 'mapa' ? 'active' : ''}`}
onClick={() => setMobileView('mapa')}
>
<FiMap />
<span>Mapa</span>
</button>
<button
className={`toggle-btn ${mobileView === 'resultados' ? 'active' : ''}`}
onClick={() => setMobileView('resultados')}
>
<FiList />
<span>Resultados</span>
</button>
</div>
</div> </div>
); );
}; };

View File

@@ -9,6 +9,7 @@ import { API_BASE_URL, assetBaseUrl } from '../../../../apiService';
import type { ResultadoMapaDto, AmbitoGeography } from '../../../../types/types'; import type { ResultadoMapaDto, AmbitoGeography } from '../../../../types/types';
import { MapaProvincial } from './MapaProvincial'; import { MapaProvincial } from './MapaProvincial';
import { CabaLupa } from './CabaLupa'; import { CabaLupa } from './CabaLupa';
import { BiZoomIn, BiZoomOut } from "react-icons/bi";
const DEFAULT_MAP_COLOR = '#E0E0E0'; const DEFAULT_MAP_COLOR = '#E0E0E0';
const FADED_BACKGROUND_COLOR = '#F0F0F0'; const FADED_BACKGROUND_COLOR = '#F0F0F0';
@@ -17,7 +18,7 @@ const normalizarTexto = (texto: string = '') => texto.trim().toUpperCase().norma
type PointTuple = [number, number]; type PointTuple = [number, number];
const PROVINCE_VIEW_CONFIG: Record<string, { center: PointTuple; zoom: number }> = { const PROVINCE_VIEW_CONFIG: Record<string, { center: PointTuple; zoom: number }> = {
"BUENOS AIRES": { center: [-60.5, -37.3], zoom: 5.5 }, "BUENOS AIRES": { center: [-60.5, -37.3], zoom: 5 },
"SANTA CRUZ": { center: [-69.5, -48.8], zoom: 5 }, "SANTA CRUZ": { center: [-69.5, -48.8], zoom: 5 },
"CIUDAD AUTONOMA DE BUENOS AIRES": { center: [-58.45, -34.6], zoom: 85 }, "CIUDAD AUTONOMA DE BUENOS AIRES": { center: [-58.45, -34.6], zoom: 85 },
"CHUBUT": { center: [-68.5, -44.5], zoom: 5.5 }, "CHUBUT": { center: [-68.5, -44.5], zoom: 5.5 },
@@ -31,7 +32,6 @@ const LUPA_SIZE_RATIO = 0.2;
const MIN_LUPA_SIZE_PX = 100; const MIN_LUPA_SIZE_PX = 100;
const MAX_LUPA_SIZE_PX = 180; const MAX_LUPA_SIZE_PX = 180;
interface MapaNacionalProps { interface MapaNacionalProps {
eleccionId: number; eleccionId: number;
categoriaId: number; categoriaId: number;
@@ -41,10 +41,19 @@ interface MapaNacionalProps {
provinciaDistritoId: string | null; provinciaDistritoId: string | null;
onAmbitoSelect: (ambitoId: string, nivel: 'provincia' | 'municipio', nombre: string) => void; onAmbitoSelect: (ambitoId: string, nivel: 'provincia' | 'municipio', nombre: string) => void;
onVolver: () => void; onVolver: () => void;
isMobileView: boolean;
} }
export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nombreProvinciaActiva, provinciaDistritoId, onAmbitoSelect, onVolver }: MapaNacionalProps) => { // --- CONFIGURACIONES DEL MAPA ---
const [position, setPosition] = useState({ zoom: 1, center: [-65, -40] as PointTuple }); const desktopProjectionConfig = { scale: 700, center: [-65, -40] as [number, number] };
const mobileProjectionConfig = { scale: 1100, center: [-64, -41] as [number, number] };
export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nombreProvinciaActiva, provinciaDistritoId, onAmbitoSelect, onVolver, isMobileView }: MapaNacionalProps) => {
const [position, setPosition] = useState({
zoom: isMobileView ? 1.5 : 1.05, // 1.5 para móvil, 1.05 para desktop
center: [-65, -40] as PointTuple
});
const initialProvincePositionRef = useRef<{ zoom: number, center: PointTuple } | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
const lupaRef = useRef<HTMLDivElement | null>(null); const lupaRef = useRef<HTMLDivElement | null>(null);
@@ -70,21 +79,33 @@ export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nom
useEffect(() => { useEffect(() => {
if (nivel === 'pais') { if (nivel === 'pais') {
setPosition({ zoom: 1, center: [-65, -40] }); setPosition({
zoom: isMobileView ? 1.4 : 1.05, // 1.5 para móvil, 1.05 para desktop
center: [-65, -40]
});
// Reseteamos el ref
initialProvincePositionRef.current = null;
} else if (nivel === 'provincia') { } else if (nivel === 'provincia') {
const nombreNormalizado = normalizarTexto(nombreAmbito); const nombreNormalizado = normalizarTexto(nombreAmbito);
const manualConfig = PROVINCE_VIEW_CONFIG[nombreNormalizado]; const manualConfig = PROVINCE_VIEW_CONFIG[nombreNormalizado];
let provinceConfig = { zoom: 7, center: [-65, -40] as PointTuple };
if (manualConfig) { if (manualConfig) {
setPosition(manualConfig); provinceConfig = manualConfig;
} else { } else {
const provinciaGeo = geoDataNacional.objects.provincias.geometries.find((g: any) => normalizarTexto(g.properties.nombre) === nombreNormalizado); const provinciaGeo = geoDataNacional.objects.provincias.geometries.find((g: any) => normalizarTexto(g.properties.nombre) === nombreNormalizado);
if (provinciaGeo) { if (provinciaGeo) {
const provinciaFeature = feature(geoDataNacional, provinciaGeo); const provinciaFeature = feature(geoDataNacional, provinciaGeo);
const centroid = geoCentroid(provinciaFeature); const centroid = geoCentroid(provinciaFeature);
setPosition({ zoom: 7, center: centroid as PointTuple }); provinceConfig = { zoom: 7, center: centroid as PointTuple };
} }
} }
setPosition(provinceConfig);
// --- Guardar el objeto de posición completo en el ref ---
initialProvincePositionRef.current = provinceConfig;
} }
}, [nivel, nombreAmbito, geoDataNacional]); }, [nivel, nombreAmbito, geoDataNacional]);
@@ -104,7 +125,7 @@ export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nom
const calculatedSize = containerRect.width * LUPA_SIZE_RATIO; const calculatedSize = containerRect.width * LUPA_SIZE_RATIO;
const newLupaSize = Math.max(MIN_LUPA_SIZE_PX, Math.min(calculatedSize, MAX_LUPA_SIZE_PX)); const newLupaSize = Math.max(MIN_LUPA_SIZE_PX, Math.min(calculatedSize, MAX_LUPA_SIZE_PX));
const horizontalOffset = newLupaSize * 0.5; const horizontalOffset = newLupaSize * 0.5;
const verticalOffset = newLupaSize * 0.2; const verticalOffset = newLupaSize * 0.2;
@@ -132,7 +153,7 @@ export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nom
if (containerRef.current) { if (containerRef.current) {
resizeObserver.observe(containerRef.current); resizeObserver.observe(containerRef.current);
} }
let timerId: NodeJS.Timeout; let timerId: NodeJS.Timeout;
if (initialLoadRef.current && nivel === 'pais') { if (initialLoadRef.current && nivel === 'pais') {
@@ -159,20 +180,93 @@ export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nom
}; };
}, [position, nivel]); }, [position, nivel]);
// --- HANDLERS PARA EL ZOOM ---
const handleZoomIn = () => {
setPosition(prev => ({
...prev,
zoom: Math.min(prev.zoom * 1.8, 100) // Multiplica el zoom actual, con un límite
}));
};
// --- Lógica de reseteo en handleZoomOut ---
const handleZoomOut = () => {
setPosition(prev => {
const newZoom = Math.max(prev.zoom / 1.8, 1);
const initialPos = initialProvincePositionRef.current;
// Si estamos en una provincia Y el nuevo zoom es igual o menor que el inicial...
if (initialPos && newZoom <= initialPos.zoom) {
// ...reseteamos a la posición inicial guardada (zoom Y centro).
return initialPos;
}
// Si no, solo actualizamos el zoom.
return { ...prev, zoom: newZoom };
});
};
const handleMoveEnd = (newPosition: { coordinates: PointTuple, zoom: number }) => {
// Solo actualizamos el centro (coordenadas), no el zoom, al arrastrar
setPosition(prev => ({ ...prev, center: newPosition.coordinates }));
};
const panEnabled =
//isMobileView &&
nivel === 'provincia' &&
initialProvincePositionRef.current !== null &&
position.zoom > initialProvincePositionRef.current.zoom &&
!nombreMunicipioSeleccionado;
const showZoomControls = nivel === 'provincia';
// --- FUNCIÓN DE FILTRO ---
const filterInteractionEvents = (event: any) => {
// La librería pasa un objeto de evento que contiene el evento original del navegador.
// Si el evento original es de la rueda del ratón ('wheel'), siempre lo bloqueamos.
if (event.sourceEvent && event.sourceEvent.type === 'wheel') {
return false;
}
// Para cualquier otro evento (arrastre, etc.), la decisión depende de nuestra lógica `panEnabled`.
return panEnabled;
};
// --- LÓGICA PARA DESHABILITAR EL BOTÓN ---
const isZoomOutDisabled =
(nivel === 'provincia' && initialProvincePositionRef.current && position.zoom <= initialProvincePositionRef.current.zoom) ||
(nivel === 'pais' && position.zoom <= (isMobileView ? 1.4 : 1.05));
return ( return (
<div className="mapa-componente-container" ref={containerRef}> <div className="mapa-componente-container" ref={containerRef}>
{showZoomControls && (
<div className="zoom-controls-container">
<button onClick={handleZoomIn} className="zoom-btn" title="Acercar">
<span className="zoom-icon-wrapper"><BiZoomIn /></span>
</button>
<button
onClick={handleZoomOut}
className={`zoom-btn ${isZoomOutDisabled ? 'disabled' : ''}`}
title="Alejar"
disabled={isZoomOutDisabled}
>
<span className="zoom-icon-wrapper"><BiZoomOut /></span>
</button>
</div>
)}
{nivel !== 'pais' && <button onClick={onVolver} className="mapa-volver-btn"> Volver</button>} {nivel !== 'pais' && <button onClick={onVolver} className="mapa-volver-btn"> Volver</button>}
<div className="mapa-render-area"> <div className="mapa-render-area">
<ComposableMap <ComposableMap
projection="geoMercator" projection="geoMercator"
projectionConfig={{ scale: 700, center: [-65, -40] }} projectionConfig={isMobileView ? mobileProjectionConfig : desktopProjectionConfig}
style={{ width: "100%", height: "100%" }} style={{ width: "100%", height: "100%" }}
> >
<ZoomableGroup <ZoomableGroup
center={position.center} center={position.center}
zoom={position.zoom} zoom={position.zoom}
filterZoomEvent={() => false} onMoveEnd={handleMoveEnd}
filterZoomEvent={filterInteractionEvents}
> >
<Geographies geography={geoDataNacional}> <Geographies geography={geoDataNacional}>
{({ geographies }: { geographies: AmbitoGeography[] }) => geographies.map((geo) => { {({ geographies }: { geographies: AmbitoGeography[] }) => geographies.map((geo) => {
@@ -233,7 +327,7 @@ export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nom
</div> </div>
)} )}
<Tooltip id="mapa-tooltip" /> <Tooltip id="mapa-tooltip" key={nivel} />
</div> </div>
); );
}; };

View File

@@ -0,0 +1,31 @@
// src/hooks/useMediaQuery.ts
import { useState, useEffect } from 'react';
export const useMediaQuery = (query: string): boolean => {
const [matches, setMatches] = useState(false);
useEffect(() => {
const media = window.matchMedia(query);
if (media.matches !== matches) {
setMatches(media.matches);
}
const listener = () => setMatches(media.matches);
// Deprecated 'addListener' for broader browser support, 'addEventListener' is preferred.
if (media.addEventListener) {
media.addEventListener('change', listener);
} else {
media.addListener(listener);
}
return () => {
if (media.removeEventListener) {
media.removeEventListener('change', listener);
} else {
media.removeListener(listener);
}
};
}, [matches, query]);
return matches;
};

View File

@@ -14,7 +14,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Api")] [assembly: System.Reflection.AssemblyCompanyAttribute("Elecciones.Api")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+3a8f64bf854b2bdf66d83e503aaacc7dca77138e")] [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+7d2922aaeb546ad280af958d81394ab1715a3267")]
[assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Api")] [assembly: System.Reflection.AssemblyProductAttribute("Elecciones.Api")]
[assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Api")] [assembly: System.Reflection.AssemblyTitleAttribute("Elecciones.Api")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]