Feat Widgets Controles y Estilos
This commit is contained in:
@@ -1,17 +1,130 @@
|
||||
// src/features/legislativas/nacionales/PanelNacionalWidget.tsx
|
||||
import { useMemo, useState, Suspense } from 'react';
|
||||
import { useMemo, useState, Suspense, useEffect } from 'react';
|
||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { getPanelElectoral } from '../../../apiService';
|
||||
import { MapaNacional } from './components/MapaNacional';
|
||||
import { PanelResultados } from './components/PanelResultados';
|
||||
import { Breadcrumbs } from './components/Breadcrumbs';
|
||||
import { MunicipioSearch } from './components/MunicipioSearch';
|
||||
import './PanelNacional.css';
|
||||
import Select from 'react-select';
|
||||
import type { PanelElectoralDto } from '../../../types/types';
|
||||
import { FiMap, FiList } from 'react-icons/fi';
|
||||
import type { PanelElectoralDto, ResultadoTicker } from '../../../types/types';
|
||||
import { FiMap, FiList, FiChevronDown, FiChevronUp } from 'react-icons/fi';
|
||||
import { useMediaQuery } from './hooks/useMediaQuery';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import { ImageWithFallback } from '../../../components/common/ImageWithFallback';
|
||||
import { assetBaseUrl } from '../../../apiService';
|
||||
|
||||
// --- COMPONENTE INTERNO PARA LA TARJETA DE RESULTADOS EN MÓVIL ---
|
||||
interface MobileResultsCardProps {
|
||||
eleccionId: number;
|
||||
ambitoId: string | null;
|
||||
categoriaId: number;
|
||||
ambitoNombre: string;
|
||||
ambitoNivel: 'pais' | 'provincia' | 'municipio';
|
||||
}
|
||||
|
||||
const formatPercent = (num: number) => `${(num || 0).toFixed(2).replace('.', ',')}%`;
|
||||
|
||||
// --- SUB-COMPONENTE PARA UNA FILA DE RESULTADO ---
|
||||
const ResultRow = ({ partido }: { partido: ResultadoTicker }) => (
|
||||
<div className="mobile-result-row" style={{ borderLeftColor: partido.color || '#ccc' }}>
|
||||
<div className="mobile-result-logo" style={{ backgroundColor: partido.color || '#e9ecef' }}>
|
||||
<ImageWithFallback src={partido.logoUrl || undefined} fallbackSrc={`${assetBaseUrl}/default-avatar.png`} alt={partido.nombre} />
|
||||
</div>
|
||||
<div className="mobile-result-info">
|
||||
{partido.nombreCandidato ? (
|
||||
<>
|
||||
<span className="mobile-result-party-name">{partido.nombreCandidato}</span>
|
||||
<span className="mobile-result-candidate-name">{partido.nombreCorto || partido.nombre}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="mobile-result-party-name">{partido.nombreCorto || partido.nombre}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mobile-result-stats">
|
||||
<strong>{formatPercent(partido.porcentaje)}</strong>
|
||||
<span>{partido.votos.toLocaleString('es-AR')}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// --- COMPONENTE REFACTORIZADO PARA LA TARJETA MÓVIL ---
|
||||
interface MobileResultsCardProps {
|
||||
eleccionId: number;
|
||||
ambitoId: string | null;
|
||||
categoriaId: number;
|
||||
ambitoNombre: string;
|
||||
ambitoNivel: 'pais' | 'provincia' | 'municipio';
|
||||
mobileView: 'mapa' | 'resultados';
|
||||
setMobileView: (view: 'mapa' | 'resultados') => void;
|
||||
}
|
||||
|
||||
const MobileResultsCard = ({
|
||||
eleccionId, ambitoId, categoriaId, ambitoNombre, ambitoNivel, mobileView, setMobileView
|
||||
}: MobileResultsCardProps) => {
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const { data } = useSuspenseQuery<PanelElectoralDto>({
|
||||
queryKey: ['panelElectoral', eleccionId, ambitoId, categoriaId],
|
||||
queryFn: () => getPanelElectoral(eleccionId, ambitoId, categoriaId),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setIsExpanded(ambitoNivel === 'municipio');
|
||||
}, [ambitoNivel]);
|
||||
|
||||
const topResults = data.resultadosPanel.slice(0, 4);
|
||||
|
||||
if (topResults.length === 0 && ambitoNivel === 'pais') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`mobile-results-card-container ${isExpanded ? 'expanded' : ''} view-${mobileView}`}>
|
||||
{/* Sección Colapsable con Resultados */}
|
||||
<div className="collapsible-section">
|
||||
<div className="mobile-results-header" onClick={() => setIsExpanded(!isExpanded)}>
|
||||
<div className="header-info">
|
||||
<h4>{ambitoNombre}</h4>
|
||||
{/* Se añade una clase para estilizar este texto específicamente */}
|
||||
<span className="header-action-text">{isExpanded ? 'Ocultar resultados' : 'Ver top 4'}</span>
|
||||
</div>
|
||||
<div className="header-toggle-icon">
|
||||
{isExpanded ? <FiChevronUp /> : <FiChevronDown />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mobile-results-content">
|
||||
{topResults.length > 0 ? (
|
||||
topResults.map(partido => <ResultRow key={partido.id} partido={partido} />)
|
||||
) : (
|
||||
<p className="no-results-text">No hay resultados para esta selección.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Fijo con Botones de Navegación */}
|
||||
<div className="mobile-card-view-toggle">
|
||||
<button
|
||||
className={`toggle-btn ${mobileView === 'mapa' ? 'active' : ''}`}
|
||||
onClick={() => setMobileView('mapa')}
|
||||
>
|
||||
<FiMap />
|
||||
<span>Mapa</span>
|
||||
</button>
|
||||
<button
|
||||
className={`toggle-btn ${mobileView === 'resultados' ? 'active' : ''}`}
|
||||
onClick={() => setMobileView('resultados')}
|
||||
>
|
||||
<FiList />
|
||||
<span>Resultados</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// --- WIDGET PRINCIPAL ---
|
||||
interface PanelNacionalWidgetProps {
|
||||
eleccionId: number;
|
||||
}
|
||||
@@ -42,7 +155,6 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) =>
|
||||
const [categoriaId, setCategoriaId] = useState<number>(2);
|
||||
const [isPanelOpen, setIsPanelOpen] = useState(true);
|
||||
const [mobileView, setMobileView] = useState<'mapa' | 'resultados'>('mapa');
|
||||
// --- DETECCIÓN DE VISTA MÓVIL ---
|
||||
const isMobile = useMediaQuery('(max-width: 800px)');
|
||||
|
||||
const handleAmbitoSelect = (nuevoAmbitoId: string, nuevoNivel: 'provincia' | 'municipio', nuevoNombre: string) => {
|
||||
@@ -91,62 +203,55 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) =>
|
||||
classNamePrefix="categoria-selector"
|
||||
isSearchable={false}
|
||||
/>
|
||||
<Breadcrumbs
|
||||
nivel={ambitoActual.nivel}
|
||||
nombreAmbito={ambitoActual.nombre}
|
||||
nombreProvincia={ambitoActual.provinciaNombre}
|
||||
onReset={handleResetToPais}
|
||||
onVolverProvincia={handleVolverAProvincia}
|
||||
/>
|
||||
</div>
|
||||
{/* --- 2. NUEVO CONTENEDOR PARA BREADCRUMBS Y BUSCADOR --- */}
|
||||
<div className="header-bottom-row">
|
||||
<Breadcrumbs
|
||||
nivel={ambitoActual.nivel}
|
||||
nombreAmbito={ambitoActual.nombre}
|
||||
nombreProvincia={ambitoActual.provinciaNombre}
|
||||
onReset={handleResetToPais}
|
||||
onVolverProvincia={handleVolverAProvincia}
|
||||
/>
|
||||
{/* --- 3. RENDERIZADO CONDICIONAL DEL BUSCADOR --- */}
|
||||
{ambitoActual.nivel === 'provincia' && ambitoActual.provinciaDistritoId && (
|
||||
<MunicipioSearch
|
||||
distritoId={ambitoActual.provinciaDistritoId}
|
||||
onMunicipioSelect={(municipioId, municipioNombre) =>
|
||||
handleAmbitoSelect(municipioId, 'municipio', municipioNombre)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
<main className={`panel-main-content ${!isPanelOpen ? 'panel-collapsed' : ''} ${isMobile ? `mobile-view-${mobileView}` : ''}`}>
|
||||
<div className="mapa-column">
|
||||
<button className="panel-toggle-btn" onClick={() => setIsPanelOpen(!isPanelOpen)} title={isPanelOpen ? "Ocultar panel" : "Mostrar panel"}>
|
||||
{isPanelOpen ? '›' : '‹'}
|
||||
</button>
|
||||
<button className="panel-toggle-btn" onClick={() => setIsPanelOpen(!isPanelOpen)} title={isPanelOpen ? "Ocultar panel" : "Mostrar panel"}> {isPanelOpen ? '›' : '‹'} </button>
|
||||
|
||||
<Suspense fallback={<div className="spinner" />}>
|
||||
<MapaNacional
|
||||
eleccionId={eleccionId}
|
||||
categoriaId={categoriaId}
|
||||
nivel={ambitoActual.nivel}
|
||||
nombreAmbito={ambitoActual.nombre}
|
||||
nombreProvinciaActiva={ambitoActual.provinciaNombre}
|
||||
provinciaDistritoId={ambitoActual.provinciaDistritoId ?? null}
|
||||
onAmbitoSelect={handleAmbitoSelect}
|
||||
onVolver={ambitoActual.nivel === 'municipio' ? handleVolverAProvincia : handleResetToPais}
|
||||
isMobileView={isMobile}
|
||||
/>
|
||||
<MapaNacional eleccionId={eleccionId} categoriaId={categoriaId} nivel={ambitoActual.nivel} nombreAmbito={ambitoActual.nombre} nombreProvinciaActiva={ambitoActual.provinciaNombre} provinciaDistritoId={ambitoActual.provinciaDistritoId ?? null} onAmbitoSelect={handleAmbitoSelect} onVolver={ambitoActual.nivel === 'municipio' ? handleVolverAProvincia : handleResetToPais} isMobileView={isMobile} />
|
||||
</Suspense>
|
||||
</div>
|
||||
<div className="resultados-column">
|
||||
<Suspense fallback={<div className="spinner" />}>
|
||||
<PanelContenido
|
||||
eleccionId={eleccionId}
|
||||
ambitoActual={ambitoActual}
|
||||
categoriaId={categoriaId}
|
||||
/>
|
||||
<PanelContenido eleccionId={eleccionId} ambitoActual={ambitoActual} categoriaId={categoriaId} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* --- NUEVO CONTROLADOR DE VISTA PARA MÓVIL --- */}
|
||||
<div className="mobile-view-toggle">
|
||||
<button
|
||||
className={`toggle-btn ${mobileView === 'mapa' ? 'active' : ''}`}
|
||||
onClick={() => setMobileView('mapa')}
|
||||
>
|
||||
<FiMap />
|
||||
<span>Mapa</span>
|
||||
</button>
|
||||
<button
|
||||
className={`toggle-btn ${mobileView === 'resultados' ? 'active' : ''}`}
|
||||
onClick={() => setMobileView('resultados')}
|
||||
>
|
||||
<FiList />
|
||||
<span>Resultados</span>
|
||||
</button>
|
||||
</div>
|
||||
<Suspense fallback={null}>
|
||||
{isMobile && (
|
||||
<MobileResultsCard
|
||||
eleccionId={eleccionId}
|
||||
ambitoId={ambitoActual.id}
|
||||
categoriaId={categoriaId}
|
||||
ambitoNombre={ambitoActual.nombre}
|
||||
ambitoNivel={ambitoActual.nivel}
|
||||
mobileView={mobileView}
|
||||
setMobileView={setMobileView}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user