Pre Refinamiento Movil
This commit is contained in:
		| @@ -12,7 +12,7 @@ | ||||
|   border-bottom: 1px solid #e0e0e0; | ||||
| } | ||||
|  | ||||
| /* Nuevo contenedor para alinear título y selector */ | ||||
| /* Contenedor para alinear título y selector */ | ||||
| .header-top-row { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
| @@ -27,26 +27,55 @@ | ||||
|  | ||||
| .categoria-selector { | ||||
|   min-width: 220px; | ||||
|   /* Ancho del selector */ | ||||
| } | ||||
|  | ||||
| .breadcrumbs { | ||||
| /* --- ESTILOS MODERNOS PARA BREADCRUMBS --- */ | ||||
|  | ||||
| .breadcrumbs-container { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   gap: 0.5rem; /* Espacio entre elementos */ | ||||
|   font-size: 0.9rem; | ||||
|   color: #666; | ||||
| } | ||||
|  | ||||
| .breadcrumb-link { | ||||
|   background: none; | ||||
|   border: none; | ||||
|   color: #007bff; | ||||
| .breadcrumb-item, .breadcrumb-item-actual { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   padding: 0.4rem 0.8rem; | ||||
|   border-radius: 8px; /* Bordes redondeados para efecto píldora */ | ||||
|   transition: background-color 0.2s ease-in-out; | ||||
| } | ||||
|  | ||||
| .breadcrumb-item { | ||||
|   background-color: #f0f0f0; | ||||
|   border: 1px solid #e0e0e0; | ||||
|   color: #333; | ||||
|   cursor: pointer; | ||||
|   padding: 0; | ||||
|   font-weight: 500; | ||||
| } | ||||
|  | ||||
| .breadcrumb-item:hover { | ||||
|   background-color: #e0e0e0; | ||||
|   border-color: #d1d1d1; | ||||
| } | ||||
|  | ||||
| .breadcrumb-item-actual { | ||||
|   background-color: transparent; | ||||
|   color: #000; | ||||
|   font-weight: 700; /* Más peso para el nivel actual */ | ||||
| } | ||||
|  | ||||
| .breadcrumb-icon { | ||||
|   margin-right: 0.4rem; | ||||
|   font-size: 1rem; | ||||
| } | ||||
|  | ||||
| .breadcrumb-separator { | ||||
|   margin: 0 0.5rem; | ||||
|   color: #a0a0a0; /* Color sutil para el separador */ | ||||
|   font-size: 1.2rem; | ||||
| } | ||||
|  | ||||
|  | ||||
| .panel-main-content { | ||||
|   display: flex; | ||||
|   height: 75vh; | ||||
| @@ -70,13 +99,126 @@ | ||||
|   min-width: 320px; /* Un ancho mínimo para que no se comprima demasiado */ | ||||
| } | ||||
|  | ||||
| /* --- NUEVOS ESTILOS --- */ | ||||
| /* --- NUEVO LAYOUT PARA TARJETAS DE PARTIDO --- */ | ||||
| .partido-fila { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   gap: 1rem; | ||||
|   padding: 1rem 0; | ||||
|   border-bottom: 1px solid #f0f0f0; /* Separador sutil */ | ||||
|   border-left: 5px solid; /* El color se aplica inline */ | ||||
|   padding-left: 1rem; | ||||
| } | ||||
|  | ||||
| .partido-logo { | ||||
|   flex-shrink: 0; | ||||
|   width: 75px; | ||||
|   height: 75px; | ||||
| } | ||||
|  | ||||
| .partido-logo img { | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   object-fit: contain; | ||||
|   border-radius: 10%; | ||||
| } | ||||
|  | ||||
| .partido-main-content { | ||||
|   flex-grow: 1; | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   gap: 0.5rem; /* Espacio entre la fila superior y la barra */ | ||||
| } | ||||
|  | ||||
| .partido-top-row { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   align-items: flex-start; /* Alinea los elementos al tope */ | ||||
| } | ||||
|  | ||||
| .partido-info-wrapper { | ||||
|   /* Ocupa el espacio disponible a la izquierda */ | ||||
|   min-width: 0; | ||||
| } | ||||
|  | ||||
| .partido-nombre { | ||||
|   font-weight: 500; | ||||
|   white-space: nowrap; | ||||
|   overflow: hidden; | ||||
|   text-overflow: ellipsis; | ||||
| } | ||||
|  | ||||
| .candidato-nombre { | ||||
|   font-size: 0.85rem; | ||||
|   color: #666; | ||||
| } | ||||
|  | ||||
| .partido-stats { | ||||
|   flex-shrink: 0; | ||||
|   text-align: right; | ||||
|   padding-left: 1rem; /* Espacio para que no se pegue al nombre */ | ||||
| } | ||||
|  | ||||
| .partido-porcentaje { | ||||
|   font-size: 1.5rem; | ||||
|   font-weight: 700; | ||||
|   display: block; | ||||
| } | ||||
|  | ||||
| .partido-votos { | ||||
|   font-size: 1rem; | ||||
|   color: #666; | ||||
|   display: block; | ||||
| } | ||||
|  | ||||
| .partido-barra-background { | ||||
|   height: 20px; | ||||
|   background-color: #f0f0f0; | ||||
|   border-radius: 4px; | ||||
| } | ||||
|  | ||||
| .partido-barra-foreground { | ||||
|   height: 100%; | ||||
|   border-radius: 4px; | ||||
|   transition: width 0.5s ease-in-out; | ||||
| } | ||||
| /* ------------------------------------------- */ | ||||
|  | ||||
| .panel-estado-recuento { | ||||
|   display: flex; | ||||
|   justify-content: space-around; | ||||
|   padding-bottom: 1.5rem; | ||||
|   margin-bottom: 1.5rem; | ||||
|   border-bottom: 1px solid #e0e0e0; | ||||
| } | ||||
|  | ||||
| .estado-item { | ||||
|   width: 100px; | ||||
|   text-align: center; | ||||
| } | ||||
|  | ||||
| .estado-item span { | ||||
|   margin-top: 0.5rem; | ||||
|   font-size: 0.9rem; | ||||
|   color: #666; | ||||
|   display: block; | ||||
| } | ||||
|  | ||||
|  | ||||
| /* --- MAPA Y ELEMENTOS ASOCIADOS (sin cambios) --- */ | ||||
| .mapa-componente-container { | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   position: relative; | ||||
|   overflow: hidden; | ||||
| } | ||||
| .mapa-render-area { | ||||
|     position: absolute; | ||||
|     top: 0; | ||||
|     left: 0; | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
| } | ||||
|  | ||||
| .mapa-volver-btn { | ||||
|   position: absolute; | ||||
|   top: 10px; | ||||
| @@ -89,144 +231,19 @@ | ||||
|   cursor: pointer; | ||||
|   box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | ||||
| } | ||||
|  | ||||
| .partido-fila  { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   margin-bottom: 1rem; | ||||
|   gap: 1rem; /* Añade un espacio entre logo, info y stats */ | ||||
| } | ||||
|  | ||||
| .partido-logo { | ||||
|   flex-shrink: 0; | ||||
|   width: 48px; | ||||
|   height: 48px; | ||||
| } | ||||
|  | ||||
| .partido-logo img { | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   object-fit: contain; | ||||
|   border-radius: 50%; | ||||
| } | ||||
|  | ||||
| .partido-info-wrapper { | ||||
|   flex-grow: 1; /* Permite que esta sección crezca */ | ||||
|   flex-shrink: 1; /* Permite que se encoja si es necesario */ | ||||
|   min-width: 0;   /* <-- TRUCO CLAVE DE FLEXBOX para que text-overflow funcione */ | ||||
| } | ||||
|  | ||||
| .partido-nombre { | ||||
|   font-weight: 500; | ||||
|   display: block; | ||||
|   white-space: nowrap;      /* <-- No permitir que el texto salte de línea */ | ||||
|   overflow: hidden;         /* <-- Ocultar el texto que se desborda */ | ||||
|   text-overflow: ellipsis;  /* <-- Añadir "..." al final */ | ||||
| } | ||||
|  | ||||
| .candidato-nombre { | ||||
|   font-size: 0.85rem; | ||||
|   color: #666; | ||||
|   display: block; | ||||
| } | ||||
|  | ||||
| .partido-barra-background { | ||||
|   height: 15px; | ||||
|   background-color: #f0f0f0; | ||||
|   border-radius: 5px; | ||||
|   margin-top: 4px; | ||||
| } | ||||
|  | ||||
| .partido-barra-foreground { | ||||
|   height: 100%; | ||||
|   border-radius: 4px; | ||||
|   transition: width 0.5s ease-in-out; | ||||
| } | ||||
|  | ||||
| .partido-stats { | ||||
|   flex-shrink: 0; /* <-- MUY IMPORTANTE: Evita que este bloque se encoja */ | ||||
|   text-align: right; | ||||
|   min-width: 100px; /* Asegura que siempre tenga espacio suficiente */ | ||||
| } | ||||
|  | ||||
| .partido-porcentaje { | ||||
|   font-size: 1.2rem; | ||||
|   font-weight: 700; | ||||
|   display: block; | ||||
| } | ||||
|  | ||||
| .partido-votos { | ||||
|   font-size: 0.8rem; | ||||
|   color: #666; | ||||
|   display: block; | ||||
| } | ||||
|  | ||||
| .panel-estado-recuento { | ||||
|   margin-top: auto; | ||||
|   padding-top: 1rem; | ||||
|   border-top: 1px solid #e0e0e0; | ||||
|   display: flex; | ||||
|   justify-content: space-around; | ||||
| } | ||||
|  | ||||
| .estado-item { | ||||
|   text-align: center; | ||||
| } | ||||
|  | ||||
| .estado-item span { | ||||
|   font-size: 0.8rem; | ||||
|   color: #666; | ||||
|   display: block; | ||||
| } | ||||
|  | ||||
| .estado-item strong { | ||||
|   font-size: 1.2rem; | ||||
|   color: #333; | ||||
| } | ||||
|  | ||||
| .rsm-zoomable-group { | ||||
|     transition: transform 0.75s ease-in-out; | ||||
| } | ||||
|  | ||||
| * Contenedor principal del contenido */ | ||||
| .panel-main-content { | ||||
|   display: flex; | ||||
|   height: 70vh; | ||||
|   min-height: 500px; | ||||
|   transition: all 0.5s ease-in-out; /* Transición suave para el layout */ | ||||
| } | ||||
|  | ||||
| /* Columna del mapa */ | ||||
| .mapa-column { | ||||
|   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 */ | ||||
|   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 */ | ||||
| } | ||||
|  | ||||
| /* --- ESTADO COLAPSADO --- */ | ||||
| /* Cuando el panel principal tiene la clase 'panel-collapsed' */ | ||||
| .panel-main-content.panel-collapsed .mapa-column { | ||||
|   flex: 1 1 100%; /* El mapa ocupa todo el ancho */ | ||||
|   flex: 1 1 100%; | ||||
| } | ||||
|  | ||||
| .panel-main-content.panel-collapsed .resultados-column { | ||||
|   flex-basis: 0; | ||||
|   min-width: 0; | ||||
|   max-width: 0; | ||||
|   padding: 0; | ||||
|   overflow: hidden; /* Oculta el contenido para que no se desborde */ | ||||
|   overflow: hidden; | ||||
| } | ||||
|  | ||||
| /* --- Estilo del botón para colapsar --- */ | ||||
| .panel-toggle-btn { | ||||
|   position: absolute; | ||||
|   top: 50%; | ||||
| @@ -248,70 +265,53 @@ | ||||
|   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.2px; | ||||
|     stroke-width: 0.25px; | ||||
|     outline: none; | ||||
|     transition: filter 0.2s ease-in-out, stroke 0.2s ease-in-out, stroke-width 0.2s ease-in-out; | ||||
|     transition: filter 0.2s ease-in-out; | ||||
| } | ||||
|  | ||||
| /* --- ESTADO HOVER (Sutil) --- */ | ||||
| /* Se aplica solo si la geografía NO está seleccionada */ | ||||
| .rsm-geography:not(.selected):hover { | ||||
|     filter: brightness(1.10); | ||||
|     stroke: #0000ff; | ||||
|     stroke-width: 0.5px; | ||||
|     filter: brightness(1.15); | ||||
|     stroke: #ffffff; | ||||
|     stroke-width: 0.25px; | ||||
| } | ||||
|  | ||||
| /* --- ESTADO SELECCIONADO (Foco) --- */ | ||||
| /* Clase que añadiremos desde React para el municipio en foco */ | ||||
| .rsm-geography.selected { | ||||
|     stroke: #0000ff; /* Borde negro para el seleccionado */ | ||||
|     stroke-width: 0.5px; /* <-- Borde más grueso para destacar */ | ||||
|     filter: none; /* Quitamos cualquier otro filtro para que se vea nítido */ | ||||
|     pointer-events: none; /* Desactivamos eventos para que no interfiera el hover */ | ||||
| } | ||||
|  | ||||
| /* Reglas para los mapas atenuados (sin cambios) */ | ||||
| .rsm-geography-faded, | ||||
| .rsm-geography-faded-municipality { | ||||
|     opacity: 0.3; | ||||
|     stroke: #000000; | ||||
|     stroke-width: 0.25px; | ||||
|     filter: none; | ||||
|     pointer-events: none; | ||||
| } | ||||
|  | ||||
| .rsm-geography-faded:hover, | ||||
| .rsm-geography-faded-municipality:hover { | ||||
|     filter: none; | ||||
|     stroke: #FFFFFF; | ||||
|     stroke-width: 0.5px; | ||||
| .rsm-geography-faded, | ||||
| .rsm-geography-faded-municipality { | ||||
|     opacity: 0.5; | ||||
|     pointer-events: none; | ||||
| } | ||||
|  | ||||
| .partido-barra-foreground { | ||||
|   height: 100%; | ||||
|   border-radius: 4px; | ||||
|   transition: width 0.5s ease-in-out;  | ||||
| .caba-comuna-geography { | ||||
|     stroke: #000000; | ||||
|     stroke-width: 0.05px; | ||||
| } | ||||
| .caba-comuna-geography:not(.selected):hover { | ||||
|     stroke: #000000; | ||||
|     stroke-width: 0.055px; | ||||
|     filter: brightness(1.25); | ||||
| } | ||||
|  | ||||
| /* Spinner para la transición entre mapas */ | ||||
| .transition-spinner { | ||||
|     position: absolute; | ||||
|     top: 0; | ||||
|     left: 0; | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     background-color: rgba(255, 255, 255, 0.5); /* Fondo blanco semitransparente */ | ||||
|     background-color: rgba(255, 255, 255, 0.5); | ||||
|     z-index: 20; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
| } | ||||
| /* Estilo del spinner en sí mismo */ | ||||
| .transition-spinner::after { | ||||
|     content: ''; | ||||
|     width: 50px; | ||||
| @@ -321,7 +321,42 @@ | ||||
|     border-radius: 50%; | ||||
|     animation: spin 1s linear infinite; | ||||
| } | ||||
|  | ||||
| @keyframes spin { | ||||
|     to { transform: rotate(360deg); } | ||||
| } | ||||
| } | ||||
| .caba-magnifier-container { | ||||
|     position: absolute; | ||||
|     height: auto; | ||||
|     transform: translate(-50%, -50%); | ||||
|     pointer-events: none; | ||||
| } | ||||
| .caba-lupa-svg { | ||||
|     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; | ||||
| } | ||||
| .caba-lupa-interactive-area:hover { | ||||
|     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;  | ||||
|   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; } | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| // src/features/legislativas/nacionales/PanelNacionalWidget.tsx | ||||
| import { useMemo, useState, Suspense } from 'react'; | ||||
| import { useSuspenseQuery } from '@tanstack/react-query'; // <-- CAMBIO CLAVE | ||||
| import { useSuspenseQuery } from '@tanstack/react-query'; | ||||
| import { getPanelElectoral } from '../../../apiService'; | ||||
| import { MapaNacional } from './components/MapaNacional'; | ||||
| import { PanelResultados } from './components/PanelResultados'; | ||||
| @@ -26,20 +26,12 @@ const CATEGORIAS_NACIONALES = [ | ||||
|   { value: 1, label: 'Senadores Nacionales' }, | ||||
| ]; | ||||
|  | ||||
| // Creamos un componente interno para poder usar Suspense correctamente | ||||
| const PanelContenido = ({ eleccionId, ambitoActual, categoriaId }: { eleccionId: number, ambitoActual: AmbitoState, categoriaId: number }) => { | ||||
|   // Este hook ahora suspenderá el renderizado si los datos no están listos | ||||
|   const { data } = useSuspenseQuery<PanelElectoralDto>({ | ||||
|     queryKey: ['panelElectoral', eleccionId, ambitoActual.id, categoriaId], | ||||
|     queryFn: () => getPanelElectoral(eleccionId, ambitoActual.id, categoriaId), | ||||
|   }); | ||||
|  | ||||
|   return ( | ||||
|     <PanelResultados | ||||
|       resultados={data.resultadosPanel} | ||||
|       estadoRecuento={data.estadoRecuento} | ||||
|     /> | ||||
|   ); | ||||
|   return <PanelResultados resultados={data.resultadosPanel} estadoRecuento={data.estadoRecuento} />; | ||||
| }; | ||||
|  | ||||
| export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) => { | ||||
| @@ -52,7 +44,7 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) => | ||||
|       id: nuevoAmbitoId, | ||||
|       nivel: nuevoNivel, | ||||
|       nombre: nuevoNombre, | ||||
|       provinciaNombre: nuevoNivel === 'municipio' ? prev.nombre : (nuevoNivel === 'provincia' ? nuevoNombre : undefined), | ||||
|       provinciaNombre: nuevoNivel === 'municipio' ? prev.provinciaNombre : (nuevoNivel === 'provincia' ? nuevoNombre : undefined), | ||||
|       provinciaDistritoId: nuevoNivel === 'provincia' ? nuevoAmbitoId : prev.provinciaDistritoId | ||||
|     })); | ||||
|   }; | ||||
| @@ -67,7 +59,8 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) => | ||||
|         id: ambitoActual.provinciaDistritoId, | ||||
|         nivel: 'provincia', | ||||
|         nombre: ambitoActual.provinciaNombre, | ||||
|         provinciaDistritoId: ambitoActual.provinciaDistritoId | ||||
|         provinciaDistritoId: ambitoActual.provinciaDistritoId, | ||||
|         provinciaNombre: ambitoActual.provinciaNombre, | ||||
|       }); | ||||
|     } else { | ||||
|       handleResetToPais(); | ||||
| @@ -101,11 +94,7 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) => | ||||
|       </header> | ||||
|       <main className={`panel-main-content ${!isPanelOpen ? 'panel-collapsed' : ''}`}> | ||||
|         <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 ? '›' : '‹'} | ||||
|           </button> | ||||
|           <Suspense fallback={<div className="spinner" />}> | ||||
| @@ -114,6 +103,7 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) => | ||||
|               categoriaId={categoriaId} | ||||
|               nivel={ambitoActual.nivel} | ||||
|               nombreAmbito={ambitoActual.nombre} | ||||
|               nombreProvinciaActiva={ambitoActual.provinciaNombre} | ||||
|               provinciaDistritoId={ambitoActual.provinciaDistritoId ?? null} | ||||
|               onAmbitoSelect={handleAmbitoSelect} | ||||
|               onVolver={ambitoActual.nivel === 'municipio' ? handleVolverAProvincia : handleResetToPais} | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| // src/features/legislativas/nacionales/components/Breadcrumbs.tsx | ||||
| import { FiHome, FiChevronRight } from 'react-icons/fi'; | ||||
|  | ||||
| interface BreadcrumbsProps { | ||||
|   nivel: 'pais' | 'provincia' | 'municipio'; | ||||
|   nombreAmbito: string; | ||||
| @@ -9,20 +11,39 @@ interface BreadcrumbsProps { | ||||
|  | ||||
| export const Breadcrumbs = ({ nivel, nombreAmbito, nombreProvincia, onReset, onVolverProvincia }: BreadcrumbsProps) => { | ||||
|   return ( | ||||
|     <div className="breadcrumbs"> | ||||
|       {nivel !== 'pais' && ( | ||||
|     <nav className="breadcrumbs-container"> | ||||
|       {nivel !== 'pais' ? ( | ||||
|         <> | ||||
|           <button onClick={onReset} className="breadcrumb-link">Argentina</button> | ||||
|           <span className="breadcrumb-separator">{'>'}</span> | ||||
|           <button onClick={onReset} className="breadcrumb-item"> | ||||
|             <FiHome className="breadcrumb-icon" /> | ||||
|             <span>Argentina</span> | ||||
|           </button> | ||||
|           <FiChevronRight className="breadcrumb-separator" /> | ||||
|         </> | ||||
|       ) : ( | ||||
|         <div className="breadcrumb-item-actual"> | ||||
|           <FiHome className="breadcrumb-icon" /> | ||||
|           <span>{nombreAmbito}</span> | ||||
|         </div> | ||||
|       )} | ||||
|        | ||||
|       {nivel === 'provincia' && ( | ||||
|         <div className="breadcrumb-item-actual"> | ||||
|           <span>{nombreAmbito}</span> | ||||
|         </div> | ||||
|       )} | ||||
|  | ||||
|       {nivel === 'municipio' && nombreProvincia && ( | ||||
|         <> | ||||
|           <button onClick={onVolverProvincia} className="breadcrumb-link">{nombreProvincia}</button> | ||||
|           <span className="breadcrumb-separator">{'>'}</span> | ||||
|           <button onClick={onVolverProvincia} className="breadcrumb-item"> | ||||
|             <span>{nombreProvincia}</span> | ||||
|           </button> | ||||
|           <FiChevronRight className="breadcrumb-separator" /> | ||||
|           <div className="breadcrumb-item-actual"> | ||||
|             <span>{nombreAmbito}</span> | ||||
|           </div> | ||||
|         </> | ||||
|       )} | ||||
|       <span className="breadcrumb-actual">{nombreAmbito}</span> | ||||
|     </div> | ||||
|     </nav> | ||||
|   ); | ||||
| }; | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -1,12 +1,14 @@ | ||||
| // src/features/legislativas/nacionales/components/MapaNacional.tsx | ||||
| import axios from 'axios'; | ||||
| import { Suspense, useState, useEffect, useCallback } from 'react'; // <-- Asegúrate de que useCallback esté importado | ||||
| import { Suspense, useState, useEffect, useCallback, useRef } from 'react'; | ||||
| import { useSuspenseQuery } from '@tanstack/react-query'; | ||||
| import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps'; | ||||
| import { Tooltip } from 'react-tooltip'; | ||||
| import { geoCentroid } from 'd3-geo'; | ||||
| import { feature } from 'topojson-client'; | ||||
| import { API_BASE_URL, assetBaseUrl } from '../../../../apiService'; | ||||
| import type { ResultadoMapaDto, AmbitoGeography } from '../../../../types/types'; | ||||
| import { MapaProvincial } from './MapaProvincial'; | ||||
| import { CabaLupa } from './CabaLupa'; | ||||
|  | ||||
| const DEFAULT_MAP_COLOR = '#E0E0E0'; | ||||
| const FADED_BACKGROUND_COLOR = '#F0F0F0'; | ||||
| @@ -14,19 +16,44 @@ 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 }, | ||||
|   "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 }, | ||||
|   "SANTA FE": { center: [-61, -31.2], zoom: 6 }, | ||||
|   "CORRIENTES": { center: [-58, -29], zoom: 7 }, | ||||
|   "RIO NEGRO": { center: [-67.5, -40], zoom: 5.5 }, | ||||
|   "TIERRA DEL FUEGO": { center: [-66.5, -54.2], zoom: 7 }, | ||||
| }; | ||||
|  | ||||
| const LUPA_SIZE_RATIO = 0.2; | ||||
| const MIN_LUPA_SIZE_PX = 100; | ||||
| const MAX_LUPA_SIZE_PX = 180; | ||||
|  | ||||
|  | ||||
| interface MapaNacionalProps { | ||||
|   eleccionId: number; | ||||
|   categoriaId: number; | ||||
|   nivel: 'pais' | 'provincia' | 'municipio'; | ||||
|   nombreAmbito: string; | ||||
|   nombreProvinciaActiva: string | undefined | null; | ||||
|   provinciaDistritoId: string | null; | ||||
|   onAmbitoSelect: (ambitoId: string, nivel: 'provincia' | 'municipio', nombre: string) => void; | ||||
|   onVolver: () => void; | ||||
| } | ||||
|  | ||||
| export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, provinciaDistritoId, onAmbitoSelect, onVolver }: MapaNacionalProps) => { | ||||
| export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nombreProvinciaActiva, provinciaDistritoId, onAmbitoSelect, onVolver }: MapaNacionalProps) => { | ||||
|   const [position, setPosition] = useState({ zoom: 1, center: [-65, -40] as PointTuple }); | ||||
|  | ||||
|   const containerRef = useRef<HTMLDivElement | null>(null); | ||||
|   const lupaRef = useRef<HTMLDivElement | null>(null); | ||||
|   const cabaPathRef = useRef<SVGPathElement | null>(null); | ||||
|   const isAnimatingRef = useRef(false); | ||||
|   const initialLoadRef = useRef(true); // Ref para controlar la carga inicial | ||||
|  | ||||
|   const [lupaStyle, setLupaStyle] = useState<React.CSSProperties>({ opacity: 0 }); | ||||
|  | ||||
|   const { data: mapaDataNacional } = useSuspenseQuery<ResultadoMapaDto[]>({ | ||||
|     queryKey: ['mapaResultados', eleccionId, categoriaId, null], | ||||
|     queryFn: async () => { | ||||
| @@ -41,63 +68,171 @@ export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, pro | ||||
|     queryFn: () => axios.get(`${assetBaseUrl}/maps/argentina-provincias.topojson`).then(res => res.data), | ||||
|   }); | ||||
|  | ||||
|   const resultadosNacionalesPorNombre = new Map<string, ResultadoMapaDto>(mapaDataNacional.map(d => [normalizarTexto(d.ambitoNombre), d])); | ||||
|    | ||||
|   const nombreMunicipioSeleccionado = nivel === 'municipio' ? nombreAmbito : null; | ||||
|  | ||||
|   // El useEffect para el zoom provincial y nacional sigue siendo correcto. | ||||
|   useEffect(() => { | ||||
|     if (nivel === 'pais') { | ||||
|       setPosition({ zoom: 1, center: [-65, -40] }); | ||||
|     } else if (nivel === 'provincia') { | ||||
|       setPosition({ zoom: 7, center: [-60.5, -37] }); | ||||
|     } | ||||
|     // La lógica de centrado en municipio se delega al hijo, que llamará a `handleCalculatedCenter` | ||||
|   }, [nivel]); | ||||
|       const nombreNormalizado = normalizarTexto(nombreAmbito); | ||||
|       const manualConfig = PROVINCE_VIEW_CONFIG[nombreNormalizado]; | ||||
|  | ||||
|   // **LA SOLUCIÓN CLAVE**: Estabilizamos la función que se pasa al hijo. | ||||
|   const handleCalculatedCenter = useCallback((center: PointTuple, zoom: number) => { | ||||
|     setPosition({ center, zoom }); | ||||
|   }, []); // El array de dependencias vacío asegura que la función nunca cambie | ||||
|       if (manualConfig) { | ||||
|         setPosition(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 }); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }, [nivel, nombreAmbito, geoDataNacional]); | ||||
|  | ||||
|   const resultadosNacionalesPorNombre = new Map<string, ResultadoMapaDto>(mapaDataNacional.map(d => [normalizarTexto(d.ambitoNombre), d])); | ||||
|   const nombreMunicipioSeleccionado = nivel === 'municipio' ? nombreAmbito : null; | ||||
|   const handleCalculatedCenter = useCallback((center: PointTuple, zoom: number) => { setPosition({ center, zoom }); }, []); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const updateLupaPosition = () => { | ||||
|       if (nivel === 'pais' && cabaPathRef.current && containerRef.current) { | ||||
|         const containerRect = containerRef.current.getBoundingClientRect(); | ||||
|         if (containerRect.width === 0) return; | ||||
|  | ||||
|         const cabaRect = cabaPathRef.current.getBoundingClientRect(); | ||||
|         const cabaCenterX = (cabaRect.left - containerRect.left) + cabaRect.width / 2; | ||||
|         const cabaCenterY = (cabaRect.top - containerRect.top) + cabaRect.height / 2; | ||||
|  | ||||
|         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; | ||||
|  | ||||
|         setLupaStyle({ | ||||
|           position: 'absolute', | ||||
|           top: `${cabaCenterY - verticalOffset}px`, | ||||
|           left: `${cabaCenterX + horizontalOffset}px`, | ||||
|           width: `${newLupaSize}px`, | ||||
|           opacity: 1, | ||||
|         }); | ||||
|       } else { | ||||
|         setLupaStyle({ opacity: 0, pointerEvents: 'none' }); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     isAnimatingRef.current = true; | ||||
|  | ||||
|     const handleResize = () => { | ||||
|       if (!isAnimatingRef.current) { | ||||
|         updateLupaPosition(); | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     const resizeObserver = new ResizeObserver(handleResize); | ||||
|     if (containerRef.current) { | ||||
|       resizeObserver.observe(containerRef.current); | ||||
|     } | ||||
|      | ||||
|     let timerId: NodeJS.Timeout; | ||||
|  | ||||
|     if (initialLoadRef.current && nivel === 'pais') { | ||||
|       // Carga inicial: posicionar inmediatamente | ||||
|       timerId = setTimeout(() => { | ||||
|         updateLupaPosition(); | ||||
|         isAnimatingRef.current = false; | ||||
|       }, 0); | ||||
|       initialLoadRef.current = false; // Marcar como ya cargado | ||||
|     } else { | ||||
|       // Transición de vuelta: esperar a que termine la animación | ||||
|       timerId = setTimeout(() => { | ||||
|         updateLupaPosition(); | ||||
|         isAnimatingRef.current = false; | ||||
|       }, 800); | ||||
|     } | ||||
|  | ||||
|     return () => { | ||||
|       if (containerRef.current) { | ||||
|         resizeObserver.unobserve(containerRef.current); | ||||
|       } | ||||
|       clearTimeout(timerId); | ||||
|       isAnimatingRef.current = false; | ||||
|     }; | ||||
|   }, [position, nivel]); | ||||
|  | ||||
|   return ( | ||||
|     <div className="mapa-componente-container"> | ||||
|     <div className="mapa-componente-container" ref={containerRef}> | ||||
|       {nivel !== 'pais' && <button onClick={onVolver} className="mapa-volver-btn">← Volver</button>} | ||||
|       <ComposableMap projection="geoMercator" projectionConfig={{ scale: 700, center: [-65, -40] }} style={{ width: "100%", height: "100%" }}> | ||||
|         <ZoomableGroup center={position.center} zoom={position.zoom} filterZoomEvent={() => false}> | ||||
|           <Geographies geography={geoDataNacional}> | ||||
|             {({ geographies }: { geographies: AmbitoGeography[] }) => geographies.map((geo) => { | ||||
|               const resultado = resultadosNacionalesPorNombre.get(normalizarTexto(geo.properties.nombre)); | ||||
|               const esProvinciaActiva = provinciaDistritoId && resultado?.ambitoId === provinciaDistritoId; | ||||
|  | ||||
|               return ( | ||||
|                 <Geography | ||||
|                   key={geo.rsmKey} geography={geo} | ||||
|                   className={`rsm-geography ${nivel !== 'pais' ? 'rsm-geography-faded' : ''}`} | ||||
|                   style={{ visibility: esProvinciaActiva ? 'hidden' : 'visible' }} | ||||
|                   fill={nivel === 'pais' ? (resultado?.colorGanador || DEFAULT_MAP_COLOR) : FADED_BACKGROUND_COLOR} | ||||
|                   onClick={() => resultado && onAmbitoSelect(resultado.ambitoId, 'provincia', resultado.ambitoNombre)} | ||||
|       <div className="mapa-render-area"> | ||||
|         <ComposableMap | ||||
|           projection="geoMercator" | ||||
|           projectionConfig={{ scale: 700, center: [-65, -40] }} | ||||
|           style={{ width: "100%", height: "100%" }} | ||||
|         > | ||||
|           <ZoomableGroup | ||||
|             center={position.center} | ||||
|             zoom={position.zoom} | ||||
|             filterZoomEvent={() => false} | ||||
|           > | ||||
|             <Geographies geography={geoDataNacional}> | ||||
|               {({ geographies }: { geographies: AmbitoGeography[] }) => geographies.map((geo) => { | ||||
|                 const nombreNormalizado = normalizarTexto(geo.properties.nombre); | ||||
|                 const esCABA = nombreNormalizado === 'CIUDAD AUTONOMA DE BUENOS AIRES'; | ||||
|                 const resultado = resultadosNacionalesPorNombre.get(nombreNormalizado); | ||||
|                 const esProvinciaActiva = provinciaDistritoId && resultado?.ambitoId === provinciaDistritoId; | ||||
|  | ||||
|                 return ( | ||||
|                   <Geography | ||||
|                     key={geo.rsmKey} | ||||
|                     geography={geo} | ||||
|                     ref={esCABA ? cabaPathRef : undefined} | ||||
|                     className={`rsm-geography ${nivel !== 'pais' ? 'rsm-geography-faded' : ''}`} | ||||
|                     style={{ | ||||
|                       visibility: esCABA ? 'hidden' : (esProvinciaActiva ? 'hidden' : 'visible'), | ||||
|                     }} | ||||
|                     fill={nivel === 'pais' ? (resultado?.colorGanador || DEFAULT_MAP_COLOR) : FADED_BACKGROUND_COLOR} | ||||
|                     onClick={() => !esCABA && resultado && onAmbitoSelect(resultado.ambitoId, 'provincia', resultado.ambitoNombre)} | ||||
|                     data-tooltip-id="mapa-tooltip" | ||||
|                     data-tooltip-content={geo.properties.nombre} | ||||
|                   /> | ||||
|                 ); | ||||
|               })} | ||||
|             </Geographies> | ||||
|  | ||||
|             {provinciaDistritoId && nombreProvinciaActiva && ( | ||||
|               <Suspense fallback={null}> | ||||
|                 <MapaProvincial | ||||
|                   eleccionId={eleccionId} | ||||
|                   categoriaId={categoriaId} | ||||
|                   distritoId={provinciaDistritoId} | ||||
|                   nombreProvincia={nombreProvinciaActiva} | ||||
|                   nombreMunicipioSeleccionado={nombreMunicipioSeleccionado} | ||||
|                   onMunicipioSelect={(ambitoId, nombre) => onAmbitoSelect(ambitoId, 'municipio', nombre)} | ||||
|                   onCalculatedCenter={handleCalculatedCenter} | ||||
|                   nivel={nivel as 'provincia' | 'municipio'} | ||||
|                 /> | ||||
|               ); | ||||
|             })} | ||||
|           </Geographies> | ||||
|               </Suspense> | ||||
|             )} | ||||
|           </ZoomableGroup> | ||||
|         </ComposableMap> | ||||
|       </div> | ||||
|  | ||||
|       {nivel === 'pais' && ( | ||||
|         <div id="caba-lupa-anchor" className="caba-magnifier-container" style={lupaStyle} ref={lupaRef}> | ||||
|           {(() => { | ||||
|             const resultadoCABA = resultadosNacionalesPorNombre.get("CIUDAD AUTONOMA DE BUENOS AIRES"); | ||||
|             const fillColor = resultadoCABA?.colorGanador || DEFAULT_MAP_COLOR; | ||||
|             const handleClick = () => { | ||||
|               if (resultadoCABA) { | ||||
|                 onAmbitoSelect(resultadoCABA.ambitoId, 'provincia', resultadoCABA.ambitoNombre); | ||||
|               } | ||||
|             }; | ||||
|  | ||||
|             return <CabaLupa fillColor={fillColor} onClick={handleClick} />; | ||||
|           })()} | ||||
|         </div> | ||||
|       )} | ||||
|  | ||||
|           {provinciaDistritoId && ( | ||||
|             <Suspense fallback={null}> | ||||
|               <MapaProvincial | ||||
|                 eleccionId={eleccionId} | ||||
|                 categoriaId={categoriaId} | ||||
|                 distritoId={provinciaDistritoId} | ||||
|                 nombreProvincia={"BUENOS AIRES"} // Esto se podría hacer dinámico si fuera necesario | ||||
|                 nombreMunicipioSeleccionado={nombreMunicipioSeleccionado} | ||||
|                 onMunicipioSelect={(ambitoId, nombre) => onAmbitoSelect(ambitoId, 'municipio', nombre)} | ||||
|                 onCalculatedCenter={handleCalculatedCenter} // Pasamos la función estabilizada | ||||
|                 nivel={nivel as 'provincia' | 'municipio'} // El cast de tipo sigue siendo necesario y correcto | ||||
|               /> | ||||
|             </Suspense> | ||||
|           )} | ||||
|         </ZoomableGroup> | ||||
|       </ComposableMap> | ||||
|       <Tooltip id="mapa-tooltip" /> | ||||
|     </div> | ||||
|   ); | ||||
|   | ||||
| @@ -32,8 +32,7 @@ export const MapaProvincial = ({ eleccionId, categoriaId, distritoId, nombreProv | ||||
|       return response.data; | ||||
|     }, | ||||
|   }); | ||||
|    | ||||
|   // El nombre del archivo ahora es completamente dinámico | ||||
|  | ||||
|   const { data: geoData } = useSuspenseQuery<any>({ | ||||
|     queryKey: ['geoDataProvincial', nombreProvincia], | ||||
|     queryFn: async () => { | ||||
| @@ -43,21 +42,22 @@ export const MapaProvincial = ({ eleccionId, categoriaId, distritoId, nombreProv | ||||
|     }, | ||||
|   }); | ||||
|  | ||||
|   // useEffect para calcular y "exportar" la posición del municipio al padre | ||||
|   // useEffect que calcula y exporta la posición del municipio al padre | ||||
|   useEffect(() => { | ||||
|     if (nivel === 'municipio' && geoData?.objects && nombreMunicipioSeleccionado) { | ||||
|       const geometries = geoData.objects[Object.keys(geoData.objects)[0]].geometries; | ||||
|       const municipioGeo = geometries.find((g: any) => normalizarTexto(g.properties.departamento) === normalizarTexto(nombreMunicipioSeleccionado)); | ||||
|       if (municipioGeo) { | ||||
|           const municipioFeature = feature(geoData, municipioGeo); | ||||
|           const centroid = geoCentroid(municipioFeature); | ||||
|           // Usamos un zoom genérico alto para cualquier municipio | ||||
|           onCalculatedCenter(centroid as PointTuple, 40); | ||||
|         const municipioFeature = feature(geoData, municipioGeo); | ||||
|         const centroid = geoCentroid(municipioFeature); | ||||
|         // Llama a la función del padre para que actualice la posición | ||||
|         onCalculatedCenter(centroid as PointTuple, 40); | ||||
|       } | ||||
|     } | ||||
|   }, [nivel, nombreMunicipioSeleccionado, geoData, onCalculatedCenter]); | ||||
|  | ||||
|   const resultadosPorNombre = new Map<string, ResultadoMapaDto>(mapaData.map(d => [normalizarTexto(d.ambitoNombre), d])); | ||||
|   const esCABA = normalizarTexto(nombreProvincia) === "CIUDAD AUTONOMA DE BUENOS AIRES"; | ||||
|  | ||||
|   return ( | ||||
|     <Geographies geography={geoData}> | ||||
| @@ -65,11 +65,19 @@ export const MapaProvincial = ({ eleccionId, categoriaId, distritoId, nombreProv | ||||
|         const resultado = resultadosPorNombre.get(normalizarTexto(geo.properties.departamento)); | ||||
|         const esSeleccionado = nombreMunicipioSeleccionado ? normalizarTexto(geo.properties.departamento) === normalizarTexto(nombreMunicipioSeleccionado) : false; | ||||
|  | ||||
|         const classNames = [ | ||||
|           'rsm-geography', | ||||
|           'mapa-provincial-geography', | ||||
|           esSeleccionado ? 'selected' : '', | ||||
|           nombreMunicipioSeleccionado && !esSeleccionado ? 'rsm-geography-faded-municipality' : '', | ||||
|           esCABA ? 'caba-comuna-geography' : '' | ||||
|         ].filter(Boolean).join(' '); | ||||
|  | ||||
|         return ( | ||||
|           <Geography | ||||
|             key={geo.rsmKey} | ||||
|             geography={geo} | ||||
|             className={`rsm-geography ${esSeleccionado ? 'selected' : ''} ${nombreMunicipioSeleccionado && !esSeleccionado ? 'rsm-geography-faded-municipality' : ''}`} | ||||
|             className={classNames} | ||||
|             fill={resultado?.colorGanador || DEFAULT_MAP_COLOR} | ||||
|             onClick={resultado ? () => onMunicipioSelect(resultado.ambitoId.toString(), resultado.ambitoNombre) : undefined} | ||||
|             data-tooltip-id="mapa-tooltip" | ||||
|   | ||||
| @@ -3,6 +3,8 @@ import type { ResultadoTicker, EstadoRecuentoTicker } from '../../../../types/ty | ||||
| import { ImageWithFallback } from '../../../../components/common/ImageWithFallback'; | ||||
| import { assetBaseUrl } from '../../../../apiService'; | ||||
| import { AnimatedNumber } from './AnimatedNumber'; | ||||
| import { CircularProgressbar, buildStyles } from 'react-circular-progressbar'; | ||||
| import 'react-circular-progressbar/dist/styles.css'; | ||||
|  | ||||
| const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`; | ||||
| const formatVotes = (num: number) => Math.round(num).toLocaleString('es-AR'); | ||||
| @@ -15,41 +17,65 @@ interface PanelResultadosProps { | ||||
| export const PanelResultados = ({ resultados, estadoRecuento }: PanelResultadosProps) => { | ||||
|   return ( | ||||
|     <div className="panel-resultados"> | ||||
|       <div className="panel-estado-recuento"> | ||||
|         <div className="estado-item"> | ||||
|           <CircularProgressbar | ||||
|             value={estadoRecuento.participacionPorcentaje} | ||||
|             text={formatPercent(estadoRecuento.participacionPorcentaje)} | ||||
|             strokeWidth={10} | ||||
|             styles={buildStyles({ | ||||
|               textColor: '#333', | ||||
|               pathColor: '#28a745', | ||||
|               trailColor: '#e9ecef', | ||||
|               textSize: '24px', | ||||
|             })} | ||||
|           /> | ||||
|           <span>Participación</span> | ||||
|         </div> | ||||
|         <div className="estado-item"> | ||||
|           <CircularProgressbar | ||||
|             value={estadoRecuento.mesasTotalizadasPorcentaje} | ||||
|             text={formatPercent(estadoRecuento.mesasTotalizadasPorcentaje)} | ||||
|             strokeWidth={10} | ||||
|             styles={buildStyles({ | ||||
|               textColor: '#333', | ||||
|               pathColor: '#007bff', | ||||
|               trailColor: '#e9ecef', | ||||
|               textSize: '24px', | ||||
|             })} | ||||
|           /> | ||||
|           <span>Escrutado</span> | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|       <div className="panel-partidos-container"> | ||||
|         {resultados.map(partido => ( | ||||
|           <div key={partido.id} className="partido-fila"> | ||||
|           <div key={partido.id} className="partido-fila" style={{ borderLeftColor: partido.color || '#888' }}> | ||||
|             <div className="partido-logo"> | ||||
|               <ImageWithFallback src={partido.logoUrl || undefined} fallbackSrc={`${assetBaseUrl}/default-avatar.png`} alt={partido.nombre} /> | ||||
|             </div> | ||||
|             <div className="partido-info-wrapper"> | ||||
|               <span className="partido-nombre">{partido.nombreCorto || partido.nombre}</span> | ||||
|               {partido.nombreCandidato && <span className="candidato-nombre">{partido.nombreCandidato}</span>} | ||||
|             <div className="partido-main-content"> | ||||
|               <div className="partido-top-row"> | ||||
|                 <div className="partido-info-wrapper"> | ||||
|                   <span className="partido-nombre">{partido.nombreCorto || partido.nombre}</span> | ||||
|                   {partido.nombreCandidato && <span className="candidato-nombre">{partido.nombreCandidato}</span>} | ||||
|                 </div> | ||||
|                 <div className="partido-stats"> | ||||
|                   <span className="partido-porcentaje"> | ||||
|                     <AnimatedNumber value={partido.porcentaje} formatter={formatPercent} /> | ||||
|                   </span> | ||||
|                   <span className="partido-votos"> | ||||
|                     <AnimatedNumber value={partido.votos} formatter={formatVotes} /> votos | ||||
|                   </span> | ||||
|                 </div> | ||||
|               </div> | ||||
|               <div className="partido-barra-background"> | ||||
|                 <div className="partido-barra-foreground" style={{ width: `${partido.porcentaje}%`, backgroundColor: partido.color || '#888' }} /> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div className="partido-stats"> | ||||
|               <span className="partido-porcentaje"> | ||||
|                 <AnimatedNumber value={partido.porcentaje} formatter={formatPercent} /> | ||||
|               </span> | ||||
|               <span className="partido-votos"> | ||||
|                 <AnimatedNumber value={partido.votos} formatter={formatVotes} /> votos | ||||
|               </span> | ||||
|             </div> | ||||
|           </div> | ||||
|         ))} | ||||
|       </div> | ||||
|  | ||||
|       <div className="panel-estado-recuento"> | ||||
|         <div className="estado-item"> | ||||
|           <span>Participación</span> | ||||
|           <strong><AnimatedNumber value={estadoRecuento.participacionPorcentaje} formatter={formatPercent} /></strong> | ||||
|         </div> | ||||
|         <div className="estado-item"> | ||||
|           <span>Mesas Escrutadas</span> | ||||
|           <strong><AnimatedNumber value={estadoRecuento.mesasTotalizadasPorcentaje} formatter={formatPercent} /></strong> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,21 @@ | ||||
| // src/features/legislativas/nacionales/components/PanelResultadosSkeleton.tsx | ||||
| const SkeletonRow = () => ( | ||||
|   <div className="partido-fila skeleton-fila"> | ||||
|     <div className="skeleton-logo" /> | ||||
|     <div className="partido-info-wrapper"> | ||||
|       <div className="skeleton-text" style={{ width: '60%' }} /> | ||||
|       <div className="skeleton-text" style={{ width: '40%', marginTop: '4px' }} /> | ||||
|       <div className="skeleton-bar" /> | ||||
|     </div> | ||||
|     <div className="partido-stats"> | ||||
|       <div className="skeleton-text" style={{ width: '70%', marginBottom: '4px' }} /> | ||||
|       <div className="skeleton-text" style={{ width: '50%' }} /> | ||||
|     </div> | ||||
|   </div> | ||||
| ); | ||||
|  | ||||
| export const PanelResultadosSkeleton = () => ( | ||||
|   <div className="panel-resultados-skeleton"> | ||||
|     {[...Array(5)].map((_, i) => <SkeletonRow key={i} />)} | ||||
|   </div> | ||||
| ); | ||||
		Reference in New Issue
	
	Block a user