From 1682d776a01ea58ce8ed63dea4a2bec3f78a96e8 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Mon, 27 Oct 2025 15:01:07 -0300 Subject: [PATCH] Feat Mapa Tipo Modal --- .../src/components/common/Modal.module.css | 49 +++++++++ .../src/components/common/ModalWrapper.tsx | 34 ++++++ .../nacionales/HomeCarouselNacionalWidget.tsx | 15 ++- .../nacionales/HomeCarouselWidget.tsx | 17 ++- .../nacionales/PanelNacional.module.css | 30 +++++ .../nacionales/PanelNacionalWidget.tsx | 10 +- Elecciones-Web/frontend/src/main.tsx | 104 +++++++++++++----- 7 files changed, 221 insertions(+), 38 deletions(-) create mode 100644 Elecciones-Web/frontend/src/components/common/Modal.module.css create mode 100644 Elecciones-Web/frontend/src/components/common/ModalWrapper.tsx diff --git a/Elecciones-Web/frontend/src/components/common/Modal.module.css b/Elecciones-Web/frontend/src/components/common/Modal.module.css new file mode 100644 index 0000000..cf17662 --- /dev/null +++ b/Elecciones-Web/frontend/src/components/common/Modal.module.css @@ -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; } +} \ No newline at end of file diff --git a/Elecciones-Web/frontend/src/components/common/ModalWrapper.tsx b/Elecciones-Web/frontend/src/components/common/ModalWrapper.tsx new file mode 100644 index 0000000..16dae55 --- /dev/null +++ b/Elecciones-Web/frontend/src/components/common/ModalWrapper.tsx @@ -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 ( +
+
e.stopPropagation()}> + + {childrenWithProps} +
+
+ ); +}; \ No newline at end of file diff --git a/Elecciones-Web/frontend/src/features/legislativas/nacionales/HomeCarouselNacionalWidget.tsx b/Elecciones-Web/frontend/src/features/legislativas/nacionales/HomeCarouselNacionalWidget.tsx index 6ad0bda..48dde53 100644 --- a/Elecciones-Web/frontend/src/features/legislativas/nacionales/HomeCarouselNacionalWidget.tsx +++ b/Elecciones-Web/frontend/src/features/legislativas/nacionales/HomeCarouselNacionalWidget.tsx @@ -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
Cargando widget...
; if (error || !data) return
No se pudieron cargar los datos.
; + 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 (

{titulo}

- +
diff --git a/Elecciones-Web/frontend/src/features/legislativas/nacionales/HomeCarouselWidget.tsx b/Elecciones-Web/frontend/src/features/legislativas/nacionales/HomeCarouselWidget.tsx index 84a64e0..7a56b38 100644 --- a/Elecciones-Web/frontend/src/features/legislativas/nacionales/HomeCarouselWidget.tsx +++ b/Elecciones-Web/frontend/src/features/legislativas/nacionales/HomeCarouselWidget.tsx @@ -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
Cargando widget...
; if (error || !data) return
No se pudieron cargar los datos.
; + 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 (

{titulo}

- +
diff --git a/Elecciones-Web/frontend/src/features/legislativas/nacionales/PanelNacional.module.css b/Elecciones-Web/frontend/src/features/legislativas/nacionales/PanelNacional.module.css index 5f300b4..276a143 100644 --- a/Elecciones-Web/frontend/src/features/legislativas/nacionales/PanelNacional.module.css +++ b/Elecciones-Web/frontend/src/features/legislativas/nacionales/PanelNacional.module.css @@ -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; } \ No newline at end of file diff --git a/Elecciones-Web/frontend/src/features/legislativas/nacionales/PanelNacionalWidget.tsx b/Elecciones-Web/frontend/src/features/legislativas/nacionales/PanelNacionalWidget.tsx index c5b77f1..98e9289 100644 --- a/Elecciones-Web/frontend/src/features/legislativas/nacionales/PanelNacionalWidget.tsx +++ b/Elecciones-Web/frontend/src/features/legislativas/nacionales/PanelNacionalWidget.tsx @@ -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 ; }; -export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) => { +export const PanelNacionalWidget = ({ eleccionId, isModal = false }: PanelNacionalWidgetProps) => { const queryClient = useQueryClient(); const [ambitoActual, setAmbitoActual] = useState({ id: null, nivel: 'pais', nombre: 'Argentina', provinciaDistritoId: null }); const [categoriaId, setCategoriaId] = useState(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 ( -
+
= { '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 = { '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( + + + + + + ); + } 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( + + + + + + + + ); + } 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( - {/* */} ); } 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( - - - - - - ); - } 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); + }); } \ No newline at end of file