// KSANET — Promociones (listado v2)
// Editorial: portada índice, atlas de la Comunidad de Madrid, fichas archivadas.
const { useState, useMemo, useRef, useEffect } = React;
function cFmt(n) {
return new Intl.NumberFormat('es-ES', { maximumFractionDigits: 0 }).format(n) + ' €';
}
function pad2(n) { return String(n).padStart(2, '0'); }
/* ─────────────────────────────────────────────────────────────
HERO · Portada índice
───────────────────────────────────────────────────────────── */
function PromosHero() {
const total = PROMOS.length;
const units = PROMOS.reduce((a, p) => a + p.units, 0);
const provs = new Set(PROMOS.map(p => p.locShort)).size;
return (
N.º 07
·
Atlas KSANET · 2026
Comunidad de Madrid
Siete cooperativas
en obra. Una sola
forma de construir.
Cada promoción es una cooperativa de socios. El precio es el coste real,
auditado mes a mes. Sin promotor, sin margen oculto. Filtra por estado, zona o tipología.
Cifras del catálogo
{total}
Promociones activas
{units}
Viviendas en cooperativa
{provs}
Municipios
A
Calificación energética
{/* Mini reel de fotos */}
{PROMOS.flatMap(p => (p.photos || []).slice(0, 1).map((src, i) => (
{p.locShort}
)))}
{/* fill */}
{PROMOS.filter(p => !p.photos || !p.photos.length).map(p => (
{p.locShort}
))}
);
}
/* ─────────────────────────────────────────────────────────────
FILTROS
───────────────────────────────────────────────────────────── */
function PromoFilters({ filter, setFilter, total }) {
const STATUSES = [
{ k: 'all', label: 'Todas' },
{ k: 'comercial', label: 'Comercialización' },
{ k: 'obra', label: 'En obra' },
{ k: 'proxima', label: 'Próximamente' },
{ k: 'entregada', label: 'Entregadas' },
];
const PROVS = [...new Set(PROMOS.map(p => p.locShort))];
return (
Estado
{STATUSES.map(s => (
setFilter({ ...filter, status: s.k })}>
{s.label}
))}
Municipio
setFilter({ ...filter, prov: 'all' })}>Todos
{PROVS.map(p => (
setFilter({ ...filter, prov: p })}>
{p}
))}
Dormitorios
{['all', '2', '3', '4'].map(d => (
setFilter({ ...filter, dorm: d })}>
{d === 'all' ? 'Cualquiera' : d + ' dorm'}
))}
{pad2(total)} resultados
);
}
/* ─────────────────────────────────────────────────────────────
ATLAS · mapa real (Leaflet + OpenStreetMap)
Mapa interactivo de la Comunidad de Madrid con marcadores
numerados sincronizados con el listado lateral.
───────────────────────────────────────────────────────────── */
function MadridAtlas({ promos, hover, setHover }) {
const focus = promos.find(p => p.id === hover) || promos[0];
const mapRef = useRef(null);
const mapEl = useRef(null);
const markersRef = useRef({});
// Init mapa
useEffect(() => {
if (!mapEl.current || mapRef.current) return;
if (typeof L === 'undefined') return;
const map = L.map(mapEl.current, {
center: [40.45, -3.65],
zoom: 9,
zoomControl: false,
attributionControl: true,
scrollWheelZoom: false,
});
// Cartografía monocroma editorial (Stadia Stamen Toner Lite — sin pedir clave si origen permitido)
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png', {
maxZoom: 18,
attribution: '© OpenStreetMap · © CARTO',
}).addTo(map);
// Capa labels separada (sutil)
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}{r}.png', {
maxZoom: 18,
pane: 'shadowPane',
}).addTo(map);
L.control.zoom({ position: 'bottomright' }).addTo(map);
mapRef.current = map;
}, []);
// Añadir marcadores
useEffect(() => {
const map = mapRef.current;
if (!map) return;
// Limpiar
Object.values(markersRef.current).forEach(m => map.removeLayer(m));
markersRef.current = {};
promos.forEach(p => {
if (!p.latlng) return;
const num = pad2(PROMOS.findIndex(x => x.id === p.id) + 1);
const colorVar = p.statusKey === 'comercial' ? 'var(--brand)'
: p.statusKey === 'obra' ? 'var(--gold)'
: p.statusKey === 'proxima' ? 'var(--moss)'
: 'var(--silver-2)';
const icon = L.divIcon({
className: 'atlas-pin',
html: `${num} `,
iconSize: [38, 38],
iconAnchor: [19, 19],
});
const marker = L.marker(p.latlng, { icon, riseOnHover: true })
.addTo(map)
.on('mouseover', () => setHover(p.id))
.on('mouseout', () => setHover(null))
.on('click', () => {
window.location.href = p.id === 'edificio-victoria'
? 'victoria-landing.html'
: `promocion.html?id=${p.id}`;
});
markersRef.current[p.id] = marker;
});
// Encajar a límites
const validPromos = promos.filter(p => p.latlng);
if (validPromos.length) {
const bounds = L.latLngBounds(validPromos.map(p => p.latlng));
map.fitBounds(bounds, { padding: [60, 60], maxZoom: 10 });
}
}, [promos]);
// Hover externo → resalta marcador
useEffect(() => {
Object.entries(markersRef.current).forEach(([id, m]) => {
const el = m.getElement();
if (!el) return;
el.classList.toggle('is-hover', id === hover);
});
}, [hover]);
return (
{/* — Cabecera del atlas — */}
Lám. 01 · Atlas Comunidad de Madrid
40°25′N · 3°42′W · datos OpenStreetMap
N ↑
{/* — Mapa real — */}
{/* Esquina: leyenda */}
Comercializa.
En obra
Próximas
Entregadas
{/* — Panel de preview foto + datos del marcador activo — */}
{focus && (
{pad2(PROMOS.findIndex(x => x.id === focus.id) + 1)}
{focus.locShort} · Madrid
{focus.fullName || focus.name}
{focus.units} viv.
·
{focus.bedrooms[0]}–{focus.bedrooms[focus.bedrooms.length-1]} dorm
·
{focus.priceFromLabel}
)}
);
}
/* ─────────────────────────────────────────────────────────────
FICHA · tarjeta archivada
───────────────────────────────────────────────────────────── */
function PromoListCard({ p, hover, setHover, idx, total }) {
const isHover = hover === p.id;
const num = pad2(PROMOS.findIndex(x => x.id === p.id) + 1);
const sold = p.sold || 0;
const pct = Math.round((sold / p.units) * 100);
return (
setHover(p.id)}
onMouseLeave={() => setHover(null)}>
{num} / {pad2(PROMOS.length)}
{p.code}
{p.status}
{p.tipo}
{p.location}
{p.name}
{(p.description || '').slice(0, 138)}…
Viviendas {p.units}
Dorms {p.bedrooms[0]}–{p.bedrooms[p.bedrooms.length-1]}
Sup. desde {p.surfaceFrom} m²
Calificación energ. A
{p.statusKey !== 'entregada' && p.statusKey !== 'proxima' && (
{sold} / {p.units} adjudicadas
{pct}%
)}
Desde
{cFmt(p.priceFrom)}
Ver promoción →
);
}
/* ─────────────────────────────────────────────────────────────
PÁGINA
───────────────────────────────────────────────────────────── */
function PromosListPage() {
useReveal();
const [filter, setFilter] = useState({ status: 'all', prov: 'all', dorm: 'all' });
const [hover, setHover] = useState(null);
const filtered = useMemo(() => PROMOS.filter(p => {
if (filter.status !== 'all' && p.statusKey !== filter.status) return false;
if (filter.prov !== 'all' && p.locShort !== filter.prov) return false;
if (filter.dorm !== 'all' && !p.bedrooms.includes(parseInt(filter.dorm))) return false;
return true;
}), [filter]);
return (
<>
{/* ATLAS · sección destacada full-width */}
Lám. 01 · Atlas KSANET
Siete cooperativas en la Comunidad de Madrid.
Pasa el ratón por cualquier marcador para previsualizar la promoción.
Cada cooperativa está numerada según el orden del catálogo 2026.
{filtered.length === 0 && (
Sin resultados
Ajusta los filtros o consulta el atlas completo.
)}
{filtered.map((p, i) => (
))}
{/* Banda inferior: índice tabular */}
Índice general · Atlas KSANET 2026
Las siete cooperativas en una sola tabla. Trazabilidad pública.
>
);
}
ReactDOM.createRoot(document.getElementById('root')).render( );