Feat Front Widgets Refactizados y Ajustes Backend
This commit is contained in:
		
							
								
								
									
										42
									
								
								Elecciones-Web/Restaurar/Nuevos/App.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								Elecciones-Web/Restaurar/Nuevos/App.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| #root { | ||||
|   max-width: 1280px; | ||||
|   margin: 0 auto; | ||||
|   padding: 2rem; | ||||
|   text-align: center; | ||||
| } | ||||
|  | ||||
| .logo { | ||||
|   height: 6em; | ||||
|   padding: 1.5em; | ||||
|   will-change: filter; | ||||
|   transition: filter 300ms; | ||||
| } | ||||
| .logo:hover { | ||||
|   filter: drop-shadow(0 0 2em #646cffaa); | ||||
| } | ||||
| .logo.react:hover { | ||||
|   filter: drop-shadow(0 0 2em #61dafbaa); | ||||
| } | ||||
|  | ||||
| @keyframes logo-spin { | ||||
|   from { | ||||
|     transform: rotate(0deg); | ||||
|   } | ||||
|   to { | ||||
|     transform: rotate(360deg); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @media (prefers-reduced-motion: no-preference) { | ||||
|   a:nth-of-type(2) .logo { | ||||
|     animation: logo-spin infinite 20s linear; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .card { | ||||
|   padding: 2em; | ||||
| } | ||||
|  | ||||
| .read-the-docs { | ||||
|   color: #888; | ||||
| } | ||||
							
								
								
									
										18
									
								
								Elecciones-Web/Restaurar/Nuevos/App.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								Elecciones-Web/Restaurar/Nuevos/App.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| // src/App.tsx | ||||
| import 'react-tooltip/dist/react-tooltip.css'; | ||||
| import MapaBsAs from './components/MapaBsAs'; | ||||
|  | ||||
| function App() { | ||||
|   return ( | ||||
|     <div style={{ width: '50%', margin: '2rem auto' }}> | ||||
|       <h1>Resultados Electorales - Provincia de Buenos Aires</h1> | ||||
|       <p>Pasa el ratón sobre un partido para ver el ganador. Haz clic para más detalles.</p> | ||||
|        | ||||
|       <MapaBsAs /> {/* Ya no necesita un div contenedor aquí */} | ||||
|  | ||||
|       {/* Próximo paso: Crear una Leyenda aquí */} | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export default App; | ||||
							
								
								
									
										58
									
								
								Elecciones-Web/Restaurar/Nuevos/components/MapaBsAs.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								Elecciones-Web/Restaurar/Nuevos/components/MapaBsAs.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| /* src/components/MapaBsAs.css */ | ||||
| .mapa-wrapper { | ||||
|   display: flex; | ||||
|   gap: 2rem; | ||||
| } | ||||
|  | ||||
| .mapa-container { | ||||
|   flex: 3; /* El mapa ocupa 3/4 del espacio */ | ||||
|   border: 1px solid #ccc; | ||||
|   border-radius: 8px; | ||||
|   position: relative; | ||||
|   /* Proporción aproximada para la provincia de Bs As */ | ||||
|   padding-top: 80%;  | ||||
| } | ||||
|  | ||||
| .mapa-container .rsm-svg { | ||||
|   position: absolute; | ||||
|   top: 0; | ||||
|   left: 0; | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
| } | ||||
|  | ||||
| /* Efecto "lift" al pasar el ratón */ | ||||
| .rsm-geography { | ||||
|   transition: transform 0.2s ease-in-out, fill 0.2s ease-in-out; | ||||
|   cursor: pointer; | ||||
| } | ||||
| .rsm-geography:hover { | ||||
|   transform: scale(1.03); | ||||
|   transform-origin: center center; | ||||
| } | ||||
|  | ||||
| .info-panel { | ||||
|   flex: 1; /* El panel ocupa 1/4 del espacio */ | ||||
|   padding: 1rem; | ||||
|   background-color: #f9f9f9; | ||||
|   border-radius: 8px; | ||||
| } | ||||
| .info-panel h3 { | ||||
|   margin-top: 0; | ||||
| } | ||||
|  | ||||
| .legend { | ||||
|   margin-top: 1.5rem; | ||||
| } | ||||
| .legend-item { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   margin-bottom: 0.5rem; | ||||
| } | ||||
| .legend-color-box { | ||||
|   width: 20px; | ||||
|   height: 20px; | ||||
|   margin-right: 10px; | ||||
|   border: 1px solid #fff; | ||||
|   outline: 1px solid #ccc; | ||||
| } | ||||
							
								
								
									
										217
									
								
								Elecciones-Web/Restaurar/Nuevos/components/MapaBsAs.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										217
									
								
								Elecciones-Web/Restaurar/Nuevos/components/MapaBsAs.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,217 @@ | ||||
| // src/components/MapaBsAs.tsx | ||||
| import { useState, useMemo } from 'react'; | ||||
| import { ComposableMap, Geographies, Geography } from 'react-simple-maps'; | ||||
| import { Tooltip } from 'react-tooltip'; | ||||
| import { useQuery } from '@tanstack/react-query'; | ||||
| import axios from 'axios'; | ||||
| import type { Feature, Geometry } from 'geojson'; | ||||
| import { geoCentroid } from 'd3-geo'; // Para calcular el centro de cada partido | ||||
| import { useSpring, animated } from 'react-spring'; // Para animar el zoom | ||||
|  | ||||
| //import geoUrl from '/partidos-bsas.topojson'; | ||||
| import './MapaBsAs.css'; | ||||
|  | ||||
| // --- Interfaces y Tipos --- | ||||
| interface ResultadoMapa { | ||||
|   partidoId: string; | ||||
|   agrupacionGanadoraId: string; | ||||
|   porcentajeGanador: number; // Nueva propiedad desde el backend | ||||
| } | ||||
|  | ||||
| interface Agrupacion { | ||||
|   id: string; | ||||
|   nombre: string; | ||||
| } | ||||
|  | ||||
| interface PartidoProperties { | ||||
|   id: number; | ||||
|   departamento: string; | ||||
|   cabecera: string; // Asegúrate de que coincida con tu topojson | ||||
|   provincia: string; | ||||
| } | ||||
|  | ||||
| type PartidoGeography = Feature<Geometry, PartidoProperties> & { rsmKey: string }; | ||||
|  | ||||
| const PALETA_COLORES: { [key: string]: [number, number, number] } = { | ||||
|   'default': [214, 214, 218] // RGB para el color por defecto | ||||
| }; | ||||
|  | ||||
| const INITIAL_PROJECTION = { | ||||
|   center: [-59.8, -37.0] as [number, number], | ||||
|   scale: 5400, | ||||
| }; | ||||
|  | ||||
| const MapaBsAs = () => { | ||||
|   const [selectedPartido, setSelectedPartido] = useState<PartidoGeography | null>(null); | ||||
|   const [projectionConfig, setProjectionConfig] = useState(INITIAL_PROJECTION); | ||||
|  | ||||
|   // --- Carga de Datos --- | ||||
|   const { data: resultadosData, isLoading: isLoadingResultados } = useQuery<ResultadoMapa[]>({ | ||||
|     queryKey: ['mapaResultados'], | ||||
|     queryFn: async () => { | ||||
|       const { data } = await axios.get('http://localhost:5217/api/Resultados/mapa'); | ||||
|       return data; | ||||
|     }, | ||||
|   }); | ||||
|  | ||||
|   const { data: geoData, isLoading: isLoadingGeo } = useQuery({ | ||||
|     queryKey: ['mapaGeoData'], | ||||
|     queryFn: async () => { | ||||
|       const { data } = await axios.get('/partidos-bsas.topojson');  | ||||
|       return data; | ||||
|     }, | ||||
|   }); | ||||
|  | ||||
|   const { data: agrupacionesData, isLoading: isLoadingAgrupaciones } = useQuery<Agrupacion[]>({ | ||||
|     queryKey: ['catalogoAgrupaciones'], | ||||
|     queryFn: async () => { | ||||
|       const { data } = await axios.get('http://localhost:5217/api/Catalogos/agrupaciones'); | ||||
|       return data; | ||||
|     }, | ||||
|   }); | ||||
|  | ||||
|   const { nombresAgrupaciones, coloresPartidos } = useMemo(() => { | ||||
|     if (!agrupacionesData) return { nombresAgrupaciones: {}, coloresPartidos: {} }; | ||||
|  | ||||
|     const nombres = agrupacionesData.reduce((acc, agrupacion) => { | ||||
|       acc[agrupacion.id] = agrupacion.nombre; | ||||
|       return acc; | ||||
|     }, {} as { [key: string]: string }); | ||||
|  | ||||
|     const colores = agrupacionesData.reduce((acc, agrupacion, index) => { | ||||
|       const baseColor = [255, 87, 51, 51, 255, 87, 51, 87, 255, 255, 51, 161, 161, 51, 255, 255, 195, 0, 199, 0, 57, 144, 12, 63, 88, 24, 69]; | ||||
|       acc[agrupacion.nombre] = [baseColor[index*3], baseColor[index*3+1], baseColor[index*3+2]]; | ||||
|       return acc; | ||||
|     }, {} as { [key: string]: [number, number, number] }); | ||||
|      | ||||
|     colores['default'] = [214, 214, 218]; | ||||
|     return { nombresAgrupaciones: nombres, coloresPartidos: colores }; | ||||
|   }, [agrupacionesData]); | ||||
|  | ||||
|   const animatedProps = useSpring({ | ||||
|     to: { scale: projectionConfig.scale, cx: projectionConfig.center[0], cy: projectionConfig.center[1] }, | ||||
|     config: { tension: 170, friction: 26 }, | ||||
|   }); | ||||
|  | ||||
|   if (isLoadingResultados || isLoadingGeo || isLoadingAgrupaciones) { | ||||
|     return <div>Cargando datos del mapa...</div>; | ||||
|   } | ||||
|  | ||||
|   const getPartyStyle = (partidoIdGeo: string) => { | ||||
|     const resultado = resultadosData?.find(r => r.partidoId === partidoIdGeo); | ||||
|     if (!resultado) { | ||||
|       return { fill: `rgb(${PALETA_COLORES.default.join(',')})` }; | ||||
|     } | ||||
|     const nombreAgrupacion = nombresAgrupaciones[resultado.agrupacionGanadoraId] || 'Otro'; | ||||
|     const baseColor = coloresPartidos[nombreAgrupacion] || PALETA_COLORES.default; | ||||
|      | ||||
|     // Calcula la opacidad basada en el porcentaje. 0.4 (débil) a 1.0 (fuerte) | ||||
|     const opacity = 0.4 + (resultado.porcentajeGanador / 100) * 0.6; | ||||
|      | ||||
|     return { fill: `rgba(${baseColor.join(',')}, ${opacity})` }; | ||||
|   }; | ||||
|  | ||||
|   const handleGeographyClick = (geo: PartidoGeography) => { | ||||
|     const centroid = geoCentroid(geo); | ||||
|     setSelectedPartido(geo); | ||||
|     setProjectionConfig({ | ||||
|       center: centroid, | ||||
|       scale: 18000, // Zoom más cercano | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   const handleReset = () => { | ||||
|     setSelectedPartido(null); | ||||
|     setProjectionConfig(INITIAL_PROJECTION); | ||||
|   }; | ||||
|    | ||||
|   return ( | ||||
|     <div className="mapa-wrapper"> | ||||
|       <div className="mapa-container"> | ||||
|         <animated.div style={{ | ||||
|           width: '100%', | ||||
|           height: '100%', | ||||
|           transform: animatedProps.scale.to(s => `scale(${s / INITIAL_PROJECTION.scale})`), | ||||
|         }}> | ||||
|           <ComposableMap | ||||
|             projection="geoMercator" | ||||
|             projectionConfig={{ | ||||
|               scale: animatedProps.scale, | ||||
|               center: [animatedProps.cx, animatedProps.cy], | ||||
|             }} | ||||
|             className="rsm-svg" | ||||
|           > | ||||
|             <Geographies geography={geoData}> | ||||
|               {({ geographies }: { geographies: PartidoGeography[] }) => | ||||
|                 geographies.map((geo) => { | ||||
|                   const partidoId = String(geo.properties.id); | ||||
|                   const partidoNombre = geo.properties.departamento; | ||||
|                   const resultado = resultadosData?.find(r => r.partidoId === partidoId); | ||||
|                   const agrupacionNombre = resultado ? (nombresAgrupaciones[resultado.agrupacionGanadoraId] || 'Desconocido') : 'Sin datos'; | ||||
|  | ||||
|                   return ( | ||||
|                     <Geography | ||||
|                       key={geo.rsmKey} | ||||
|                       geography={geo} | ||||
|                       data-tooltip-id="partido-tooltip" | ||||
|                       data-tooltip-content={`${partidoNombre}: ${agrupacionNombre}`} | ||||
|                       fill={getPartyStyle(partidoId).fill} | ||||
|                       stroke="#FFF" | ||||
|                       className="rsm-geography" | ||||
|                       style={{ | ||||
|                         default: { outline: 'none' }, | ||||
|                         hover: { outline: 'none', stroke: '#FF5722', strokeWidth: 2, fill: getPartyStyle(partidoId).fill }, | ||||
|                         pressed: { outline: 'none' }, | ||||
|                       }} | ||||
|                       onClick={() => handleGeographyClick(geo)} | ||||
|                     /> | ||||
|                   ); | ||||
|                 }) | ||||
|               } | ||||
|             </Geographies> | ||||
|           </ComposableMap> | ||||
|         </animated.div> | ||||
|         <Tooltip id="partido-tooltip" /> | ||||
|       </div> | ||||
|  | ||||
|       <div className="info-panel"> | ||||
|         <button onClick={handleReset}>Resetear Vista</button> | ||||
|         {selectedPartido ? ( | ||||
|           <div> | ||||
|             <h3>{selectedPartido.properties.departamento}</h3> | ||||
|             <p><strong>Cabecera:</strong> {selectedPartido.properties.cabecera}</p> | ||||
|             <p><strong>ID:</strong> {selectedPartido.properties.id}</p> | ||||
|             {/* Aquí mostrarías más datos del partido seleccionado */} | ||||
|           </div> | ||||
|         ) : ( | ||||
|           <div> | ||||
|             <h3>Provincia de Buenos Aires</h3> | ||||
|             <p>Selecciona un partido para ver más detalles.</p> | ||||
|           </div> | ||||
|         )} | ||||
|         <Legend colores={coloresPartidos} /> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
|  | ||||
| // Componente de Leyenda separado | ||||
| const Legend = ({ colores }: { colores: { [key: string]: [number, number, number] } }) => { | ||||
|     return ( | ||||
|         <div className="legend"> | ||||
|             <h4>Leyenda</h4> | ||||
|             {Object.entries(colores).map(([nombre, color]) => { | ||||
|                 if (nombre === 'default') return null; | ||||
|                 return ( | ||||
|                     <div key={nombre} className="legend-item"> | ||||
|                         <div className="legend-color-box" style={{ backgroundColor: `rgb(${color.join(',')})` }} /> | ||||
|                         <span>{nombre}</span> | ||||
|                     </div> | ||||
|                 ); | ||||
|             })} | ||||
|         </div> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| export default MapaBsAs; | ||||
							
								
								
									
										68
									
								
								Elecciones-Web/Restaurar/Nuevos/index.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								Elecciones-Web/Restaurar/Nuevos/index.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| :root { | ||||
|   font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; | ||||
|   line-height: 1.5; | ||||
|   font-weight: 400; | ||||
|  | ||||
|   color-scheme: light dark; | ||||
|   color: rgba(255, 255, 255, 0.87); | ||||
|   background-color: #242424; | ||||
|  | ||||
|   font-synthesis: none; | ||||
|   text-rendering: optimizeLegibility; | ||||
|   -webkit-font-smoothing: antialiased; | ||||
|   -moz-osx-font-smoothing: grayscale; | ||||
| } | ||||
|  | ||||
| a { | ||||
|   font-weight: 500; | ||||
|   color: #646cff; | ||||
|   text-decoration: inherit; | ||||
| } | ||||
| a:hover { | ||||
|   color: #535bf2; | ||||
| } | ||||
|  | ||||
| body { | ||||
|   margin: 0; | ||||
|   display: flex; | ||||
|   place-items: center; | ||||
|   min-width: 320px; | ||||
|   min-height: 100vh; | ||||
| } | ||||
|  | ||||
| h1 { | ||||
|   font-size: 3.2em; | ||||
|   line-height: 1.1; | ||||
| } | ||||
|  | ||||
| button { | ||||
|   border-radius: 8px; | ||||
|   border: 1px solid transparent; | ||||
|   padding: 0.6em 1.2em; | ||||
|   font-size: 1em; | ||||
|   font-weight: 500; | ||||
|   font-family: inherit; | ||||
|   background-color: #1a1a1a; | ||||
|   cursor: pointer; | ||||
|   transition: border-color 0.25s; | ||||
| } | ||||
| button:hover { | ||||
|   border-color: #646cff; | ||||
| } | ||||
| button:focus, | ||||
| button:focus-visible { | ||||
|   outline: 4px auto -webkit-focus-ring-color; | ||||
| } | ||||
|  | ||||
| @media (prefers-color-scheme: light) { | ||||
|   :root { | ||||
|     color: #213547; | ||||
|     background-color: #ffffff; | ||||
|   } | ||||
|   a:hover { | ||||
|     color: #747bff; | ||||
|   } | ||||
|   button { | ||||
|     background-color: #f9f9f9; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										17
									
								
								Elecciones-Web/Restaurar/Nuevos/main.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								Elecciones-Web/Restaurar/Nuevos/main.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| // src/main.tsx | ||||
| import React from 'react' | ||||
| import ReactDOM from 'react-dom/client' | ||||
| import App from './App.tsx' | ||||
| import './index.css' | ||||
| import { QueryClient, QueryClientProvider } from '@tanstack/react-query' | ||||
|  | ||||
| // Crea un cliente | ||||
| const queryClient = new QueryClient() | ||||
|  | ||||
| ReactDOM.createRoot(document.getElementById('root')!).render( | ||||
|   <React.StrictMode> | ||||
|     <QueryClientProvider client={queryClient}> | ||||
|       <App /> | ||||
|     </QueryClientProvider> | ||||
|   </React.StrictMode>, | ||||
| ) | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										12
									
								
								Elecciones-Web/Restaurar/Nuevos/types/custom.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								Elecciones-Web/Restaurar/Nuevos/types/custom.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| // src/types/custom.d.ts | ||||
|  | ||||
| // Solución para el problema 1: Le dice a TypeScript que el módulo 'react-simple-maps' existe. | ||||
| // Esto evita el error de "módulo no encontrado" y trata sus componentes como 'any'. | ||||
| declare module 'react-simple-maps'; | ||||
|  | ||||
| // Solución para el problema 2: Le dice a TypeScript cómo manejar la importación de archivos .topojson. | ||||
| // Define que cuando importemos un archivo con esa extensión, el contenido será de tipo 'any'. | ||||
| declare module '*.topojson' { | ||||
|   const value: any; | ||||
|   export default value; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user