Refinamiento de Funciones y Estética de Mapa
This commit is contained in:
		| @@ -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. */} | ||||||
|   | |||||||
| @@ -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,95 +353,112 @@ | |||||||
|   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 { | ||||||
| @@ -356,7 +470,318 @@ | |||||||
|   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; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -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> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| @@ -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]); | ||||||
|  |  | ||||||
| @@ -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> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| @@ -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; | ||||||
|  | }; | ||||||
| @@ -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")] | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user