Feat Widgets Cards y Optimización de Consultas
This commit is contained in:
		| @@ -0,0 +1,64 @@ | ||||
| // src/features/legislativas/nacionales/components/MiniMapaSvg.tsx | ||||
| import { useQuery } from '@tanstack/react-query'; | ||||
| import axios from 'axios'; | ||||
| import { useMemo } from 'react'; | ||||
| import { assetBaseUrl } from '../../../../apiService'; | ||||
|  | ||||
| interface MiniMapaSvgProps { | ||||
|     provinciaNombre: string; | ||||
|     fillColor: string; | ||||
| } | ||||
|  | ||||
| // Función para normalizar el nombre de la provincia y que coincida con el nombre del archivo SVG | ||||
| const normalizarNombreParaUrl = (nombre: string) =>  | ||||
|     nombre | ||||
|         .toLowerCase() | ||||
|         .replace(/ /g, '_') // Reemplaza espacios con guiones bajos | ||||
|         .normalize("NFD")    // Descompone acentos para eliminarlos en el siguiente paso | ||||
|         .replace(/[\u0300-\u036f]/g, ""); // Elimina los acentos | ||||
|  | ||||
| export const MiniMapaSvg = ({ provinciaNombre, fillColor }: MiniMapaSvgProps) => { | ||||
|     const nombreNormalizado = normalizarNombreParaUrl(provinciaNombre); | ||||
|     // Asumimos que los SVGs están en /public/maps/provincias-svg/ | ||||
|     const mapFileUrl = `${assetBaseUrl}/maps/provincias-svg/${nombreNormalizado}.svg`; | ||||
|  | ||||
|     // Usamos React Query para fetchear el contenido del SVG como texto | ||||
|     const { data: svgContent, isLoading, isError } = useQuery<string>({ | ||||
|         queryKey: ['svgMapa', nombreNormalizado], | ||||
|         queryFn: async () => { | ||||
|             const response = await axios.get(mapFileUrl, { responseType: 'text' }); | ||||
|             return response.data; | ||||
|         }, | ||||
|         staleTime: Infinity, // Estos archivos son estáticos y no cambian | ||||
|         gcTime: Infinity, | ||||
|         retry: false, // No reintentar si el archivo no existe | ||||
|     }); | ||||
|  | ||||
|     // Usamos useMemo para modificar el SVG solo cuando el contenido o el color cambian | ||||
|     const modifiedSvg = useMemo(() => { | ||||
|         if (!svgContent) return ''; | ||||
|  | ||||
|         // Usamos una expresión regular para encontrar todas las etiquetas <path> | ||||
|         // y añadirles el atributo de relleno con el color del ganador. | ||||
|         // Esto sobrescribirá cualquier 'fill' que ya exista en la etiqueta. | ||||
|         return svgContent.replace(/<path/g, `<path fill="${fillColor}"`); | ||||
|     }, [svgContent, fillColor]); | ||||
|  | ||||
|     if (isLoading) { | ||||
|         return <div className="map-placeholder" />; | ||||
|     } | ||||
|  | ||||
|     if (isError || !modifiedSvg) { | ||||
|         // Muestra un placeholder si el SVG no se encontró o está vacío | ||||
|         return <div className="map-placeholder error" />; | ||||
|     } | ||||
|  | ||||
|     // Renderizamos el SVG modificado. dangerouslySetInnerHTML es seguro aquí | ||||
|     // porque el contenido proviene de nuestros propios archivos SVG estáticos. | ||||
|     return ( | ||||
|         <div  | ||||
|             className="map-svg-container"  | ||||
|             dangerouslySetInnerHTML={{ __html: modifiedSvg }}  | ||||
|         /> | ||||
|     ); | ||||
| }; | ||||
| @@ -0,0 +1,78 @@ | ||||
| // src/features/legislativas/nacionales/components/ProvinciaCard.tsx | ||||
| import type { ResumenProvincia } from '../../../../types/types'; | ||||
| import { MiniMapaSvg } from './MiniMapaSvg'; | ||||
| import { ImageWithFallback } from '../../../../components/common/ImageWithFallback'; | ||||
| import { assetBaseUrl } from '../../../../apiService'; | ||||
|  | ||||
| interface ProvinciaCardProps { | ||||
|     data: ResumenProvincia; | ||||
| } | ||||
|  | ||||
| const formatNumber = (num: number) => num.toLocaleString('es-AR'); | ||||
| const formatPercent = (num: number) => `${num.toFixed(2).replace('.', ',')}%`; | ||||
|  | ||||
| export const ProvinciaCard = ({ data }: ProvinciaCardProps) => { | ||||
|     // Determinamos el color del ganador para pasárselo al mapa. | ||||
|     // Si no hay ganador, usamos un color gris por defecto. | ||||
|     const colorGanador = data.resultados[0]?.color || '#d1d1d1'; | ||||
|  | ||||
|     return ( | ||||
|         <div className="provincia-card"> | ||||
|             <header className="card-header"> | ||||
|                 <div className="header-info"> | ||||
|                     <h3 style={{ whiteSpace: 'normal' }}>{data.provinciaNombre}</h3> | ||||
|                     <span>DIPUTADOS NACIONALES</span> | ||||
|                 </div> | ||||
|                 <div className="header-map"> | ||||
|                     <MiniMapaSvg provinciaNombre={data.provinciaNombre} fillColor={colorGanador} /> | ||||
|                 </div> | ||||
|             </header> | ||||
|             <div className="card-body"> | ||||
|                 {data.resultados.map(res => ( | ||||
|                     <div key={res.agrupacionId} className="candidato-row"> | ||||
|                         <ImageWithFallback src={res.fotoUrl ?? undefined} fallbackSrc={`${assetBaseUrl}/default-avatar.png`} alt={res.nombreCandidato ?? res.nombreAgrupacion} className="candidato-foto" /> | ||||
|  | ||||
|                         <div className="candidato-data"> | ||||
|                             {res.nombreCandidato && ( | ||||
|                                 <span className="candidato-nombre">{res.nombreCandidato}</span> | ||||
|                             )} | ||||
|  | ||||
|                             <span className={`candidato-partido ${!res.nombreCandidato ? 'main-title' : ''}`}> | ||||
|                                 {res.nombreAgrupacion} | ||||
|                             </span> | ||||
|  | ||||
|                             <div className="progress-bar-container"> | ||||
|                                 <div className="progress-bar" style={{ width: `${res.porcentaje}%`, backgroundColor: res.color || '#ccc' }} /> | ||||
|                             </div> | ||||
|                         </div> | ||||
|  | ||||
|                         <div className="candidato-stats"> | ||||
|                             <span className="stats-percent">{formatPercent(res.porcentaje)}</span> | ||||
|                             <span className="stats-votos">{formatNumber(res.votos)} votos</span> | ||||
|                         </div> | ||||
|                         <div className="stats-bancas"> | ||||
|                             +{res.bancasObtenidas} | ||||
|                             <span>Bancas</span> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 ))} | ||||
|             </div> | ||||
|             <footer className="card-footer"> | ||||
|                 <div> | ||||
|                     <span>Participación</span> | ||||
|                     {/* Usamos los datos reales del estado de recuento */} | ||||
|                     <strong>{formatPercent(data.estadoRecuento?.participacionPorcentaje ?? 0)}</strong> | ||||
|                 </div> | ||||
|                 <div> | ||||
|                     <span>Mesas escrutadas</span> | ||||
|                     <strong>{formatPercent(data.estadoRecuento?.mesasTotalizadasPorcentaje ?? 0)}</strong> | ||||
|                 </div> | ||||
|                 <div> | ||||
|                     <span>Votos totales</span> | ||||
|                     {/* Usamos el nuevo campo cantidadVotantes */} | ||||
|                     <strong>{formatNumber(data.estadoRecuento?.cantidadVotantes ?? 0)}</strong> | ||||
|                 </div> | ||||
|             </footer> | ||||
|         </div> | ||||
|     ); | ||||
| }; | ||||
		Reference in New Issue
	
	Block a user