// 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.

Ver el índice Cómo funciona la cooperativa
Cifras del catálogo
{total} Promociones activas
{units} Viviendas en cooperativa
{provs} Municipios
A Calificación energética
{/* Mini reel de fotos */}
); } /* ───────────────────────────────────────────────────────────── 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 => ( ))}
Municipio
{PROVS.map(p => ( ))}
Dormitorios
{['all', '2', '3', '4'].map(d => ( ))}
{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}
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.

Catálogo
{(filtered.length ? filtered : PROMOS).map((p, i) => { const num = pad2(PROMOS.findIndex(x => x.id === p.id) + 1); return ( setHover(p.id)} onMouseLeave={() => setHover(null)}> {num}
{p.name}
{p.locShort} · {p.tipo}
); })}
{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.

Promoción Municipio Tipología Viviendas Estado Desde Cert.
{PROMOS.map((p, i) => ( setHover(p.id)} onMouseLeave={() => setHover(null)}> {pad2(i + 1)} {p.fullName || p.name} {p.locShort} {p.tipo} {p.units} {p.status} {cFmt(p.priceFrom)} A ))}