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; eleccionId: number;
categoriaId: number; categoriaId: number;
titulo: string; titulo: string;
mapLinkUrl: string;
} }
const formatPercent = (num: number | null | undefined) => `${(num || 0).toFixed(2).replace('.', ',')}%`; 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 uniqueId = `swiper-${Math.random().toString(36).substring(2, 9)}`;
const prevButtonClass = `prev-${uniqueId}`; const prevButtonClass = `prev-${uniqueId}`;
const nextButtonClass = `next-${uniqueId}`; const nextButtonClass = `next-${uniqueId}`;
@@ -55,14 +54,22 @@ export const HomeCarouselNacionalWidget = ({ eleccionId, categoriaId, titulo, ma
if (isLoading) return <div>Cargando widget...</div>; if (isLoading) return <div>Cargando widget...</div>;
if (error || !data) return <div>No se pudieron cargar los datos.</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 ( return (
<div className={styles.homeCarouselWidget}> <div className={styles.homeCarouselWidget}>
<div className={`${styles.widgetHeader} ${styles.headerSingleLine}`}> <div className={`${styles.widgetHeader} ${styles.headerSingleLine}`}>
<h2 className={styles.widgetTitle}>{titulo}</h2> <h2 className={styles.widgetTitle}>{titulo}</h2>
<a href={mapLinkUrl} className={`${styles.mapLinkButton} noAjax`}> <button onClick={handleOpenMap} className={`${styles.mapLinkButton} noAjax`}>
<TfiMapAlt /> <TfiMapAlt />
<span className={styles.buttonText}>Ver Mapa</span> <span className={styles.buttonText}>Ver Mapa</span>
</a> </button>
</div> </div>
<div className={styles.carouselContainer}> <div className={styles.carouselContainer}>

View File

@@ -19,7 +19,6 @@ interface Props {
distritoId: string; distritoId: string;
categoriaId: number; categoriaId: number;
titulo: string; titulo: string;
mapLinkUrl: string;
} }
const formatPercent = (num: number | null | undefined) => `${(num || 0).toFixed(2).replace('.', ',')}%`; 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 uniqueId = `swiper-${Math.random().toString(36).substring(2, 9)}`;
const prevButtonClass = `prev-${uniqueId}`; const prevButtonClass = `prev-${uniqueId}`;
const nextButtonClass = `next-${uniqueId}`; const nextButtonClass = `next-${uniqueId}`;
@@ -56,14 +55,24 @@ export const HomeCarouselWidget = ({ eleccionId, distritoId, categoriaId, titulo
if (isLoading) return <div>Cargando widget...</div>; if (isLoading) return <div>Cargando widget...</div>;
if (error || !data) return <div>No se pudieron cargar los datos.</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 ( return (
<div className={styles.homeCarouselWidget}> <div className={styles.homeCarouselWidget}>
<div className={`${styles.widgetHeader} ${styles.headerSingleLine}`}> <div className={`${styles.widgetHeader} ${styles.headerSingleLine}`}>
<h2 className={styles.widgetTitle}>{titulo}</h2> <h2 className={styles.widgetTitle}>{titulo}</h2>
<a href={mapLinkUrl} className={`${styles.mapLinkButton} noAjax`}> <button onClick={handleOpenMap} className={`${styles.mapLinkButton} noAjax`}>
<TfiMapAlt /> <TfiMapAlt />
<span className={styles.buttonText}>Ver Mapa</span> <span className={styles.buttonText}>Ver Mapa</span>
</a> </button>
</div> </div>
<div className={styles.carouselContainer}> <div className={styles.carouselContainer}>

View File

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

View File

@@ -2,6 +2,7 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ModalWrapper } from './components/common/ModalWrapper';
/* /*
import { BancasWidget } from './features/legislativas/provinciales/BancasWidget' import { BancasWidget } from './features/legislativas/provinciales/BancasWidget'
import { CongresoWidget } from './features/legislativas/provinciales/CongresoWidget' import { CongresoWidget } from './features/legislativas/provinciales/CongresoWidget'
@@ -72,28 +73,24 @@ const WIDGET_MAP: Record<string, React.ElementType> = {
'home-carousel-provincial': HomeCarouselProvincialWidget, 'home-carousel-provincial': HomeCarouselProvincialWidget,
}; };
// Vite establece `import.meta.env.DEV` a `true` cuando ejecutamos 'npm run dev' // --- LÓGICA DEL MODAL (AHORA GLOBAL) ---
if (import.meta.env.DEV) {
// --- MODO DESARROLLO --- // 1. Crear un contenedor persistente para el modal en el DOM.
// Renderizamos nuestra página de showcase en el div#root let modalHost = document.getElementById('elecciones-modal-root');
ReactDOM.createRoot(document.getElementById('root')!).render( if (!modalHost) {
<React.StrictMode> modalHost = document.createElement('div');
<QueryClientProvider client={queryClient}> modalHost.id = 'elecciones-modal-root';
<DevAppLegislativas /> document.body.appendChild(modalHost);
{/* <DevApp /> */} }
</QueryClientProvider> let modalRoot: ReactDOM.Root | null = null;
</React.StrictMode>
); const renderWidgets = (container: HTMLElement, props: DOMStringMap) => {
} 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; const widgetName = props.eleccionesWidget;
if (widgetName && WIDGET_MAP[widgetName]) { if (widgetName && WIDGET_MAP[widgetName]) {
const WidgetComponent = WIDGET_MAP[widgetName]; const WidgetComponent = WIDGET_MAP[widgetName];
const root = ReactDOM.createRoot(container); const root = ReactDOM.createRoot(container);
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
@@ -104,10 +101,61 @@ if (import.meta.env.DEV) {
} else { } else {
console.error(`React: ERROR - No se encontró un componente para el nombre de widget: "${widgetName}"`); 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. const openModal = (widgetName: string, props: DOMStringMap) => {
(window as any).EleccionesWidgets = { const WidgetComponent = WIDGET_MAP[widgetName];
render: renderWidgets 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 ---
// Simplemente renderizamos la app de showcase. El modal ya está disponible globalmente.
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<DevAppLegislativas />
</QueryClientProvider>
</React.StrictMode>
);
} else {
// --- MODO PRODUCCIÓN ---
// 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);
});
} }