Refinamiento de Funciones y Estética de Mapa
This commit is contained in:
@@ -5,7 +5,7 @@ import './DevAppStyle.css'
|
||||
export const DevAppLegislativas = () => {
|
||||
return (
|
||||
<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.
|
||||
Para tus datos de prueba provinciales, este ID es 1. */}
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
.panel-header {
|
||||
padding: 1rem 1.5rem;
|
||||
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 */
|
||||
@@ -29,20 +35,89 @@
|
||||
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 --- */
|
||||
|
||||
.breadcrumbs-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem; /* Espacio entre elementos */
|
||||
font-size: 0.9rem;
|
||||
gap: 0.5rem;
|
||||
/* Espacio entre elementos */
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.breadcrumb-item, .breadcrumb-item-actual {
|
||||
.breadcrumb-item,
|
||||
.breadcrumb-item-actual {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -62,7 +137,8 @@
|
||||
.breadcrumb-item-actual {
|
||||
background-color: transparent;
|
||||
color: #000;
|
||||
font-weight: 700; /* Más peso para el nivel actual */
|
||||
font-weight: 700;
|
||||
/* Más peso para el nivel actual */
|
||||
}
|
||||
|
||||
.breadcrumb-icon {
|
||||
@@ -71,7 +147,8 @@
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
color: #a0a0a0; /* Color sutil para el separador */
|
||||
color: #a0a0a0;
|
||||
/* Color sutil para el separador */
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
@@ -85,18 +162,21 @@
|
||||
|
||||
/* Columna del mapa */
|
||||
.mapa-column {
|
||||
flex: 2; /* Por defecto, ocupa 2/3 del espacio */
|
||||
flex: 2;
|
||||
/* Por defecto, ocupa 2/3 del espacio */
|
||||
position: relative;
|
||||
transition: flex 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
/* Columna de resultados */
|
||||
.resultados-column {
|
||||
flex: 1; /* Por defecto, ocupa 1/3 */
|
||||
flex: 1;
|
||||
/* Por defecto, ocupa 1/3 */
|
||||
overflow-y: auto;
|
||||
padding: 1.5rem;
|
||||
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 --- */
|
||||
@@ -105,8 +185,8 @@
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem 0;
|
||||
border-bottom: 1px solid #f0f0f0; /* Separador sutil */
|
||||
border-left: 5px solid; /* El color se aplica inline */
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
border-left: 5px solid;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
@@ -125,15 +205,22 @@
|
||||
|
||||
.partido-main-content {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem; /* Espacio entre la fila superior y la barra */
|
||||
display: grid;
|
||||
/* CAMBIO: De flex a grid */
|
||||
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 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start; /* Alinea los elementos al tope */
|
||||
/* Hacemos que este contenedor sea "invisible" para el grid,
|
||||
promoviendo a sus hijos (info y stats) a la cuadrícula principal. */
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.partido-info-wrapper {
|
||||
@@ -142,7 +229,7 @@
|
||||
}
|
||||
|
||||
.partido-nombre {
|
||||
font-weight: 500;
|
||||
font-weight: 800;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -156,7 +243,8 @@
|
||||
.partido-stats {
|
||||
flex-shrink: 0;
|
||||
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 {
|
||||
@@ -175,6 +263,8 @@
|
||||
height: 20px;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
grid-column: 1 / 3;
|
||||
/* Le indicamos que ocupe ambas columnas (de la línea 1 a la 3) */
|
||||
}
|
||||
|
||||
.partido-barra-foreground {
|
||||
@@ -182,6 +272,7 @@
|
||||
border-radius: 4px;
|
||||
transition: width 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
/* ------------------------------------------- */
|
||||
|
||||
.panel-estado-recuento {
|
||||
@@ -212,13 +303,15 @@
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mapa-render-area {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mapa-volver-btn {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
@@ -231,12 +324,15 @@
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.rsm-zoomable-group {
|
||||
transition: transform 0.75s ease-in-out;
|
||||
transition: transform 0.75s ease-in-out;
|
||||
}
|
||||
|
||||
.panel-main-content.panel-collapsed .mapa-column {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.panel-main-content.panel-collapsed .resultados-column {
|
||||
flex-basis: 0;
|
||||
min-width: 0;
|
||||
@@ -244,6 +340,7 @@
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-toggle-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
@@ -256,107 +353,435 @@
|
||||
background-color: white;
|
||||
border-radius: 4px 0 0 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.3rem;
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
display: flex;
|
||||
align-items: 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;
|
||||
}
|
||||
|
||||
.panel-toggle-btn:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.rsm-geography {
|
||||
cursor: pointer;
|
||||
stroke: #000000;
|
||||
stroke-width: 0.25px;
|
||||
outline: none;
|
||||
transition: filter 0.2s ease-in-out;
|
||||
cursor: pointer;
|
||||
stroke: #000000;
|
||||
stroke-width: 0.25px;
|
||||
outline: none;
|
||||
transition: filter 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.rsm-geography:not(.selected):hover {
|
||||
filter: brightness(1.15);
|
||||
stroke: #ffffff;
|
||||
stroke-width: 0.25px;
|
||||
filter: brightness(1.25); /* Mantenemos el brillo */
|
||||
stroke: #ffffff; /* Color del borde a blanco */
|
||||
stroke-width: 0.25px;
|
||||
paint-order: stroke; /* Asegura que el borde se dibuje encima del relleno */
|
||||
}
|
||||
|
||||
.rsm-geography.selected {
|
||||
stroke: #000000;
|
||||
stroke-width: 0.25px;
|
||||
filter: none;
|
||||
pointer-events: none;
|
||||
stroke: #000000;
|
||||
stroke-width: 0.25px;
|
||||
filter: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.rsm-geography-faded,
|
||||
.rsm-geography-faded-municipality {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.caba-comuna-geography {
|
||||
stroke: #000000;
|
||||
stroke-width: 0.05px;
|
||||
stroke: #000000;
|
||||
stroke-width: 0.05px;
|
||||
}
|
||||
|
||||
.caba-comuna-geography:not(.selected):hover {
|
||||
stroke: #000000;
|
||||
stroke-width: 0.055px;
|
||||
filter: brightness(1.25);
|
||||
stroke: #000000;
|
||||
stroke-width: 0.055px;
|
||||
filter: brightness(1.25);
|
||||
}
|
||||
|
||||
.transition-spinner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.transition-spinner::after {
|
||||
content: '';
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 5px solid rgba(0, 0, 0, 0.2);
|
||||
border-top-color: #007bff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
content: '';
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 5px solid rgba(0, 0, 0, 0.2);
|
||||
border-top-color: #007bff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.caba-magnifier-container {
|
||||
position: absolute;
|
||||
height: auto;
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
height: auto;
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.caba-lupa-svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
pointer-events: none;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.caba-lupa-interactive-area {
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
filter: drop-shadow(0px 2px 4px rgba(0,0,0,0.25));
|
||||
transition: transform 0.2s ease-in-out;
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
filter: drop-shadow(0px 2px 4px rgba(0, 0, 0, 0.25));
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.caba-lupa-interactive-area:hover {
|
||||
filter: brightness(1.15);
|
||||
stroke: #ffffff;
|
||||
stroke-width: 0.25px;
|
||||
filter: brightness(1.15);
|
||||
stroke: #ffffff;
|
||||
stroke-width: 0.25px;
|
||||
}
|
||||
|
||||
.skeleton-fila div {
|
||||
background: #f6f7f8;
|
||||
background-image: linear-gradient(to right, #f6f7f8 0%, #edeef1 20%, #f6f7f8 40%, #f6f7f8 100%);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 800px 104px;
|
||||
background-size: 800px 104px;
|
||||
animation: shimmer 1s linear infinite;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.skeleton-logo { width: 65px; height: 65px; }
|
||||
.skeleton-text { height: 1em; }
|
||||
.skeleton-bar { height: 20px; margin-top: 4px; }
|
||||
.skeleton-logo {
|
||||
width: 65px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import { Breadcrumbs } from './components/Breadcrumbs';
|
||||
import './PanelNacional.css';
|
||||
import Select from 'react-select';
|
||||
import type { PanelElectoralDto } from '../../../types/types';
|
||||
import { FiMap, FiList } from 'react-icons/fi';
|
||||
import { useMediaQuery } from './hooks/useMediaQuery';
|
||||
|
||||
interface PanelNacionalWidgetProps {
|
||||
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 [categoriaId, setCategoriaId] = useState<number>(2);
|
||||
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) => {
|
||||
setAmbitoActual(prev => ({
|
||||
@@ -76,12 +81,14 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) =>
|
||||
<div className="panel-nacional-container">
|
||||
<header className="panel-header">
|
||||
<div className="header-top-row">
|
||||
<h1>Resultados elecciones {ambitoActual.nombre}</h1>
|
||||
<h1>Legislativas Argentina 2025</h1>
|
||||
<Select
|
||||
options={CATEGORIAS_NACIONALES}
|
||||
value={selectedCategoria}
|
||||
onChange={(option) => option && setCategoriaId(option.value)}
|
||||
className="categoria-selector"
|
||||
classNamePrefix="categoria-selector"
|
||||
isSearchable={false}
|
||||
/>
|
||||
</div>
|
||||
<Breadcrumbs
|
||||
@@ -92,7 +99,7 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) =>
|
||||
onVolverProvincia={handleVolverAProvincia}
|
||||
/>
|
||||
</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">
|
||||
<button className="panel-toggle-btn" onClick={() => setIsPanelOpen(!isPanelOpen)} title={isPanelOpen ? "Ocultar panel" : "Mostrar panel"}>
|
||||
{isPanelOpen ? '›' : '‹'}
|
||||
@@ -107,6 +114,7 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) =>
|
||||
provinciaDistritoId={ambitoActual.provinciaDistritoId ?? null}
|
||||
onAmbitoSelect={handleAmbitoSelect}
|
||||
onVolver={ambitoActual.nivel === 'municipio' ? handleVolverAProvincia : handleResetToPais}
|
||||
isMobileView={isMobile}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
@@ -120,6 +128,24 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) =>
|
||||
</Suspense>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import { API_BASE_URL, assetBaseUrl } from '../../../../apiService';
|
||||
import type { ResultadoMapaDto, AmbitoGeography } from '../../../../types/types';
|
||||
import { MapaProvincial } from './MapaProvincial';
|
||||
import { CabaLupa } from './CabaLupa';
|
||||
import { BiZoomIn, BiZoomOut } from "react-icons/bi";
|
||||
|
||||
const DEFAULT_MAP_COLOR = '#E0E0E0';
|
||||
const FADED_BACKGROUND_COLOR = '#F0F0F0';
|
||||
@@ -17,7 +18,7 @@ const normalizarTexto = (texto: string = '') => texto.trim().toUpperCase().norma
|
||||
type PointTuple = [number, 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 },
|
||||
"CIUDAD AUTONOMA DE BUENOS AIRES": { center: [-58.45, -34.6], zoom: 85 },
|
||||
"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 MAX_LUPA_SIZE_PX = 180;
|
||||
|
||||
|
||||
interface MapaNacionalProps {
|
||||
eleccionId: number;
|
||||
categoriaId: number;
|
||||
@@ -41,10 +41,19 @@ interface MapaNacionalProps {
|
||||
provinciaDistritoId: string | null;
|
||||
onAmbitoSelect: (ambitoId: string, nivel: 'provincia' | 'municipio', nombre: string) => void;
|
||||
onVolver: () => void;
|
||||
isMobileView: boolean;
|
||||
}
|
||||
|
||||
export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nombreProvinciaActiva, provinciaDistritoId, onAmbitoSelect, onVolver }: MapaNacionalProps) => {
|
||||
const [position, setPosition] = useState({ zoom: 1, center: [-65, -40] as PointTuple });
|
||||
// --- CONFIGURACIONES DEL MAPA ---
|
||||
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 lupaRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -70,21 +79,33 @@ export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nom
|
||||
|
||||
useEffect(() => {
|
||||
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') {
|
||||
const nombreNormalizado = normalizarTexto(nombreAmbito);
|
||||
const manualConfig = PROVINCE_VIEW_CONFIG[nombreNormalizado];
|
||||
|
||||
let provinceConfig = { zoom: 7, center: [-65, -40] as PointTuple };
|
||||
|
||||
if (manualConfig) {
|
||||
setPosition(manualConfig);
|
||||
provinceConfig = manualConfig;
|
||||
} else {
|
||||
const provinciaGeo = geoDataNacional.objects.provincias.geometries.find((g: any) => normalizarTexto(g.properties.nombre) === nombreNormalizado);
|
||||
if (provinciaGeo) {
|
||||
const provinciaFeature = feature(geoDataNacional, provinciaGeo);
|
||||
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]);
|
||||
|
||||
@@ -104,7 +125,7 @@ export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nom
|
||||
|
||||
const calculatedSize = containerRect.width * LUPA_SIZE_RATIO;
|
||||
const newLupaSize = Math.max(MIN_LUPA_SIZE_PX, Math.min(calculatedSize, MAX_LUPA_SIZE_PX));
|
||||
|
||||
|
||||
const horizontalOffset = newLupaSize * 0.5;
|
||||
const verticalOffset = newLupaSize * 0.2;
|
||||
|
||||
@@ -132,7 +153,7 @@ export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nom
|
||||
if (containerRef.current) {
|
||||
resizeObserver.observe(containerRef.current);
|
||||
}
|
||||
|
||||
|
||||
let timerId: NodeJS.Timeout;
|
||||
|
||||
if (initialLoadRef.current && nivel === 'pais') {
|
||||
@@ -159,20 +180,93 @@ export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nom
|
||||
};
|
||||
}, [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 (
|
||||
<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>}
|
||||
|
||||
<div className="mapa-render-area">
|
||||
<ComposableMap
|
||||
projection="geoMercator"
|
||||
projectionConfig={{ scale: 700, center: [-65, -40] }}
|
||||
projectionConfig={isMobileView ? mobileProjectionConfig : desktopProjectionConfig}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
>
|
||||
<ZoomableGroup
|
||||
center={position.center}
|
||||
zoom={position.zoom}
|
||||
filterZoomEvent={() => false}
|
||||
onMoveEnd={handleMoveEnd}
|
||||
filterZoomEvent={filterInteractionEvents}
|
||||
>
|
||||
<Geographies geography={geoDataNacional}>
|
||||
{({ geographies }: { geographies: AmbitoGeography[] }) => geographies.map((geo) => {
|
||||
@@ -233,7 +327,7 @@ export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nom
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Tooltip id="mapa-tooltip" />
|
||||
<Tooltip id="mapa-tooltip" key={nivel} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user