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;
|
||||
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}>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user