Feat Widgets Cards y Optimización de Consultas
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
// src/features/legislativas/rovinciales/DevAppLegislativas.tsx
|
||||
import { ResultadosNacionalesCardsWidget } from './nacionales/ResultadosNacionalesCardsWidget';
|
||||
import { CongresoNacionalWidget } from './nacionales/CongresoNacionalWidget';
|
||||
import { PanelNacionalWidget } from './nacionales/PanelNacionalWidget';
|
||||
import './DevAppStyle.css'
|
||||
|
||||
@@ -6,9 +8,8 @@ export const DevAppLegislativas = () => {
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>Visor de Widgets</h1>
|
||||
|
||||
{/* Le pasamos el ID de la elección que queremos visualizar.
|
||||
Para tus datos de prueba provinciales, este ID es 1. */}
|
||||
<ResultadosNacionalesCardsWidget eleccionId={2} />
|
||||
<CongresoNacionalWidget eleccionId={2} />
|
||||
<PanelNacionalWidget eleccionId={2} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
// src/features/legislativas/nacionales/CongresoNacionalWidget.tsx
|
||||
import { useState, Suspense, useMemo } from 'react';
|
||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { DiputadosNacionalesLayout } from '../../../components/common/DiputadosNacionalesLayout';
|
||||
import { SenadoresNacionalesLayout } from '../../../components/common/SenadoresNacionalesLayout';
|
||||
import { getComposicionNacional, type ComposicionNacionalData, type PartidoComposicionNacional } from '../../../apiService';
|
||||
import '../provinciales/CongresoWidget.css';
|
||||
|
||||
interface CongresoNacionalWidgetProps {
|
||||
eleccionId: number;
|
||||
}
|
||||
|
||||
const formatTimestamp = (dateString: string) => {
|
||||
if (!dateString) return '...';
|
||||
const date = new Date(dateString);
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const year = date.getFullYear();
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
return `${day}/${month}/${year} ${hours}:${minutes}`;
|
||||
};
|
||||
|
||||
const WidgetContent = ({ eleccionId }: CongresoNacionalWidgetProps) => {
|
||||
const [camaraActiva, setCamaraActiva] = useState<'diputados' | 'senadores'>('diputados');
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
|
||||
const { data } = useSuspenseQuery<ComposicionNacionalData>({
|
||||
queryKey: ['composicionNacional', eleccionId],
|
||||
queryFn: () => getComposicionNacional(eleccionId),
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
|
||||
const datosCamaraActual = data[camaraActiva];
|
||||
|
||||
const partidosOrdenados = useMemo(() => {
|
||||
if (!datosCamaraActual?.partidos) return [];
|
||||
const partidosACopiar = [...datosCamaraActual.partidos];
|
||||
partidosACopiar.sort((a, b) => {
|
||||
const ordenA = camaraActiva === 'diputados' ? a.ordenDiputadosNacionales : a.ordenSenadoresNacionales;
|
||||
const ordenB = camaraActiva === 'diputados' ? b.ordenDiputadosNacionales : b.ordenSenadoresNacionales;
|
||||
return (ordenA ?? 999) - (ordenB ?? 999);
|
||||
});
|
||||
return partidosACopiar;
|
||||
}, [datosCamaraActual, camaraActiva]);
|
||||
|
||||
const partyDataParaLayout = useMemo(() => {
|
||||
if (camaraActiva === 'senadores') return partidosOrdenados;
|
||||
if (!partidosOrdenados || !datosCamaraActual.presidenteBancada?.color) return partidosOrdenados;
|
||||
const partidoPresidente = partidosOrdenados.find(p => p.color === datosCamaraActual.presidenteBancada!.color);
|
||||
if (!partidoPresidente) return partidosOrdenados;
|
||||
|
||||
const adjustedPartyData = JSON.parse(JSON.stringify(partidosOrdenados));
|
||||
const partidoAjustar = adjustedPartyData.find((p: PartidoComposicionNacional) => p.id === partidoPresidente.id);
|
||||
|
||||
if (partidoAjustar) {
|
||||
const tipoBanca = datosCamaraActual.presidenteBancada.tipoBanca;
|
||||
if (tipoBanca === 'ganada' && partidoAjustar.bancasGanadas > 0) {
|
||||
partidoAjustar.bancasGanadas -= 1;
|
||||
} else if (tipoBanca === 'previa' && partidoAjustar.bancasFijos > 0) {
|
||||
partidoAjustar.bancasFijos -= 1;
|
||||
} else {
|
||||
if (partidoAjustar.bancasGanadas > 0) {
|
||||
partidoAjustar.bancasGanadas -= 1;
|
||||
} else if (partidoAjustar.bancasFijos > 0) {
|
||||
partidoAjustar.bancasFijos -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return adjustedPartyData;
|
||||
}, [partidosOrdenados, datosCamaraActual.presidenteBancada, camaraActiva]);
|
||||
|
||||
return (
|
||||
<div className="congreso-container">
|
||||
<div className="congreso-grafico">
|
||||
<div
|
||||
className={`congreso-hemiciclo-wrapper ${isHovering ? 'is-hovering' : ''}`}
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
>
|
||||
{camaraActiva === 'diputados' ?
|
||||
<DiputadosNacionalesLayout
|
||||
partyData={partyDataParaLayout}
|
||||
presidenteBancada={datosCamaraActual.presidenteBancada || null}
|
||||
size={700}
|
||||
/> :
|
||||
<SenadoresNacionalesLayout
|
||||
partyData={partyDataParaLayout}
|
||||
presidenteBancada={datosCamaraActual.presidenteBancada || null}
|
||||
size={700}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<div className="congreso-footer">
|
||||
<div className="footer-legend">
|
||||
<div className="footer-legend-item">
|
||||
{/* Usamos la nueva clase CSS para el círculo sólido */}
|
||||
<span className="legend-icon legend-icon--solid"></span>
|
||||
<span>Bancas en juego</span>
|
||||
</div>
|
||||
<div className="footer-legend-item">
|
||||
{/* Reemplazamos el SVG por un span con la nueva clase para el anillo */}
|
||||
<span className="legend-icon legend-icon--ring"></span>
|
||||
<span>Bancas previas</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="footer-timestamp">
|
||||
Última Actualización: {formatTimestamp(datosCamaraActual.ultimaActualizacion)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="congreso-summary">
|
||||
<div className="chamber-tabs">
|
||||
<button className={camaraActiva === 'diputados' ? 'active' : ''} onClick={() => setCamaraActiva('diputados')}>
|
||||
Diputados
|
||||
</button>
|
||||
<button className={camaraActiva === 'senadores' ? 'active' : ''} onClick={() => setCamaraActiva('senadores')}>
|
||||
Senadores
|
||||
</button>
|
||||
</div>
|
||||
<h3>{datosCamaraActual.camaraNombre}</h3>
|
||||
<div className="summary-metric">
|
||||
<span>Total de Bancas</span>
|
||||
<strong>{datosCamaraActual.totalBancas}</strong>
|
||||
</div>
|
||||
<div className="summary-metric">
|
||||
<span>Bancas en Juego</span>
|
||||
<strong>{datosCamaraActual.bancasEnJuego}</strong>
|
||||
</div>
|
||||
<hr />
|
||||
<div className="partido-lista-container">
|
||||
<ul className="partido-lista">
|
||||
{partidosOrdenados
|
||||
.filter(p => p.bancasTotales > 0)
|
||||
.map((partido: PartidoComposicionNacional) => (
|
||||
<li key={partido.id}>
|
||||
<span className="partido-color-box" style={{ backgroundColor: partido.color || '#808080' }}></span>
|
||||
<span className="partido-nombre">{partido.nombreCorto || partido.nombre}</span>
|
||||
<strong
|
||||
className="partido-bancas"
|
||||
title={`${partido.bancasFijos} bancas previas + ${partido.bancasGanadas} ganadas`}
|
||||
>
|
||||
{partido.bancasTotales}
|
||||
</strong>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<Tooltip id="party-tooltip" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CongresoNacionalWidget = ({ eleccionId }: CongresoNacionalWidgetProps) => {
|
||||
return (
|
||||
<Suspense fallback={<div className="congreso-container loading" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '400px' }}>Cargando composición del congreso...</div>}>
|
||||
<WidgetContent eleccionId={eleccionId} />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,11 @@
|
||||
/* src/features/legislativas/nacionales/PanelNaciona.css */
|
||||
/* src/features/legislativas/nacionales/PanelNacional.css */
|
||||
.panel-nacional-container {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
@@ -491,13 +492,11 @@
|
||||
/* --- NUEVOS ESTILOS PARA EL TOGGLE MÓVIL --- */
|
||||
.mobile-view-toggle {
|
||||
display: none;
|
||||
/* Oculto por defecto */
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
position: absolute; /* <-- CAMBIO: De 'fixed' a 'absolute' */
|
||||
bottom: 10px; /* <-- AJUSTE: Menos espacio desde abajo */
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 100;
|
||||
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 30px;
|
||||
padding: 5px;
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
/* src/features/legislativas/nacionales/ResultadosNacionalesCardsWidget.css */
|
||||
|
||||
/* --- Variables de Diseño --- */
|
||||
:root {
|
||||
--card-border-color: #e0e0e0;
|
||||
--card-bg-color: #ffffff;
|
||||
--card-header-bg-color: #f8f9fa;
|
||||
--card-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
--text-primary: #212529;
|
||||
--text-secondary: #6c757d;
|
||||
--font-family: "Public Sans", system-ui, sans-serif;
|
||||
--primary-accent-color: #007bff;
|
||||
}
|
||||
|
||||
/* --- Contenedor Principal del Widget --- */
|
||||
.cards-widget-container {
|
||||
font-family: var(--font-family);
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 2rem auto;
|
||||
}
|
||||
|
||||
.cards-widget-container h2 {
|
||||
font-size: 1.75rem;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--card-border-color);
|
||||
}
|
||||
|
||||
/* --- Grilla de Tarjetas --- */
|
||||
.cards-grid {
|
||||
display: grid;
|
||||
/* Crea columnas flexibles que se ajustan al espacio disponible */
|
||||
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
/* --- Tarjeta Individual --- */
|
||||
.provincia-card {
|
||||
background-color: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--card-shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden; /* Asegura que los bordes redondeados se apliquen al contenido */
|
||||
}
|
||||
|
||||
/* --- Cabecera de la Tarjeta --- */
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: var(--card-header-bg-color);
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--card-border-color);
|
||||
}
|
||||
|
||||
.header-info h3 {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.header-info span {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.header-map {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background-color: #e9ecef;
|
||||
padding: 0.25rem;
|
||||
box-sizing: border-box; /* Para que el padding no aumente el tamaño total */
|
||||
}
|
||||
|
||||
/* Contenedor del SVG para asegurar que se ajuste al espacio */
|
||||
.map-svg-container, .map-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Estilo para el SVG renderizado */
|
||||
.map-svg-container svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain; /* Asegura que el mapa no se deforme */
|
||||
}
|
||||
|
||||
/* Placeholder para cuando el mapa no carga */
|
||||
.map-placeholder.error {
|
||||
background-color: #f8d7da; /* Un color de fondo rojizo para indicar un error */
|
||||
}
|
||||
|
||||
/* --- Cuerpo de la Tarjeta --- */
|
||||
.card-body {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.candidato-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.candidato-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.candidato-foto {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.candidato-data {
|
||||
flex-grow: 1;
|
||||
min-width: 0; /* Permite que el texto se trunque si es necesario */
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.candidato-nombre {
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
color: var(--text-primary);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.candidato-partido {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
display: block;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
height: 6px;
|
||||
background-color: #e9ecef;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
transition: width 0.5s ease-out;
|
||||
}
|
||||
|
||||
.candidato-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
text-align: right;
|
||||
flex-shrink: 0;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.stats-percent {
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.stats-votos {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.stats-bancas {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 6px;
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin-left: 0.75rem;
|
||||
font-weight: 700;
|
||||
font-size: 1.2rem;
|
||||
color: var(--primary-accent-color);
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.stats-bancas span {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
|
||||
/* --- Pie de la Tarjeta --- */
|
||||
.card-footer {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
background-color: var(--card-header-bg-color);
|
||||
border-top: 1px solid var(--card-border-color);
|
||||
padding: 0.75rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-footer div {
|
||||
border-right: 1px solid var(--card-border-color);
|
||||
}
|
||||
|
||||
.card-footer div:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.card-footer span {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.card-footer strong {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* --- Media Query para Móvil --- */
|
||||
@media (max-width: 480px) {
|
||||
.cards-grid {
|
||||
/* En pantallas muy pequeñas, forzamos una sola columna */
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.header-info h3 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- NUEVOS ESTILOS PARA EL NOMBRE DEL PARTIDO CUANDO ES EL TÍTULO PRINCIPAL --- */
|
||||
.candidato-partido.main-title {
|
||||
font-size: 0.95rem; /* Hacemos la fuente más grande */
|
||||
font-weight: 700; /* La ponemos en negrita, como el nombre del candidato */
|
||||
color: var(--text-primary); /* Usamos el color de texto principal */
|
||||
text-transform: none; /* Quitamos el 'uppercase' para que se lea mejor */
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// src/features/legislativas/nacionales/ResultadosNacionalesCardsWidget.tsx
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getResumenPorProvincia } from '../../../apiService';
|
||||
import { ProvinciaCard } from './components/ProvinciaCard';
|
||||
import './ResultadosNacionalesCardsWidget.css';
|
||||
|
||||
interface Props {
|
||||
eleccionId: number;
|
||||
}
|
||||
|
||||
export const ResultadosNacionalesCardsWidget = ({ eleccionId }: Props) => {
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ['resumenPorProvincia', eleccionId],
|
||||
queryFn: () => getResumenPorProvincia(eleccionId),
|
||||
});
|
||||
|
||||
if (isLoading) return <div>Cargando resultados por provincia...</div>;
|
||||
if (error) return <div>Error al cargar los datos.</div>;
|
||||
|
||||
return (
|
||||
<section className="cards-widget-container">
|
||||
<h2>Resultados elecciones nacionales 2025</h2>
|
||||
<div className="cards-grid">
|
||||
{data?.map(provinciaData => (
|
||||
<ProvinciaCard key={provinciaData.provinciaId} data={provinciaData} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -1,27 +1,35 @@
|
||||
/* src/features/legislativas/provinciales/CongresoWidget.css */
|
||||
.congreso-container {
|
||||
display: flex;
|
||||
/* Se reduce ligeramente el espacio entre el gráfico y el panel */
|
||||
gap: 1rem;
|
||||
gap: 1.5rem;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #e0e0e0;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
max-width: 800px;
|
||||
max-width: 900px;
|
||||
margin: 20px auto;
|
||||
font-family: "Public Sans", system-ui, sans-serif;
|
||||
color: #333333;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.congreso-grafico {
|
||||
/* --- CAMBIO PRINCIPAL: Se aumenta la proporción del gráfico --- */
|
||||
flex: 1 1 65%;
|
||||
flex: 2;
|
||||
min-width: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.congreso-hemiciclo-wrapper {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.congreso-hemiciclo-wrapper.is-hovering .party-block:not(:hover) {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.congreso-grafico svg {
|
||||
@@ -30,35 +38,139 @@
|
||||
animation: fadeIn 0.8s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
/* --- NUEVOS ESTILOS PARA EL FOOTER DEL GRÁFICO --- */
|
||||
.congreso-footer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 1rem 0 1rem;
|
||||
margin-top: auto; /* Empuja el footer a la parte inferior del contenedor flex */
|
||||
font-size: 0.8em;
|
||||
color: #666;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
.footer-legend {
|
||||
display: flex;
|
||||
gap: 1.5rem; /* Espacio entre los items de la leyenda */
|
||||
}
|
||||
|
||||
.footer-legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem; /* Espacio entre el icono y el texto */
|
||||
}
|
||||
|
||||
.footer-timestamp {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* --- ESTILOS PARA HOVER --- */
|
||||
|
||||
/* Estilo base para cada círculo de escaño */
|
||||
.seat-circle {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.party-block {
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.party-block:hover .seat-circle {
|
||||
stroke: #333 !important; /* Borde oscuro para resaltar */
|
||||
stroke-width: 1.5px !important;
|
||||
stroke-opacity: 1;
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
/* CORRECCIÓN: El selector ahora apunta al wrapper correcto */
|
||||
.congreso-hemiciclo-wrapper.is-hovering .party-block:not(:hover) {
|
||||
opacity: 0.3; /* Hacemos el desvanecimiento más pronunciado */
|
||||
}
|
||||
.congreso-grafico svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
animation: fadeIn 0.8s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: scale(0.9); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
/* --- INICIO DE NUEVOS ESTILOS PARA EL FOOTER DEL GRÁFICO --- */
|
||||
.congreso-footer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 0.5rem 0 0.5rem;
|
||||
margin-top: auto;
|
||||
font-size: 0.8em;
|
||||
color: #666;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.footer-legend {
|
||||
display: flex;
|
||||
gap: 1.25rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer-legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
/* Creamos una clase base para ambos iconos para compartir tamaño */
|
||||
.legend-icon {
|
||||
display: inline-block;
|
||||
width: 14px; /* Tamaño base para ambos iconos */
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Estilo para el icono de "Bancas en juego" (círculo sólido) */
|
||||
.legend-icon--solid {
|
||||
background-color: #888;
|
||||
border: 1px solid #777;
|
||||
}
|
||||
|
||||
/* Estilo para el icono de "Bancas previas" (anillo translúcido) */
|
||||
.legend-icon--ring {
|
||||
background-color: rgba(136, 136, 136, 0.3); /* #888 con opacidad */
|
||||
border: 1px solid #888; /* Borde sólido del mismo color */
|
||||
}
|
||||
|
||||
.footer-timestamp {
|
||||
font-weight: 500;
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
.congreso-summary {
|
||||
/* --- CAMBIO PRINCIPAL: Se reduce la proporción del panel de datos --- */
|
||||
flex: 1 1 35%;
|
||||
flex: 1;
|
||||
border-left: 1px solid #e0e0e0;
|
||||
/* Se reduce el padding para dar aún más espacio al gráfico */
|
||||
padding-left: 1rem;
|
||||
padding-left: 1.25rem; /* Un poco más de padding */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.congreso-summary h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.75rem; /* Margen inferior reducido */
|
||||
font-size: 1.4em;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.chamber-tabs {
|
||||
display: flex;
|
||||
margin-bottom: 1.5rem;
|
||||
margin-bottom: 1rem; /* Margen inferior reducido */
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
@@ -66,7 +178,7 @@
|
||||
|
||||
.chamber-tabs button {
|
||||
flex: 1;
|
||||
padding: 0.75rem 0.5rem;
|
||||
padding: 0.5rem 0.5rem;
|
||||
border: none;
|
||||
background-color: #f8f9fa;
|
||||
color: #6c757d;
|
||||
@@ -94,7 +206,7 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-bottom: 0.25rem; /* Margen inferior muy reducido */
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
@@ -107,7 +219,15 @@
|
||||
.congreso-summary hr {
|
||||
border: none;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
margin: 1.5rem 0;
|
||||
margin: 1rem 0; /* Margen vertical reducido */
|
||||
}
|
||||
|
||||
/* Contenedor de la lista de partidos para aplicar el scroll */
|
||||
.partido-lista-container {
|
||||
flex-grow: 1; /* Ocupa el espacio vertical disponible */
|
||||
overflow-y: auto; /* Muestra el scrollbar si es necesario */
|
||||
min-height: 0; /* Truco de Flexbox para que el scroll funcione */
|
||||
padding-right: 8px; /* Espacio para el scrollbar */
|
||||
}
|
||||
|
||||
.partido-lista {
|
||||
@@ -119,14 +239,14 @@
|
||||
.partido-lista li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
margin-bottom: 0.85rem; /* Un poco más de espacio entre items */
|
||||
}
|
||||
|
||||
.partido-color-box {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 3px;
|
||||
margin-right: 10px;
|
||||
width: 16px; /* Cuadro de color más grande */
|
||||
height: 16px;
|
||||
border-radius: 4px; /* Un poco más cuadrado */
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -139,19 +259,54 @@
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
/* --- Media Query para Responsividad Móvil --- */
|
||||
/* --- Media Query para Responsividad Móvil (HASTA 768px) --- */
|
||||
@media (max-width: 768px) {
|
||||
.congreso-container {
|
||||
flex-direction: column;
|
||||
padding: 1.5rem;
|
||||
padding: 0.5rem;
|
||||
height: auto;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.congreso-summary {
|
||||
border-left: none;
|
||||
padding-left: 0;
|
||||
margin-top: 2rem;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
|
||||
.partido-lista-container {
|
||||
overflow-y: visible;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.congreso-footer {
|
||||
flex-direction: column; /* Apila la leyenda y el timestamp verticalmente */
|
||||
align-items: flex-start; /* Alinea todo a la izquierda */
|
||||
gap: 0.5rem; /* Añade un pequeño espacio entre la leyenda y el timestamp */
|
||||
padding: 0.75rem 0rem; /* Ajusta el padding para móvil */
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer-legend {
|
||||
gap: 0.75rem; /* Reduce el espacio entre los items de la leyenda */
|
||||
}
|
||||
|
||||
.footer-legend-item{
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.footer-timestamp {
|
||||
font-size: 0.75em; /* Reduce el tamaño de la fuente para que quepa mejor */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* --- Media Query para Escritorio (DESDE 769px en adelante) --- */
|
||||
@media (min-width: 769px) {
|
||||
.congreso-container {
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user