Feat Mapa Tipo Modal

This commit is contained in:
2025-10-27 15:01:07 -03:00
parent 3a43c4a74a
commit 1682d776a0
7 changed files with 221 additions and 38 deletions

View File

@@ -0,0 +1,49 @@
/* src/components/common/Modal.module.css */
.modalOverlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
animation: fadeIn 0.3s ease;
}
.modalContent {
background-color: white;
padding: 10px;
border-radius: 8px;
position: relative;
width: 90%; /* Ancho máximo del widget del panel */
height: 95%; /* Alto máximo */
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
}
.closeButton {
position: absolute;
top: 10px;
right: 10px;
background: transparent;
border: none;
font-size: 2rem;
color: #888;
cursor: pointer;
padding: 0;
line-height: 1;
z-index: 1010;
}
.closeButton:hover {
color: #000;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}

View File

@@ -0,0 +1,34 @@
// src/components/common/ModalWrapper.tsx
import React from 'react';
import styles from './Modal.module.css';
import { IoClose } from "react-icons/io5";
interface Props {
children: React.ReactNode;
}
export const ModalWrapper = ({ children }: Props) => {
const handleClose = () => {
(window as any).EleccionesWidgets.closeModal();
};
// Usamos React.cloneElement para añadir la prop 'isModal' al componente hijo
const childrenWithProps = React.Children.map(children, child => {
if (React.isValidElement(child)) {
// Le pasamos isModal={true} al widget que está adentro
return React.cloneElement(child, { isModal: true } as any);
}
return child;
});
return (
<div className={styles.modalOverlay} onClick={handleClose}>
<div className={styles.modalContent} onClick={(e) => e.stopPropagation()}>
<button className={styles.closeButton} onClick={handleClose}>
<IoClose />
</button>
{childrenWithProps}
</div>
</div>
);
};

View File

@@ -18,7 +18,6 @@ interface Props {
eleccionId: number;
categoriaId: number;
titulo: string;
mapLinkUrl: string;
}
const formatPercent = (num: number | null | undefined) => `${(num || 0).toFixed(2).replace('.', ',')}%`;
@@ -41,7 +40,7 @@ const formatDateTime = (dateString: string | undefined | null) => {
}
};
export const HomeCarouselNacionalWidget = ({ eleccionId, categoriaId, titulo, mapLinkUrl }: Props) => {
export const HomeCarouselNacionalWidget = ({ eleccionId, categoriaId, titulo }: Props) => {
const uniqueId = `swiper-${Math.random().toString(36).substring(2, 9)}`;
const prevButtonClass = `prev-${uniqueId}`;
const nextButtonClass = `next-${uniqueId}`;
@@ -55,14 +54,22 @@ export const HomeCarouselNacionalWidget = ({ eleccionId, categoriaId, titulo, ma
if (isLoading) return <div>Cargando widget...</div>;
if (error || !data) return <div>No se pudieron cargar los datos.</div>;
const handleOpenMap = () => {
(window as any).EleccionesWidgets.openModal('panel-nacional', {
eleccionId: eleccionId.toString(),
// No pasamos ámbito para que muestre la vista país por defecto
categoriaId: categoriaId.toString()
});
};
return (
<div className={styles.homeCarouselWidget}>
<div className={`${styles.widgetHeader} ${styles.headerSingleLine}`}>
<h2 className={styles.widgetTitle}>{titulo}</h2>
<a href={mapLinkUrl} className={`${styles.mapLinkButton} noAjax`}>
<button onClick={handleOpenMap} className={`${styles.mapLinkButton} noAjax`}>
<TfiMapAlt />
<span className={styles.buttonText}>Ver Mapa</span>
</a>
</button>
</div>
<div className={styles.carouselContainer}>

View File

@@ -19,7 +19,6 @@ interface Props {
distritoId: string;
categoriaId: number;
titulo: string;
mapLinkUrl: string;
}
const formatPercent = (num: number | null | undefined) => `${(num || 0).toFixed(2).replace('.', ',')}%`;
@@ -42,7 +41,7 @@ const formatDateTime = (dateString: string | undefined | null) => {
}
};
export const HomeCarouselWidget = ({ eleccionId, distritoId, categoriaId, titulo, mapLinkUrl }: Props) => {
export const HomeCarouselWidget = ({ eleccionId, distritoId, categoriaId, titulo }: Props) => {
const uniqueId = `swiper-${Math.random().toString(36).substring(2, 9)}`;
const prevButtonClass = `prev-${uniqueId}`;
const nextButtonClass = `next-${uniqueId}`;
@@ -56,14 +55,24 @@ export const HomeCarouselWidget = ({ eleccionId, distritoId, categoriaId, titulo
if (isLoading) return <div>Cargando widget...</div>;
if (error || !data) return <div>No se pudieron cargar los datos.</div>;
const handleOpenMap = () => {
(window as any).EleccionesWidgets.openModal('panel-nacional', {
eleccionId: eleccionId.toString(),
// Pasamos un ámbito inicial para que el mapa sepa qué mostrar
ambitoId: `distrito:${distritoId}`,
categoriaId: categoriaId.toString(),
nivel: 'provincia'
});
};
return (
<div className={styles.homeCarouselWidget}>
<div className={`${styles.widgetHeader} ${styles.headerSingleLine}`}>
<h2 className={styles.widgetTitle}>{titulo}</h2>
<a href={mapLinkUrl} className={`${styles.mapLinkButton} noAjax`}>
<button onClick={handleOpenMap} className={`${styles.mapLinkButton} noAjax`}>
<TfiMapAlt />
<span className={styles.buttonText}>Ver Mapa</span>
</a>
</button>
</div>
<div className={styles.carouselContainer}>

View File

@@ -576,4 +576,34 @@
.mobileResultsHeader .headerInfo h4 { font-size: 0.75rem; text-transform: uppercase; }
.mobileResultsHeader .headerInfo .headerActionText { font-size: 0.7rem; }
.mobileCardViewToggle .toggleBtn { padding: 6px 10px; font-size: 0.8rem; }
}
/* Estilos que se aplican al contenedor principal del widget cuando está en un modal */
.panelNacionalContainer.isModal {
width: 100%;
height: 100%;
max-width: 900px;
align-self: center;
margin: 0;
border: none;
border-radius: 0;
box-shadow: none;
display: flex;
flex-direction: column;
}
/* Hacemos que el contenido principal (mapa + resultados) crezca para llenar el espacio */
.panelNacionalContainer.isModal .panelMainContent {
flex-grow: 1;
height: auto; /* Quitamos la altura fija */
min-height: 0; /* Permitimos que se encoja si es necesario */
}
/* Aseguramos que la columna de resultados también sea flexible en el modal */
.panelNacionalContainer.isModal .resultadosColumn {
display: flex;
flex-direction: column;
}
.panelNacionalContainer.isModal .panelPartidosContainer {
flex-grow: 1;
}

View File

@@ -124,6 +124,7 @@ const MobileResultsCard = ({
// --- WIDGET PRINCIPAL ---
interface PanelNacionalWidgetProps {
eleccionId: number;
isModal?: boolean; // Aceptamos la nueva prop opcional
}
type AmbitoState = {
@@ -159,7 +160,7 @@ const PanelContenido = ({ eleccionId, ambitoActual, categoriaId }: { eleccionId:
return <PanelResultados resultados={data.resultadosPanel} estadoRecuento={data.estadoRecuento} />;
};
export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) => {
export const PanelNacionalWidget = ({ eleccionId, isModal = false }: PanelNacionalWidgetProps) => {
const queryClient = useQueryClient();
const [ambitoActual, setAmbitoActual] = useState<AmbitoState>({ id: null, nivel: 'pais', nombre: 'Argentina', provinciaDistritoId: null });
const [categoriaId, setCategoriaId] = useState<number>(3);
@@ -215,8 +216,13 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) =>
isMobile ? styles[`mobile-view-${mobileView}`] : ''
].join(' ');
const containerClasses = [
styles.panelNacionalContainer,
isModal ? styles.isModal : ''
].join(' ');
return (
<div className={styles.panelNacionalContainer}>
<div className={containerClasses}>
<Toaster
position="bottom-center"
containerClassName={styles.widgetToasterContainer}

View File

@@ -2,6 +2,7 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ModalWrapper } from './components/common/ModalWrapper';
/*
import { BancasWidget } from './features/legislativas/provinciales/BancasWidget'
import { CongresoWidget } from './features/legislativas/provinciales/CongresoWidget'
@@ -59,7 +60,7 @@ const WIDGET_MAP: Record<string, React.ElementType> = {
'concejales-por-seccion': ConcejalesPorSeccionWidget,
'resultados-tabla-detallada-por-seccion' : ResultadosTablaDetalladaWidget,
'resultados-tabla-detallada-por-municipio' : ResultadosRankingMunicipioWidget,*/
// Widgets Legislativas Nacionales 2025
'home-carousel': HomeCarouselWidget,
'home-carousel-nacional': HomeCarouselNacionalWidget,
@@ -72,42 +73,89 @@ const WIDGET_MAP: Record<string, React.ElementType> = {
'home-carousel-provincial': HomeCarouselProvincialWidget,
};
// Vite establece `import.meta.env.DEV` a `true` cuando ejecutamos 'npm run dev'
// --- LÓGICA DEL MODAL (AHORA GLOBAL) ---
// 1. Crear un contenedor persistente para el modal en el DOM.
let modalHost = document.getElementById('elecciones-modal-root');
if (!modalHost) {
modalHost = document.createElement('div');
modalHost.id = 'elecciones-modal-root';
document.body.appendChild(modalHost);
}
let modalRoot: ReactDOM.Root | null = null;
const renderWidgets = (container: HTMLElement, props: DOMStringMap) => {
const widgetName = props.eleccionesWidget;
if (widgetName && WIDGET_MAP[widgetName]) {
const WidgetComponent = WIDGET_MAP[widgetName];
const root = ReactDOM.createRoot(container);
root.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<WidgetComponent {...props} />
</QueryClientProvider>
</React.StrictMode>
);
} else {
console.error(`React: ERROR - No se encontró un componente para el nombre de widget: "${widgetName}"`);
}
};
const openModal = (widgetName: string, props: DOMStringMap) => {
const WidgetComponent = WIDGET_MAP[widgetName];
if (WidgetComponent && modalHost) {
if (!modalRoot) {
modalRoot = ReactDOM.createRoot(modalHost);
}
modalRoot.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<ModalWrapper>
<WidgetComponent {...props} />
</ModalWrapper>
</QueryClientProvider>
</React.StrictMode>
);
} else {
console.error(`Error: Se intentó abrir un widget en modal no encontrado: "${widgetName}"`);
}
};
const closeModal = () => {
if (modalRoot) {
modalRoot.unmount();
modalRoot = null;
}
};
// 2. Definir la API pública y asignarla al objeto window.
const publicApi = {
render: renderWidgets,
openModal: openModal,
closeModal: closeModal,
};
(window as any).EleccionesWidgets = publicApi;
// --- LÓGICA DE INICIALIZACIÓN ---
if (import.meta.env.DEV) {
// --- MODO DESARROLLO ---
// Renderizamos nuestra página de showcase en el div#root
// Simplemente renderizamos la app de showcase. El modal ya está disponible globalmente.
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<DevAppLegislativas />
{/* <DevApp /> */}
</QueryClientProvider>
</React.StrictMode>
);
} else {
// --- MODO PRODUCCIÓN ---
// Exponemos la función de renderizado para el bootstrap.js
// La función de renderizado acepta el contenedor y las props
const renderWidgets = (container: HTMLElement, props: DOMStringMap) => {
const widgetName = props.eleccionesWidget;
if (widgetName && WIDGET_MAP[widgetName]) {
const WidgetComponent = WIDGET_MAP[widgetName];
const root = ReactDOM.createRoot(container);
root.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<WidgetComponent {...props} />
</QueryClientProvider>
</React.StrictMode>
);
} else {
console.error(`React: ERROR - No se encontró un componente para el nombre de widget: "${widgetName}"`);
}
};
// La función expuesta ahora se llamará por cada widget, no una sola vez.
(window as any).EleccionesWidgets = {
render: renderWidgets
};
// Procesamos la cola de renderizado si existe (para el script asíncrono)
const renderQueue = (window as any).EleccionesWidgets?.q || [];
renderQueue.forEach((request: { container: HTMLElement, props: DOMStringMap }) => {
publicApi.render(request.container, request.props);
});
}