From 3750d1a56d3311ec92c79dc6cb564a0b8a68239c Mon Sep 17 00:00:00 2001 From: dmolinari Date: Sat, 20 Sep 2025 22:31:11 -0300 Subject: [PATCH] =?UTF-8?q?Refinamiento=20de=20Funciones=20y=20Est=C3=A9ti?= =?UTF-8?q?ca=20de=20Mapa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../legislativas/DevAppLegislativas.tsx | 2 +- .../legislativas/nacionales/PanelNacional.css | 589 +++++++++++++++--- .../nacionales/PanelNacionalWidget.tsx | 30 +- .../nacionales/components/MapaNacional.tsx | 118 +++- .../nacionales/hooks/useMediaQuery.ts | 31 + .../net9.0/Elecciones.Api.AssemblyInfo.cs | 2 +- 6 files changed, 674 insertions(+), 98 deletions(-) create mode 100644 Elecciones-Web/frontend/src/features/legislativas/nacionales/hooks/useMediaQuery.ts diff --git a/Elecciones-Web/frontend/src/features/legislativas/DevAppLegislativas.tsx b/Elecciones-Web/frontend/src/features/legislativas/DevAppLegislativas.tsx index 2570e13..9a3512f 100644 --- a/Elecciones-Web/frontend/src/features/legislativas/DevAppLegislativas.tsx +++ b/Elecciones-Web/frontend/src/features/legislativas/DevAppLegislativas.tsx @@ -5,7 +5,7 @@ import './DevAppStyle.css' export const DevAppLegislativas = () => { return (
-

Il visualizzatore di widget - Elecciones Nacionales 2025

+

Visor de Widgets

{/* Le pasamos el ID de la elección que queremos visualizar. Para tus datos de prueba provinciales, este ID es 1. */} diff --git a/Elecciones-Web/frontend/src/features/legislativas/nacionales/PanelNacional.css b/Elecciones-Web/frontend/src/features/legislativas/nacionales/PanelNacional.css index ff9b324..fa4a518 100644 --- a/Elecciones-Web/frontend/src/features/legislativas/nacionales/PanelNacional.css +++ b/Elecciones-Web/frontend/src/features/legislativas/nacionales/PanelNacional.css @@ -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; + } + } +} \ No newline at end of file diff --git a/Elecciones-Web/frontend/src/features/legislativas/nacionales/PanelNacionalWidget.tsx b/Elecciones-Web/frontend/src/features/legislativas/nacionales/PanelNacionalWidget.tsx index 0773b17..0b1d824 100644 --- a/Elecciones-Web/frontend/src/features/legislativas/nacionales/PanelNacionalWidget.tsx +++ b/Elecciones-Web/frontend/src/features/legislativas/nacionales/PanelNacionalWidget.tsx @@ -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({ id: null, nivel: 'pais', nombre: 'Argentina', provinciaDistritoId: null }); const [categoriaId, setCategoriaId] = useState(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) =>
-

Resultados elecciones {ambitoActual.nombre}

+

Legislativas Argentina 2025