/* EcoEsperanza — Website UI kit Interactive marketing-site recreation. Self-contained presentational components styled via the DS tokens (site.css). Router switches between Inicio / Proyectos / Educación / Contacto. */ const { useState, useEffect, useRef } = React; /* ---- helpers ------------------------------------------------------------- */ /* Curated, verified Unsplash photos — content confirmed via contact sheet. Warm, hopeful, human-in-nature. Keys describe where each is used. */ const PHOTOS = { heroForest: 'photo-1441974231531-c6227db76b6e', // sunlit forest soilSeedling: 'photo-1532601224476-15c79f2f7a51', // wind turbines on green hills (monitoring/climate) birds: 'photo-1437622368342-7a3d73a34c8f', // sea turtle (biodiversity) treePlanting: 'photo-1591857177580-dc82b9ac4e1e', // urban community garden greenSun: 'photo-1500382017468-9049fed747ef', // wheat field at sunset handsPlant: 'photo-1473773508845-188df298d2d1', // aerial pine forest recycling: 'photo-1591193686104-fddba4d0e4d8', // baled recycled bottles kidsNature: 'photo-1542273917363-3b1817f69a2d', // misty green forest volunteers: 'photo-1560493676-04071c5f467b', // crop field rows turbines: 'photo-1466611653911-95081537e5b7', // wind turbines at sunset leaves: 'photo-1518173946687-a4c8892bbd9f', // dewy green leaves riverForest: 'photo-1498925008800-019c7d59d903', // aerial forest + river canopy: 'photo-1497436072909-60f360e1d4b1', // aerial forest + turquoise river teaching: 'photo-1576085898323-218337e3e43c', // education workshop / talk forestPath: 'photo-1542601906990-b4d3fb778b09', // hands holding a seedling }; const img = (key, w = 1200, h) => `https://images.unsplash.com/${PHOTOS[key] || key}?auto=format&fit=crop&w=${w}${h ? `&h=${h}` : ''}`; const Icon = ({ n, ...p }) => ; function refreshIcons() { if (window.lucide) window.lucide.createIcons(); } /* WhatsApp brand glyph (Lucide has no brand logos). Used in the floating bubble and the contact line. */ const WHATSAPP_NUMBER = '573164208654'; const WhatsAppGlyph = ({ size = 24 }) => ( ); function FloatingWhatsApp() { return ( ); } const NAV = [ { id: 'inicio', label: 'Inicio' }, { id: 'proyectos', label: 'Proyectos' }, { id: 'datos', label: 'Datos' }, { id: 'educacion', label: 'Educación' }, { id: 'contacto', label: 'Contacto' }, ]; /* ---- IQAir / AirVisual live integration --------------------------------- The "Datos" page shows city-level air quality (US AQI) from IQAir's official AirVisual API. Paste your free Community API key below (get one at iqair.com/air-pollution-data-api) and it refreshes every few minutes. Without a key it gracefully shows the reference values seeded here. NOTE: in production, proxy the key through a small backend so it isn't exposed in the browser. Attribution to IQAir is required by their terms. --------------------------------------------------------------------------- */ const IQAIR_API = 'https://api.airvisual.com/v2'; const IQAIR_KEY = ''; // ← pega aquí tu API key gratuita de IQAir // city/state/country must match IQAir's database exactly const CITIES = [ { label: 'Bogotá', country: 'Colombia', state: 'Bogota D.C.', city: 'Bogota', lat: 4.6097, lng: -74.0817, aqi: 75, main: 'PM2.5', temp: 14, hum: 78 }, { label: 'São Paulo', country: 'Brazil', state: 'Sao Paulo', city: 'Sao Paulo', lat: -23.5505, lng: -46.6333, aqi: 56, main: 'PM2.5', temp: 27, hum: 61 }, { label: 'Valencia', country: 'Spain', state: 'Valencia', city: 'Valencia', lat: 39.4699, lng: -0.3763, aqi: 38, main: 'PM2.5', temp: 22, hum: 65 }, { label: 'Provincia de Buenos Aires', country: 'Argentina', state: 'Buenos Aires F.D.', city: 'Buenos Aires', lat: -34.6037, lng: -58.3816, aqi: 82, main: 'PM2.5', temp: 19, hum: 70 }, ]; /* The 7 individual community stations (mirrors the monitor dashboard). Shown as points on the map; pm25 in µg/m³. Wire to OpenSenseMap box ids for live data. */ const MAP_STATIONS = [ { name: 'Bogotá · Centro', city: 'Bogotá', lat: 4.6097, lng: -74.0817, pm25: 22, temp: 14.2 }, { name: 'Bogotá · Kennedy', city: 'Bogotá', lat: 4.6280, lng: -74.1500, pm25: 31, temp: 15.1 }, { name: 'São Paulo · Sé', city: 'São Paulo', lat: -23.5505, lng: -46.6333, pm25: 18, temp: 26.8 }, { name: 'São Paulo · Pinheiros', city: 'São Paulo', lat: -23.5670, lng: -46.7020, pm25: 15, temp: 27.4 }, { name: 'Valencia · Ciutat Vella', city: 'Valencia', lat: 39.4699, lng: -0.3763, pm25: 9, temp: 22.1 }, { name: 'Valencia · Cabanyal', city: 'Valencia', lat: 39.4720, lng: -0.3290, pm25: 12, temp: 22.6 }, { name: 'Provincia de Buenos Aires · Centro', city: 'Provincia de Buenos Aires', lat: -34.6037, lng: -58.3816, pm25: 29, temp: 19.4 }, ]; const IQAIR_POLLUTANT = { p2: 'PM2.5', p1: 'PM10', o3: 'O₃', n2: 'NO₂', s2: 'SO₂', co: 'CO' }; // US AQI categories const aqiBand = (aqi) => aqi <= 50 ? { label: 'Buena', bg: 'var(--success-bg)', fg: 'var(--green-800)', dot: 'var(--success)' } : aqi <= 100 ? { label: 'Moderada', bg: 'var(--warning-bg)', fg: 'var(--gold-700)', dot: 'var(--warning)' } : aqi <= 150 ? { label: 'Dañina (sensibles)', bg: 'var(--danger-bg)', fg: 'var(--red-600)', dot: 'var(--gold-600)' } : { label: 'Dañina', bg: 'var(--danger-bg)', fg: 'var(--red-600)', dot: 'var(--danger)' }; async function fetchCity(c) { const url = `${IQAIR_API}/city?city=${encodeURIComponent(c.city)}&state=${encodeURIComponent(c.state)}&country=${encodeURIComponent(c.country)}&key=${IQAIR_KEY}`; const res = await fetch(url); if (!res.ok) throw new Error('iqair ' + res.status); const j = await res.json(); if (j.status !== 'success') throw new Error('iqair ' + j.status); const p = j.data.current.pollution, w = j.data.current.weather; return { aqi: p.aqius, main: IQAIR_POLLUTANT[p.mainus] || p.mainus, temp: Math.round(w.tp), hum: w.hu }; } function timeAgo(ts) { const s = Math.max(0, Math.round((Date.now() - ts) / 1000)); if (s < 60) return `hace ${s}s`; const m = Math.round(s / 60); return m < 60 ? `hace ${m} min` : `hace ${Math.round(m / 60)} h`; } /* PM2.5 (µg/m³) → band + colour (WHO/EPA-ish thresholds). */ const pmBand = (pm) => pm <= 12 ? { label: 'Buena', hex: '#2BA557', fg: 'var(--green-800)', bg: 'var(--success-bg)' } : pm <= 35 ? { label: 'Moderada', hex: '#E9A81C', fg: 'var(--gold-700)', bg: 'var(--warning-bg)' } : pm <= 55 ? { label: 'Dañina (sensibles)', hex: '#C98A00', fg: 'var(--gold-700)', bg: 'var(--warning-bg)' } : { label: 'Dañina', hex: '#C0392B', fg: 'var(--red-600)', bg: 'var(--danger-bg)' }; /* Interactive Leaflet map of the individual community stations (OpenStreetMap tiles). Replaces an OpenSenseMap iframe — their headers block embedding. Markers are colour-coded by PM2.5; click for a popup with the reading. */ function StationMap({ stations }) { const ref = React.useRef(null); const mapRef = React.useRef(null); const layerRef = React.useRef(null); useEffect(() => { if (!window.L || !ref.current || mapRef.current) return; const map = window.L.map(ref.current, { scrollWheelZoom: false }); window.L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 18, attribution: '© OpenStreetMap', }).addTo(map); mapRef.current = map; setTimeout(() => map.invalidateSize(), 200); return () => { map.remove(); mapRef.current = null; }; }, []); useEffect(() => { const map = mapRef.current; if (!map || !window.L) return; if (layerRef.current) layerRef.current.remove(); const group = window.L.featureGroup(); stations.forEach(s => { const band = pmBand(s.pm25); const m = window.L.circleMarker([s.lat, s.lng], { radius: 10, color: '#fff', weight: 3, fillColor: band.hex, fillOpacity: 1, }); m.bindPopup( `${s.name}
` + `${s.city}
` + `${s.pm25} µg/m³ · ${band.label}
` + `${s.temp}°C · PM2.5` ); m.addTo(group); }); group.addTo(map); layerRef.current = group; const b = group.getBounds(); if (b.isValid()) map.fitBounds(b, { padding: [50, 50], maxZoom: 5 }); }, [stations]); return
; } /* ====================================================================== */ /* Header */ /* ====================================================================== */ function Header({ view, go }) { const [scrolled, setScrolled] = useState(false); const [menuOpen, setMenuOpen] = useState(false); useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 12); window.addEventListener('scroll', onScroll); onScroll(); return () => window.removeEventListener('scroll', onScroll); }, []); // Lock body scroll while the mobile drawer is open useEffect(() => { document.body.style.overflow = menuOpen ? 'hidden' : ''; return () => { document.body.style.overflow = ''; }; }, [menuOpen]); const navTo = (id) => { setMenuOpen(false); go(id); }; return (
navTo('inicio')} style={{ cursor: 'pointer' }}> EcoEsperanza
{/* Mobile drawer */}
setMenuOpen(false)}>
); } /* ====================================================================== */ /* Shared bits */ /* ====================================================================== */ function SectionHead({ eyebrow, icon, title, lead, center, mod }) { return (
{eyebrow && {icon && }{eyebrow}}

{title}

{lead &&

{lead}

}
); } function ProjectCard({ p, go }) { return (
go('proyectos')}>
{p.badge && {p.badge.dot && }{p.badge.text}}
{p.eyebrow}

{p.title}

{p.text}

Leer más
); } function Faq({ items }) { // Independent open/close per item — opening or closing one never affects the // others. Items are always rendered (no scroll-reveal) so none can vanish. const [open, setOpen] = useState(() => new Set([0])); const toggle = (i) => setOpen(prev => { const next = new Set(prev); next.has(i) ? next.delete(i) : next.add(i); return next; }); return (
{items.map((it, i) => { const isOpen = open.has(i); return (

{it.a}

); })}
); } const FAQ_ITEMS = [ { q: '¿Qué es EcoEsperanza?', a: 'EcoEsperanza es una organización sin fines de lucro dedicada a la protección del medio ambiente y la sostenibilidad a través de la educación, el monitoreo climático y la acción comunitaria.' }, { q: '¿Cómo puedo involucrarme?', a: 'Puedes participar en nuestros programas de educación ambiental, voluntariado y proyectos comunitarios para restaurar el entorno natural.' }, { q: '¿Qué proyectos realizan?', a: 'Realizamos proyectos de monitoreo climático, educación ambiental y acciones comunitarias para promover ecosistemas sostenibles y la participación ciudadana.' }, { q: '¿Cómo se financian?', a: 'Nos financiamos a través de donaciones, subvenciones y colaboraciones con otras organizaciones y empresas comprometidas con el medio ambiente.' }, { q: '¿Dónde están ubicados?', a: 'Trabajamos en colaboración con grupos locales en distintas comunidades —con estaciones activas en Bogotá, São Paulo y Valencia— para maximizar nuestro impacto ambiental.' }, ]; /* ====================================================================== */ /* Footer + CTA band */ /* ====================================================================== */ function CtaBand({ go }) { return (

Cada dato cuenta. Cada acción suma. Súmate a EcoEsperanza.

); } function Footer({ go }) { return ( ); } /* ====================================================================== */ /* INICIO */ /* ====================================================================== */ function HomeView({ go }) { const stats = [ { ico: 'radio-tower', val: '7', lbl: 'Estaciones activas' }, { ico: 'map-pin', val: '4', lbl: 'Ciudades en dos continentes' }, { ico: 'calendar', val: '2017', lbl: 'Desde nuestro origen' }, { ico: 'globe', val: '24/7', lbl: 'Datos abiertos en tiempo real' }, ]; const projects = [ { eyebrow: 'Monitoreo', title: 'Red de Estaciones Climáticas Comunitarias', img: img('soilSeedling', 640, 400), text: 'Sensores que miden clima y calidad del aire y publican en abierto en OpenSenseMap.', badge: { tone: 'success', dot: true, text: 'Activa' } }, { eyebrow: 'Biodiversidad', title: 'Observatorio de Biodiversidad Urbana', img: img('birds', 640, 400), text: 'Registro de flora y fauna con iNaturalist para mapear la vida de cada barrio.', badge: { tone: 'data', text: 'iNaturalist' } }, { eyebrow: 'Comunidad', title: 'Educación y Acción Comunitaria', img: img('treePlanting', 640, 400), text: 'Talleres, huertos urbanos, reciclaje y jornadas de reforestación con vecinos.', badge: { tone: 'sun', text: 'Talleres' } }, ]; const gallery = ['greenSun', 'handsPlant', 'recycling', 'kidsNature', 'volunteers', 'turbines', 'leaves', 'riverForest']; return (
{/* Hero */}
{/* Stats */}
{stats.map((s, i) => (
{s.lbl}
{s.val}
))}
{/* Sobre nosotros */}
Sobre Nosotros

Ciencia y comunidad al servicio del planeta

EcoEsperanza nació en Colombia en octubre de 2017 como respuesta a la creciente crisis medioambiental: deforestación, contaminación y pérdida de biodiversidad.

Creemos que la información ambiental debe ser libre, accesible y útil para toda la sociedad. Por eso combinamos tecnología de bajo costo, datos abiertos y participación ciudadana para proteger y restaurar los ecosistemas.

{/* Misión / Visión */}
Nuestra Misión

Proteger y restaurar el medio ambiente

En todo el mundo, mediante el uso de tecnología, datos abiertos y participación ciudadana, promoviendo la educación ambiental y fomentando acciones comunitarias que garanticen la sostenibilidad de los ecosistemas.

Nuestra Visión

Una referencia mundial en conservación

Conectar comunidades, ciencia y tecnología para mantener un presente resiliente y responsable, y crear un futuro consciente y en armonía con la naturaleza.

{/* Proyectos preview */}
{projects.map((p, i) => )}
{/* Galería */}
{gallery.map((key, i) => )}
); } /* ====================================================================== */ /* PROYECTOS */ /* ====================================================================== */ function ProyectosView({ go }) { const areas = [ { ico: 'activity', title: 'Monitoreo Climático y Datos Abiertos', text: 'Redes de sensores que miden temperatura, humedad, presión, calidad del aire y lluvia, publicadas en abierto en OpenSenseMap.' }, { ico: 'graduation-cap', title: 'Educación Ambiental', text: 'Talleres, charlas escolares, cursos en línea y guías didácticas para formar ciudadanía informada y comprometida.' }, { ico: 'hand-heart', title: 'Acción Comunitaria Sostenible', text: 'Reforestaciones, limpiezas de ríos y playas, huertos urbanos y restauración de humedales, co-diseñados con la comunidad.' }, ]; return (
go('inicio')} style={{ cursor: 'pointer' }}>InicioProyectos

Proyectos estratégicos de monitoreo y acción ambiental

Tecnología, ciencia ciudadana y participación comunitaria en distintas ciudades del mundo.

Monitoreo

Red de Estaciones Climáticas Comunitarias

Recopilamos datos en tiempo real sobre clima y calidad del aire en zonas urbanas vulnerables. La información se transmite a plataformas abiertas como OpenSenseMap.

  • Bogotá, Colombia — 2 estaciones tras lluvias extremas y altos niveles de PM2.5.
  • São Paulo, Brasil — 2 estaciones por olas de calor e inundaciones repentinas.
  • Valencia, España — 2 estaciones tras tormentas intensas y contaminación urbana.
  • Provincia de Buenos Aires, Argentina — 1 estación por contaminación urbana.
Biodiversidad

Observatorio de Biodiversidad Urbana

Promovemos el uso de iNaturalist para registrar especies de flora y fauna en las zonas donde operamos, conectando los datos climáticos con la salud de los ecosistemas.

  • Documentar la biodiversidad local.
  • Identificar especies en riesgo o invasoras.
  • Concientizar sobre la riqueza natural del entorno.
{areas.map((a, i) => (

{a.title}

{a.text}

))}
); } /* ====================================================================== */ /* EDUCACIÓN */ /* ====================================================================== */ function EducacionView({ go }) { const programs = [ { ico: 'book-open', title: 'Talleres y charlas', text: 'Sesiones presenciales en escuelas, iglesias y comunidades sobre ecología y cambio climático.' }, { ico: 'graduation-cap', title: 'Cursos en línea', text: 'Formación abierta sobre sostenibilidad, conservación y uso de herramientas de ciencia ciudadana.' }, { ico: 'radio-tower', title: 'Tecnología para conservar', text: 'Aprende a usar sensores ambientales y aplicaciones como iNaturalist y OpenSenseMap.' }, { ico: 'hand-heart', title: 'Voluntariado', text: 'Súmate a reforestaciones, huertos urbanos y jornadas de limpieza con tu comunidad.' }, ]; return (
go('inicio')} style={{ cursor: 'pointer' }}>InicioEducación

Educación ambiental para restaurar el entorno natural

No se puede proteger lo que no se conoce. Formamos ciudadanía capaz de comprender los datos y actuar sobre ellos.

Nuestro enfoque

Aprender, comprender y actuar

Desarrollamos programas educativos para distintos públicos —estudiantes, comunidades locales, iglesias y voluntariados— con talleres, charlas, cursos en línea y campañas de sensibilización.

Los contenidos abarcan desde conceptos básicos de ecología y cambio climático hasta el uso de herramientas tecnológicas para la conservación.

Taller de educación ambiental
{programs.map((p, i) => (

{p.title}

{p.text}

))}
); } /* ====================================================================== */ /* CONTACTO */ /* ====================================================================== */ function ContactoView({ go }) { const [sent, setSent] = useState(false); return (
go('inicio')} style={{ cursor: 'pointer' }}>InicioContacto

Contáctanos

Estamos aquí para ayudarte en la conservación ambiental. Escríbenos y súmate a la red.

{ e.preventDefault(); setSent(true); }}> {sent &&
¡Gracias! Te responderemos pronto.
}
); } /* ====================================================================== */ /* DATOS (en vivo) */ /* ====================================================================== */ function DatosView({ go }) { const [cities, setCities] = useState(CITIES); const [updated, setUpdated] = useState(Date.now()); const [loading, setLoading] = useState(false); const [liveCount, setLiveCount] = useState(0); const refresh = React.useCallback(async () => { setLoading(true); let live = 0; const next = await Promise.all(CITIES.map(async (c) => { if (!IQAIR_KEY) return c; try { const d = await fetchCity(c); live++; return { ...c, aqi: d.aqi, main: d.main, temp: d.temp, hum: d.hum }; } catch (e) { return c; } })); setCities(next); setLiveCount(live); setUpdated(Date.now()); setLoading(false); }, []); useEffect(() => { refresh(); const t = setInterval(refresh, 180000); // auto-refresh every 3 min return () => clearInterval(t); }, [refresh]); const avgAqi = Math.round(cities.reduce((a, c) => a + c.aqi, 0) / cities.length); const worst = cities.reduce((a, c) => c.aqi > a.aqi ? c : a, cities[0]); const kpis = [ { ico: 'building-2', val: String(cities.length), lbl: 'Ciudades monitoreadas' }, { ico: 'wind', val: String(avgAqi), lbl: 'AQI (US) promedio de la red' }, { ico: 'trending-up', val: String(worst.aqi), lbl: `AQI más alto · ${worst.label}` }, { ico: 'building-2', val: '2', lbl: 'Continentes monitoreados' }, ]; return (
go('inicio')} style={{ cursor: 'pointer' }}>InicioDatos en vivo

Calidad del aire, ciudad por ciudad

La información ambiental debe ser libre, accesible y útil para toda la sociedad. Mostramos el índice de calidad del aire (AQI) en tiempo real de las ciudades donde trabajamos, con datos de IQAir.

{kpis.map((k, i) => (
{k.lbl}
{k.val}
))}
Calidad del aire en vivo

Índice por ciudad (AQI US)

En vivo
{cities.map((c, i) => { const band = aqiBand(c.aqi); return (
{c.label} {c.country} · {c.temp}°C · {c.hum}% · {c.main}
{c.aqi} AQI {band.label}
); })}
{liveCount > 0 ? <> {liveCount} ciudad{liveCount === 1 ? '' : 'es'} en directo desde IQAir : <> Mostrando valores de referencia · añade tu API key para datos en vivo} Actualizado {timeAgo(updated)}
{/* Mapa interactivo OpenSenseMap */}
7 estaciones · Mapa © OpenStreetMap Abrir explorador de sensores
); } /* ====================================================================== */ /* App */ /* ====================================================================== */ function App() { const [view, setView] = useState('inicio'); const go = (v) => { setView(v); window.scrollTo({ top: 0, behavior: 'instant' }); }; // refresh icons + wire reveal-on-scroll whenever the view changes useEffect(() => { refreshIcons(); const t = setTimeout(refreshIcons, 120); const els = document.querySelectorAll('.reveal'); let io; if ('IntersectionObserver' in window) { io = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('in'); io.unobserve(e.target); } }); }, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' }); els.forEach(el => io.observe(el)); } else { els.forEach(el => el.classList.add('in')); } // Safety net: never leave content permanently hidden (non-scrolling // contexts, print/PDF export, capture tools). Reveal all after a beat. const safety = setTimeout(() => els.forEach(el => el.classList.add('in')), 1400); return () => { clearTimeout(t); clearTimeout(safety); if (io) io.disconnect(); }; }, [view]); const View = { inicio: HomeView, proyectos: ProyectosView, datos: DatosView, educacion: EducacionView, contacto: ContactoView }[view]; return ( <>
); } ReactDOM.createRoot(document.getElementById('root')).render();