Feat Mapa Tipo Modal
This commit is contained in:
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -576,4 +576,34 @@
|
|||||||
.mobileResultsHeader .headerInfo h4 { font-size: 0.75rem; text-transform: uppercase; }
|
.mobileResultsHeader .headerInfo h4 { font-size: 0.75rem; text-transform: uppercase; }
|
||||||
.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;
|
||||||
}
|
}
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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'
|
||||||
@@ -59,7 +60,7 @@ const WIDGET_MAP: Record<string, React.ElementType> = {
|
|||||||
'concejales-por-seccion': ConcejalesPorSeccionWidget,
|
'concejales-por-seccion': ConcejalesPorSeccionWidget,
|
||||||
'resultados-tabla-detallada-por-seccion' : ResultadosTablaDetalladaWidget,
|
'resultados-tabla-detallada-por-seccion' : ResultadosTablaDetalladaWidget,
|
||||||
'resultados-tabla-detallada-por-municipio' : ResultadosRankingMunicipioWidget,*/
|
'resultados-tabla-detallada-por-municipio' : ResultadosRankingMunicipioWidget,*/
|
||||||
|
|
||||||
// Widgets Legislativas Nacionales 2025
|
// Widgets Legislativas Nacionales 2025
|
||||||
'home-carousel': HomeCarouselWidget,
|
'home-carousel': HomeCarouselWidget,
|
||||||
'home-carousel-nacional': HomeCarouselNacionalWidget,
|
'home-carousel-nacional': HomeCarouselNacionalWidget,
|
||||||
@@ -72,42 +73,89 @@ 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) ---
|
||||||
|
|
||||||
|
// 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) {
|
if (import.meta.env.DEV) {
|
||||||
// --- MODO DESARROLLO ---
|
// --- 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(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<DevAppLegislativas />
|
<DevAppLegislativas />
|
||||||
{/* <DevApp /> */}
|
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// --- MODO PRODUCCIÓN ---
|
// --- MODO PRODUCCIÓN ---
|
||||||
// Exponemos la función de renderizado para el bootstrap.js
|
// Procesamos la cola de renderizado si existe (para el script asíncrono)
|
||||||
// La función de renderizado acepta el contenedor y las props
|
const renderQueue = (window as any).EleccionesWidgets?.q || [];
|
||||||
const renderWidgets = (container: HTMLElement, props: DOMStringMap) => {
|
renderQueue.forEach((request: { container: HTMLElement, props: DOMStringMap }) => {
|
||||||
const widgetName = props.eleccionesWidget;
|
publicApi.render(request.container, request.props);
|
||||||
|
});
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user