PULSE

import React, { useState, useEffect, useRef } from "react"; // ───────────────────────────────────────────────────────────── // CANÂ Regulation Pulse™ // Take your Nervous System Pulse in 30 Seconds. // ───────────────────────────────────────────────────────────── // ── CONEXIÓN A LA BASE DE DATOS (Supabase) ── // Pega aquí los dos valores de tu proyecto Supabase (ver la guía). // Si los dejas vacíos, la app funciona igual pero el mapa muestra datos de ejemplo. const SUPABASE_URL = "https://ylxasrxzfiiuhlvjedbn.supabase.co"; // ej: https://xxxxxxxx.supabase.co const SUPABASE_KEY = "sb_publishable_r5vzqNjUm_A48dUmpE17lw_lHqZwIE1"; // la "anon public key" const backendReady = () => SUPABASE_URL.length > 0 && SUPABASE_KEY.length > 0; // Detecta la ciudad de la persona automáticamente (sin pedir permiso ni datos) // Intenta varios servicios por si alguno falla o limita peticiones. async function detectCity() { // Servicio 1: ipapi.co try { const res = await fetch("https://ipapi.co/json/"); if (res.ok) { const d = await res.json(); if (d && d.city) return { city: d.city, country: d.country_name || null, countryCode: d.country_code || null }; } } catch (e) { /* sigue al siguiente */ } // Servicio 2: ipwho.is (respaldo) try { const res = await fetch("https://ipwho.is/"); if (res.ok) { const d = await res.json(); if (d && d.city) return { city: d.city, country: d.country || null, countryCode: d.country_code || null }; } } catch (e) { /* sigue al siguiente */ } // Servicio 3: geojs.io (respaldo) try { const res = await fetch("https://get.geojs.io/v1/ip/geo.json"); if (res.ok) { const d = await res.json(); if (d && d.city) return { city: d.city, country: d.country || null, countryCode: d.country_code || null }; } } catch (e) { /* nada */ } return null; } // Guarda un pulso de forma anónima (cinco valores + estado + ciudad) async function savePulse(stateKey, values, place) { if (!backendReady()) return; try { await fetch(`${SUPABASE_URL}/rest/v1/pulses`, { method: "POST", headers: { "Content-Type": "application/json", apikey: SUPABASE_KEY, Prefer: "return=minimal", }, body: JSON.stringify({ state: stateKey, calm: values.calm, clarity: values.clarity, energy: values.energy, presence: values.presence, connection: values.connection, city: place?.city || null, country: place?.country || null, }), }); } catch (e) { /* silencioso */ } } // Lee el reparto de estados, opcionalmente filtrado por ciudad async function fetchCollective(city) { if (!backendReady()) return null; try { const url = city ? `${SUPABASE_URL}/rest/v1/pulse_distribution?city=eq.${encodeURIComponent(city)}` : `${SUPABASE_URL}/rest/v1/pulse_distribution`; const res = await fetch(url, { headers: { apikey: SUPABASE_KEY }, }); if (!res.ok) return null; return await res.json(); } catch (e) { return null; } } const PALETTE = { ink: "#0E1512", // deep forest-black background ink2: "#141E19", // panel bone: "#EDE7D8", // primary text (warm bone) bone2: "#9AA79B", // muted text sage: "#6FA287", // green accent (calm/regulation) gold: "#E4B23C", // yellow accent (energy/highlight) clay: "#C97B4A", // terracotta (activation) line: "rgba(237,231,216,0.12)", }; const METRICS = [ { key: "calm", label: "CALM", prompt: { es: "Hoy mi cuerpo se siente…", en: "Today my body feels…" }, low: { es: "Muy activado", en: "Very activated" }, high: { es: "Muy calmado", en: "Very calm" }, es: "Calma", enName: "Calm", def: { es: "El estado de tu sistema nervioso: en reposo o en alerta.", en: "The state of your nervous system: at rest or on alert." } }, { key: "clarity", label: "CLARITY", prompt: { es: "Mi mente se siente…", en: "My mind feels…" }, low: { es: "Muy saturada", en: "Very cluttered" }, high: { es: "Muy clara", en: "Very clear" }, es: "Claridad", enName: "Clarity", def: { es: "Qué tan enfocada y despejada está tu mente ahora.", en: "How focused and clear your mind is right now." } }, { key: "energy", label: "ENERGY", prompt: { es: "Mi energía hoy es…", en: "My energy today is…" }, low: { es: "Muy baja", en: "Very low" }, high: { es: "Muy alta", en: "Very high" }, es: "Energía", enName: "Energy", def: { es: "Tu carga vital disponible: cansancio o vitalidad.", en: "Your available vital charge: fatigue or vitality." } }, { key: "presence", label: "PRESENCE", prompt: { es: "Me siento…", en: "I feel…" }, low: { es: "Desconectado", en: "Disconnected" }, high: { es: "Muy presente", en: "Very present" }, es: "Presencia", enName: "Presence", def: { es: "Qué tanto estás en el aquí y ahora, no en tu cabeza.", en: "How much you're in the here and now, not in your head." } }, { key: "connection", label: "CONNECTION", prompt: { es: "Me siento…", en: "I feel…" }, low: { es: "Muy aislado", en: "Very isolated" }, high: { es: "Muy conectado", en: "Very connected" }, es: "Conexión", enName: "Connection", def: { es: "Tu sensación de vínculo con otros y con tu entorno.", en: "Your sense of bond with others and your surroundings." } }, ]; // The CANÂ vocabulary — a language, not a score. const PULSE_STATES = [ { key: "grounded", glyph: "🌿", name: "Grounded", esName: "Enraizado", color: "#6FA287", desc: { es: "Tu sistema descansa en calma y seguridad.", en: "Your system rests in calm and safety." } }, { key: "flowing", glyph: "🌊", name: "Flowing", esName: "En Flujo", color: "#4E9AA6", desc: { es: "Energía y calma en equilibrio. Tu mejor estado.", en: "Energy and calm in balance. Your best state." } }, { key: "activated", glyph: "⚡", name: "Activated", esName: "Activado", color: "#E4B23C", desc: { es: "Tu sistema está encendido y en alerta.", en: "Your system is switched on and alert." } }, { key: "overloaded", glyph: "🌫", name: "Overloaded", esName: "Sobrecargado", color: "#C97B4A", desc: { es: "Demasiada carga acumulada, sin descarga.", en: "Too much load built up, with no release." } }, { key: "recovering", glyph: "🕊", name: "Recovering", esName: "Recuperando", color: "#B69ADf", desc: { es: "Tu sistema está descansando y reparándose.", en: "Your system is resting and repairing." } }, ]; // Identity circles (alternate entry mode) const DAY_STATES = [ { key: "flowing", dot: "🟢", label: { es: "Estoy fluyendo.", en: "I'm flowing." }, weight: 1.0 }, { key: "coping", dot: "🟡", label: { es: "Estoy funcionando.", en: "I'm functioning." }, weight: 0.72 }, { key: "surviving", dot: "🟠", label: { es: "Estoy sobreviviendo.", en: "I'm surviving." }, weight: 0.48 }, { key: "depleted", dot: "🔴", label: { es: "Estoy agotado.", en: "I'm depleted." }, weight: 0.28 }, { key: "flooded", dot: "⚫", label: { es: "Estoy completamente desbordado.", en: "I'm completely overwhelmed." }, weight: 0.12 }, ]; // Aggregate country data (illustrative) const COUNTRY_PULSE = [ { key: "recovering", name: "Recovering", pct: 27, color: "#B69ADF" }, { key: "flowing", name: "Flowing", pct: 22, color: "#4E9AA6" }, { key: "activated", name: "Activated", pct: 19, color: "#E4B23C" }, { key: "grounded", name: "Grounded", pct: 18, color: "#6FA287" }, { key: "overloaded", name: "Overloaded", pct: 14, color: "#C97B4A" }, ]; // ── UI strings, bilingual ── const T = { tagline: { es: "Toma el pulso de tu sistema nervioso en 30 segundos.", en: "Take your Nervous System Pulse in 30 Seconds." }, introQ: { es: "¿Cómo está tu sistema nervioso hoy?", en: "How is your nervous system today?" }, introSub: { es: "No es una encuesta. Es tu pulso. Menos de 30 segundos.", en: "Not a survey. It's your pulse. Under 30 seconds." }, takePulse: { es: "Tomar mi pulso", en: "Take my pulse" }, measureHint: { es: "Cinco indicadores. Mueve cada uno según tu día.", en: "Five indicators. Move each one to match your day." }, remeasureHint: { es: "Vuelve a medir. Mueve cada indicador según cómo te sientes ahora.", en: "Measure again. Move each indicator to how you feel now." }, seePulse: { es: "Ver mi pulso", en: "See my pulse" }, todayPulse: { es: "Tu Pulso CANÂ de hoy", en: "Your CANÂ Pulse today" }, recommend: { es: "Hoy te recomendamos", en: "Today we recommend" }, blissSub: { es: "2 minutos · sonido + respiración", en: "2 minutes · sound + breath" }, listen: { es: "▶ Escuchar", en: "▶ Listen" }, sessionOn: { es: "BLISS · en curso", en: "BLISS · in session" }, inhale: { es: "Inhala… exhala…", en: "Inhale… exhale…" }, secondsLeft: { es: "s restantes", en: "s left" }, backToMeasure: { es: "Volver a medir →", en: "Measure again →" }, skipMeasure: { es: "Saltar y medir →", en: "Skip and measure →" }, soundPlaying: { es: "Sonando", en: "Playing" }, soundResume: { es: "Reanudar sonido", en: "Resume sound" }, soundLabelBliss: { es: "música de relajación 432 Hz · campanas · cuenco tibetano · lluvia suave", en: "432 Hz relaxation music · chimes · singing bowl · soft rain" }, soundLabelVitality: { es: "música activadora 432 Hz · ritmo · brillo · beta 16 Hz", en: "432 Hz energizing music · rhythm · sparkle · beta 16 Hz" }, pulseChanged: { es: "Tu Pulso cambió.", en: "Your Pulse changed." }, beforeAfter: { es: "Antes → Después", en: "Before → After" }, seeCollective: { es: "Ver el pulso colectivo →", en: "See the collective pulse →" }, startOver: { es: "Empezar de nuevo", en: "Start over" }, countryQ: { es: "¿Cómo está respirando tu comunidad hoy?", en: "How is your community breathing today?" }, countrySub: { es: "Sin patologías. Sin etiquetas clínicas. Solo el lenguaje CANÂ.", en: "No pathology. No clinical labels. Only the CANÂ language." }, takeAnother: { es: "↺ Tomar otro pulso", en: "↺ Take another pulse" }, recommendCalm: { es: "Tu sistema está activado. Hoy te recomendamos calmar:", en: "Your system is activated. Today we recommend calming:" }, recommendEnergy: { es: "Tu energía está baja. Hoy te recomendamos activar:", en: "Your energy is low. Today we recommend energizing:" }, blissSubShort: { es: "5 min · calmar", en: "5 min · calm" }, vitalitySubShort: { es: "5 min · activar", en: "5 min · energize" }, // Breathing guidance shown during the session breathGuideCalm: { es: "Mira fijamente el centro del círculo. Coloca una mano en el pecho y otra en el abdomen. Y solo respira.", en: "Gaze softly at the center of the circle. Place one hand on your chest and one on your belly. And just breathe.", }, breathGuideEnergy: { es: "Mira el centro del círculo. Respira más rápido y profundo. Siente cómo entra la energía.", en: "Look at the center of the circle. Breathe faster and deeper. Feel the energy coming in.", }, breathGuide: { es: "Mira fijamente el centro del círculo. Coloca una mano en el pecho y otra en el abdomen. Y solo respira.", en: "Gaze softly at the center of the circle. Place one hand on your chest and one on your belly. And just breathe.", }, }; function scoreToState(avg) { // avg 0..100 → a CANÂ word if (avg >= 78) return PULSE_STATES[0]; // grounded if (avg >= 62) return PULSE_STATES[1]; // flowing if (avg >= 46) return PULSE_STATES[2]; // activated if (avg >= 30) return PULSE_STATES[4]; // recovering return PULSE_STATES[3]; // overloaded } // ─── Circular gauge component ─── function Ring({ pct, color, size = 132, stroke = 9, label, value, delay = 0 }) { const [shown, setShown] = useState(0); const r = (size - stroke) / 2; const c = 2 * Math.PI * r; useEffect(() => { const t = setTimeout(() => setShown(pct), delay); return () => clearTimeout(t); }, [pct, delay]); return (
{value != null ? value : `${Math.round(shown)}`}
{label}
); } // ─── Slider ─── function PulseSlider({ metric, value, onChange, lang }) { return (
{metric.label} {value}
{metric.prompt[lang]}
{metric.def[lang]}
onChange(Number(e.target.value))} style={{ width: "100%", accentColor: PALETTE.sage, cursor: "pointer" }} />
{metric.low[lang]} {metric.high[lang]}
); } // ─── Invitación a la web (se usa al final del recorrido) ─── function CanaInvite({ lang, PALETTE }) { return (
{lang === "es" ? "Esto es solo el comienzo." : "This is just the beginning."}
{lang === "es" ? "Descubre cómo Canâ diseña experiencias para regular tu sistema nervioso a través de los sentidos." : "Discover how Canâ designs experiences to regulate your nervous system through the senses."}
{lang === "es" ? "Conoce Canâ →" : "Discover Canâ →"}
); } // ───────────────────────────────────────────────────────────── // BLISS Frequency Engine — 432 Hz base, generado en tiempo real // · Tono base 432 Hz + drone suave (quinta justa) // · Beat binaural: 8 Hz (banda alpha, calma alerta) // · Latido de coherencia: pulso cada ~10 s (0.1 Hz) que guía la respiración // Todo con Web Audio API — sin archivos, frecuencia exacta y limpia. // ───────────────────────────────────────────────────────────── function useBlissFrequency() { const ctxRef = useRef(null); const nodesRef = useRef([]); const [playing, setPlaying] = useState(false); const [paused, setPaused] = useState(false); // Pausar de verdad (mantiene el audio vivo, solo lo suspende) const pause = () => { const ctx = ctxRef.current; if (ctx && ctx.state === "running") { ctx.suspend(); setPaused(true); } }; // Reanudar el mismo audio suspendido const resume = () => { const ctx = ctxRef.current; if (ctx && ctx.state === "suspended") { ctx.resume(); setPaused(false); } }; const stop = () => { const ctx = ctxRef.current; if (ctx) { const now = ctx.currentTime; nodesRef.current.forEach((n) => { if (n.gain) { try { n.gain.gain.cancelScheduledValues(now); n.gain.gain.setTargetAtTime(0, now, 0.4); } catch (e) {} } }); setTimeout(() => { nodesRef.current.forEach((n) => { try { n.osc && n.osc.stop(); } catch (e) {} if (n.timer) clearTimeout(n.timer); }); nodesRef.current = []; try { ctx.close(); } catch (e) {} ctxRef.current = null; }, 700); } else { nodesRef.current.forEach((n) => { if (n.timer) clearTimeout(n.timer); }); } setPlaying(false); }; const start = (mode = "bliss") => { if (ctxRef.current) return; const Ctx = window.AudioContext || window.webkitAudioContext; const ctx = new Ctx(); ctxRef.current = ctx; const nodes = []; const master = ctx.createGain(); master.gain.value = 0; master.gain.setTargetAtTime(mode === "vitality" ? 0.4 : 0.42, ctx.currentTime, mode === "vitality" ? 1.4 : 2.5); master.connect(ctx.destination); // Reverberación (más corta y brillante para vitality, amplia para bliss) let reverb = null; try { const rev = ctx.createConvolver(); const len = ctx.sampleRate * (mode === "vitality" ? 1.4 : 3); const impulse = ctx.createBuffer(2, len, ctx.sampleRate); for (let ch = 0; ch < 2; ch++) { const d = impulse.getChannelData(ch); for (let i = 0; i < len; i++) d[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / len, mode === "vitality" ? 3.5 : 2.8); } rev.buffer = impulse; const revGain = ctx.createGain(); revGain.gain.value = mode === "vitality" ? 0.22 : 0.35; rev.connect(revGain); revGain.connect(master); reverb = rev; } catch (e) {} // Voz: oscilador con panorámica + ganancia (con envío opcional a reverb) const voice = (freq, type, level, pan, toReverb = false) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); const panner = ctx.createStereoPanner ? ctx.createStereoPanner() : null; osc.type = type; osc.frequency.value = freq; gain.gain.value = level; osc.connect(gain); const dest = panner || master; if (panner) { panner.pan.value = pan; gain.connect(panner); panner.connect(master); } else { gain.connect(master); } if (toReverb && reverb) gain.connect(reverb); osc.start(); nodes.push({ osc, gain }); return { osc, gain }; }; // ══════════ BLISS — calmar (theta, lento, cálido) ══════════ if (mode === "bliss") { // 1) Beat binaural THETA (5 Hz): 432 izq / 437 der — casi inaudible, subliminal voice(432, "sine", 0.022, -1); voice(437, "sine", 0.022, 1); // 2) Drone base cálido (216 Hz), apenas un susurro voice(216, "sine", 0.03, 0); // 3) MÚSICA — pads de acordes lentos y suaves afinados en 432 Hz. // Progresión ambient que respira; notas derivadas de A=432. const A = 432; const HZ = { A2: A / 2, // 216 C3: A / 2 * 1.1892, // ~256.9 (Do) D3: A / 2 * 1.3348, // ~288.3 (Re) E3: A * 0.75, // 324 (Mi) F3: A * 0.7937, // ~342.9 (Fa) G3: A * 0.8909, // ~384.9 (Sol) A3: A, // 432 (La) C4: A * 1.1892, // ~513.8 D4: A * 1.3348, // ~576.6 E4: A * 1.5, // 648 G4: A * 1.7818, // ~769.7 }; // Progresión lenta de 4 acordes, tono cálido y abierto const chords = [ [HZ.A2, HZ.C3, HZ.E3, HZ.A3], // Lam [HZ.F3, HZ.A3, HZ.C4], // Fa [HZ.C3, HZ.E3, HZ.G3, HZ.C4], // Do [HZ.G3, HZ.D4, HZ.G4], // Sol ]; const chordDur = 16; // aún más lento y flotante const allNotes = [...new Set(chords.flat())]; const padGains = {}; allNotes.forEach((f) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = "sine"; // sine puro = más suave y dulce osc.frequency.value = f; gain.gain.value = 0; osc.connect(gain); if (reverb) gain.connect(reverb); gain.connect(master); osc.start(); padGains[f] = gain; nodes.push({ osc, gain }); }); let chordIdx = 0; const stepChord = () => { if (!ctxRef.current) return; const now = ctx.currentTime; const active = chords[chordIdx % chords.length]; allNotes.forEach((f) => { const g = padGains[f]; const target = active.includes(f) ? 0.09 : 0; // música al frente g.gain.cancelScheduledValues(now); g.gain.setTargetAtTime(target, now, chordDur * 0.45); // fundido muy largo }); chordIdx++; const t = setTimeout(stepChord, chordDur * 1000); nodes.push({ timer: t }); }; stepChord(); // 4) CAMPANAS — más frecuentes y con más variedad de notas. // Timbre de campana real: fundamental + armónicos con decaimiento largo. const chimeNotes = [HZ.A3, HZ.C4, HZ.D4, HZ.E4, HZ.G4, HZ.G3]; const playChime = (f, level = 0.06) => { if (!ctxRef.current) return; const now = ctx.currentTime; // fundamental + dos armónicos suaves para timbre de campana [[1, 1], [2, 0.4], [3, 0.15]].forEach(([mult, amp]) => { const o = ctx.createOscillator(); const g = ctx.createGain(); o.type = "sine"; o.frequency.value = f * 2 * mult; g.gain.setValueAtTime(0, now); g.gain.linearRampToValueAtTime(level * amp, now + 0.04); g.gain.exponentialRampToValueAtTime(0.0001, now + 5.5); // cola larga y brillante o.connect(g); if (reverb) g.connect(reverb); g.connect(master); o.start(now); o.stop(now + 5.7); }); }; const chime = () => { if (!ctxRef.current) return; const f = chimeNotes[Math.floor(Math.random() * chimeNotes.length)]; playChime(f); const next = 4000 + Math.random() * 5000; // cada 4–9 s (más frecuentes) const t = setTimeout(chime, next); nodes.push({ timer: t }); }; nodes.push({ timer: setTimeout(chime, 2500) }); // 5) CUENCO TIBETANO ocasional — nota grave sostenida que aparece de vez en cuando const bowl = () => { if (!ctxRef.current) return; const now = ctx.currentTime; const o = ctx.createOscillator(); const o2 = ctx.createOscillator(); const g = ctx.createGain(); o.type = "sine"; o.frequency.value = HZ.A2; // 216 o2.type = "sine"; o2.frequency.value = HZ.A2 * 1.003; // leve batido, "vivo" g.gain.setValueAtTime(0, now); g.gain.linearRampToValueAtTime(0.07, now + 0.8); // ataque suave g.gain.exponentialRampToValueAtTime(0.0001, now + 9); // decaimiento muy largo o.connect(g); o2.connect(g); if (reverb) g.connect(reverb); g.connect(master); o.start(now); o2.start(now); o.stop(now + 9.2); o2.stop(now + 9.2); const next = 22000 + Math.random() * 14000; // cada 22–36 s const t = setTimeout(bowl, next); nodes.push({ timer: t }); }; nodes.push({ timer: setTimeout(bowl, 8000) }); // 6) NATURALEZA — lluvia muy suave de fondo const noiseBuf = ctx.createBuffer(1, ctx.sampleRate * 4, ctx.sampleRate); const data = noiseBuf.getChannelData(0); let b0 = 0, b1 = 0, b2 = 0; for (let i = 0; i < data.length; i++) { const white = Math.random() * 2 - 1; b0 = 0.99765 * b0 + white * 0.0990460; b1 = 0.96300 * b1 + white * 0.2965164; b2 = 0.57000 * b2 + white * 1.0526913; data[i] = (b0 + b1 + b2 + white * 0.1848) * 0.16; } const noise = ctx.createBufferSource(); noise.buffer = noiseBuf; noise.loop = true; const rain = ctx.createBiquadFilter(); rain.type = "lowpass"; rain.frequency.value = 1100; rain.Q.value = 0.4; const rainGain = ctx.createGain(); rainGain.gain.value = 0.16; // muy sutil noise.connect(rain); rain.connect(rainGain); rainGain.connect(master); noise.start(); nodes.push({ osc: noise }); // 7) Latido de coherencia — LFO 0.1 Hz que respira el volumen del master const lfo = ctx.createOscillator(); const lfoGain = ctx.createGain(); lfo.type = "sine"; lfo.frequency.value = 0.1; lfoGain.gain.value = 0.06; lfo.connect(lfoGain); lfoGain.connect(master.gain); lfo.start(); nodes.push({ osc: lfo }); } // fin BLISS // ══════════ VITALITY — activar (beta alto, rítmico, brillante) ══════════ if (mode === "vitality") { // 1) Beat binaural BETA (~16 Hz): 432 izq / 448 der — alerta y enfoque voice(432, "sine", 0.05, -1); voice(448, "sine", 0.05, 1); // 2) Música brillante — acordes mayores luminosos afinados en 432 Hz const A = 432; const NB = { A2: A / 2, Csh3: A / 2 * 1.2599, E3: A * 0.75, A3: A, Csh4: A * 1.2599, E4: A * 1.5, Gsh4: A * 1.8877, B4: A * 2.2449, D4: A * 1.3348, Fsh4: A * 1.6818, }; // Progresión mayor alegre: La → Re → Mi → La (más rápida que bliss) const chords = [ [NB.A2, NB.Csh3, NB.E3, NB.A3], // La mayor [NB.A2, NB.D4, NB.Fsh4], // Re mayor [NB.E3, NB.Gsh4, NB.B4], // Mi mayor [NB.A3, NB.Csh4, NB.E4], // La mayor agudo ]; const chordDur = 6; // más ágil const allNotes = [...new Set(chords.flat())]; const padGains = {}; allNotes.forEach((f) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = "sawtooth"; // más brillante y con cuerpo osc.frequency.value = f; gain.gain.value = 0; // filtro para suavizar el sawtooth (que no sea áspero) const filt = ctx.createBiquadFilter(); filt.type = "lowpass"; filt.frequency.value = 2400; filt.Q.value = 0.6; osc.connect(filt); filt.connect(gain); if (reverb) gain.connect(reverb); gain.connect(master); osc.start(); padGains[f] = gain; nodes.push({ osc, gain }); }); let ci = 0; const stepChord = () => { if (!ctxRef.current) return; const now = ctx.currentTime; const active = chords[ci % chords.length]; allNotes.forEach((f) => { const g = padGains[f]; g.gain.cancelScheduledValues(now); g.gain.setTargetAtTime(active.includes(f) ? 0.06 : 0, now, chordDur * 0.3); }); ci++; const tt = setTimeout(stepChord, chordDur * 1000); nodes.push({ timer: tt }); }; stepChord(); // 3) PULSO RÍTMICO — un latido tipo tambor suave que da energía y ritmo const beat = () => { if (!ctxRef.current) return; const now = ctx.currentTime; // "kick" suave: seno grave que cae rápido const o = ctx.createOscillator(); const g = ctx.createGain(); o.type = "sine"; o.frequency.setValueAtTime(160, now); o.frequency.exponentialRampToValueAtTime(60, now + 0.12); g.gain.setValueAtTime(0.14, now); g.gain.exponentialRampToValueAtTime(0.0001, now + 0.28); o.connect(g); g.connect(master); o.start(now); o.stop(now + 0.3); const tt = setTimeout(beat, 600); // ~100 BPM, ritmo activador estable nodes.push({ timer: tt }); }; nodes.push({ timer: setTimeout(beat, 400) }); // 4) CAMPANITAS brillantes agudas — chispa de energía const sparkleNotes = [NB.E4, NB.Gsh4, NB.B4, NB.Csh4]; const sparkle = () => { if (!ctxRef.current) return; const now = ctx.currentTime; const f = sparkleNotes[Math.floor(Math.random() * sparkleNotes.length)]; const o = ctx.createOscillator(); const g = ctx.createGain(); o.type = "triangle"; o.frequency.value = f * 2; g.gain.setValueAtTime(0, now); g.gain.linearRampToValueAtTime(0.05, now + 0.02); g.gain.exponentialRampToValueAtTime(0.0001, now + 1.6); o.connect(g); if (reverb) g.connect(reverb); g.connect(master); o.start(now); o.stop(now + 1.7); const tt = setTimeout(sparkle, 1800 + Math.random() * 2600); nodes.push({ timer: tt }); }; nodes.push({ timer: setTimeout(sparkle, 1500) }); // 5) Brillo de fondo — aire agudo suave (ruido filtrado alto) const nb = ctx.createBuffer(1, ctx.sampleRate * 3, ctx.sampleRate); const nd = nb.getChannelData(0); for (let i = 0; i < nd.length; i++) nd[i] = (Math.random() * 2 - 1) * 0.5; const air = ctx.createBufferSource(); air.buffer = nb; air.loop = true; const hp = ctx.createBiquadFilter(); hp.type = "highpass"; hp.frequency.value = 6000; const airGain = ctx.createGain(); airGain.gain.value = 0.05; air.connect(hp); hp.connect(airGain); airGain.connect(master); air.start(); nodes.push({ osc: air }); } // fin VITALITY nodes.push({ gain: master }); nodesRef.current = nodes; setPlaying(true); }; // Baja el volumen suavemente (~4 s) y luego detiene — para el final de la sesión const fadeOut = () => { const ctx = ctxRef.current; if (!ctx) return; const master = nodesRef.current.find((n) => n.gain && !n.osc); if (master && master.gain) { const now = ctx.currentTime; master.gain.gain.cancelScheduledValues(now); master.gain.gain.setTargetAtTime(0, now, 1.3); // descenso suave } setTimeout(() => stop(), 4200); }; useEffect(() => () => stop(), []); // limpiar al desmontar return { playing, paused, start, stop, fadeOut, pause, resume }; } export default function CanaPulse() { const [screen, setScreen] = useState("intro"); // intro, measure, result, session, remeasure, result2, map const [values, setValues] = useState(Object.fromEntries(METRICS.map((m) => [m.key, 3]))); const [firstResult, setFirstResult] = useState(null); const [secondValues, setSecondValues] = useState(null); const [sessionSeconds, setSessionSeconds] = useState(300); const [lang, setLang] = useState("es"); // "es" | "en" const [collective, setCollective] = useState(null); // datos reales del mapa const [sessionMode, setSessionMode] = useState("bliss"); // "bliss" (calmar) | "vitality" (activar) const [place, setPlace] = useState(null); // { city, country } const bliss = useBlissFrequency(); // Detecta la ciudad una vez al abrir la app (silencioso, sin pedir permiso) useEffect(() => { detectCity().then((p) => { if (p) setPlace(p); }); }, []); const t = (key) => T[key][lang]; // traductor const pct = (v) => Math.round(((v - 1) / 4) * 100); const avg = (obj) => Math.round(METRICS.reduce((s, m) => s + pct(obj[m.key]), 0) / METRICS.length); // Session timer + BLISS frequency (5 minutos reales) useEffect(() => { if (screen !== "session") { bliss.stop(); return; } setSessionSeconds(300); const id = setInterval(() => { setSessionSeconds((s) => { if (s <= 1) { clearInterval(id); bliss.fadeOut(); // baja el volumen suavemente al terminar // Al terminar la música, pasa automáticamente a medir cómo se siente ahora setTimeout(() => setScreen("remeasure"), 5000); return 0; } return s - 1; }); }, 1000); // tiempo real: 1 segundo return () => { clearInterval(id); bliss.stop(); }; }, [screen]); // audio se inicia en el botón "Escuchar" (gesto del usuario) // Carga los datos reales del mapa colectivo (de la ciudad detectada) al abrirlo useEffect(() => { if (screen !== "map") return; fetchCollective(place?.city).then((data) => { if (data && data.length) setCollective(data); }); }, [screen]); const wrap = { minHeight: "100vh", background: `radial-gradient(1200px 600px at 50% -10%, #16221C 0%, ${PALETTE.ink} 55%)`, color: PALETTE.bone, fontFamily: "'Inter', system-ui, sans-serif", display: "flex", flexDirection: "column", alignItems: "center", padding: "0 20px", }; const card = { width: "100%", maxWidth: 480, margin: "0 auto", }; const btn = (primary = true) => ({ width: "100%", padding: "16px", borderRadius: 999, border: "none", background: primary ? PALETTE.sage : "transparent", color: primary ? PALETTE.ink : PALETTE.bone2, fontSize: 15, fontWeight: 600, letterSpacing: "0.02em", cursor: "pointer", boxShadow: primary ? "0 8px 30px rgba(111,162,135,0.25)" : "none", outline: primary ? "none" : `1px solid ${PALETTE.line}`, transition: "transform .15s", }); const langToggle = (
{["es", "en"].map((L) => ( ))}
); const wordmark = (
CANÂ Regulation Pulse {langToggle}
); // ─── INTRO ─── if (screen === "intro") { return (
{wordmark}

{t("introQ")}

{t("introSub")}

); } // ─── MEASURE ─── if (screen === "measure" || screen === "remeasure") { const isRe = screen === "remeasure"; const state = isRe ? secondValues || values : values; const setState = isRe ? setSecondValues : setValues; const cur = state || Object.fromEntries(METRICS.map((m) => [m.key, 3])); return (
{wordmark}

{isRe ? t("remeasureHint") : t("measureHint")}

{METRICS.map((m) => ( setState({ ...cur, [m.key]: v })} /> ))}
); } // ─── RESULT (first) ─── if (screen === "result") { const overall = avg(firstResult || values); const state = scoreToState(overall); // Recomendación: si la energía está baja → VITALITY (activar); si no → BLISS (calmar) const energyPct = pct((firstResult || values).energy); const recMode = energyPct < 50 ? "vitality" : "bliss"; return (
{wordmark}
{t("todayPulse")}
{state.glyph} {lang === "es" ? state.esName : state.name}
{state.desc.es}
{state.desc.en}
{METRICS.map((m, i) => { const p = pct((firstResult || values)[m.key]); const col = p >= 60 ? PALETTE.sage : p >= 40 ? PALETTE.gold : PALETTE.clay; return ; })}
{recMode === "vitality" ? t("recommendEnergy") : t("recommendCalm")}
{recMode === "vitality" ? "VITALITY" : "BLISS"}
{recMode === "vitality" ? t("vitalitySubShort") : t("blissSubShort")}
); } // ─── SESSION ─── if (screen === "session") { const total = 300; const prog = ((total - sessionSeconds) / total) * 100; const isVit = sessionMode === "vitality"; const accent = isVit ? PALETTE.clay : PALETTE.gold; return (
{isVit ? "VITALITY" : "BLISS"} · {lang === "es" ? "en curso" : "in session"}
{/* Frase guía */}
{isVit ? t("breathGuideEnergy") : t("breathGuideCalm")}
{/* Orbe holográfico — respira más rápido en modo activar */}
{isVit ? (lang === "es" ? "Respira… con energía" : "Breathe… with energy") : t("inhale")}
{Math.floor(sessionSeconds / 60)}:{String(Math.floor(sessionSeconds % 60)).padStart(2, "0")}
{/* Control del sonido — pausa/reanuda, SOLO mientras dura la sesión */} {sessionSeconds > 0 && ( <>
{isVit ? t("soundLabelVitality") : t("soundLabelBliss")}
> )} {sessionSeconds === 0 ? ( ) : ( )}
); } // ─── RESULT 2 (comparison) ─── if (screen === "result2") { const before = firstResult || values; const after = secondValues || values; return (
{wordmark}
{t("pulseChanged")}
{t("beforeAfter")}
{METRICS.map((m) => { const b = pct(before[m.key]); const a = pct(after[m.key]); const up = a >= b; return (
{lang === "es" ? m.es : m.enName} {b} {a}
); })}
{/* Invitación a visitar la web */}
); } // ─── MAP (collective) ─── if (screen === "map") { // Usa datos reales si la base de datos está conectada y tiene registros; si no, ejemplo const rows = (collective || COUNTRY_PULSE).map((r) => { const rawKey = (r.key || r.state || "").toString().trim().toLowerCase(); const meta = PULSE_STATES.find((p) => p.key === rawKey); return { key: rawKey, name: meta ? (lang === "es" ? meta.esName : meta.name) : (r.name || r.state || rawKey), glyph: meta ? meta.glyph : "", color: meta ? meta.color : PALETTE.sage, desc: meta ? (lang === "es" ? meta.desc.es : meta.desc.en) : "", pct: Math.round(r.pct), }; }).sort((a, b) => b.pct - a.pct); const isReal = !!collective; return (
{wordmark}

{place?.city ? (lang === "es" ? `¿Cómo está respirando ${place.city} hoy?` : `How is ${place.city} breathing today?`) : t("countryQ")}

{t("countrySub")}

{rows.map((s) => (
{s.name} {s.pct}%
{s.desc && (
{s.desc}
)}
))}
{!isReal && (
{lang === "es" ? "Datos de ejemplo · conecta la base de datos para el mapa real." : "Sample data · connect the database for the live map."}
)} {/* Invitación a visitar la web */}
); } return null; } const keyframes = ` @import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,500;9..144,600&family=Inter:wght@400;500;600&display=swap'); @keyframes canaPulse { 0%,100%{ box-shadow:0 0 0 0 rgba(111,162,135,0.35);} 50%{ box-shadow:0 0 0 18px rgba(111,162,135,0);} } @keyframes breathe { 0%,100%{ transform:scale(0.8); opacity:0.6;} 50%{ transform:scale(1.15); opacity:1;} } @keyframes fadeInSoft { 0%{ opacity:0; transform:translateY(8px);} 100%{ opacity:1; transform:translateY(0);} } @keyframes logoFade { 0%{ opacity:0; transform:scale(0.92);} 100%{ opacity:1; transform:scale(1);} } /* ── Orbe holográfico: capas de color VIVAS que rotan y cambian de tono ── */ .holoLayer { position: absolute; inset: 0; border-radius: 50%; mix-blend-mode: screen; filter: blur(5px); } .holoA { background: radial-gradient(circle at 40% 40%, #1D6FFF 0%, #3B82F6 45%, transparent 68%); animation: holoShiftA 14s ease-in-out infinite, holoSpin 30s linear infinite; } .holoB { background: radial-gradient(circle at 62% 52%, #FF2D55 0%, #FF6B9D 40%, transparent 65%); animation: holoShiftB 18s ease-in-out infinite, holoSpin 40s linear infinite reverse; } .holoC { background: radial-gradient(circle at 50% 66%, #00E676 0%, #22C55E 42%, transparent 66%); animation: holoShiftC 16s ease-in-out infinite, holoSpin 34s linear infinite; } /* transiciona por azul → rojo → verde → blanco y mezclas, con más saturación y brillo */ @keyframes holoShiftA { 0%,100%{ filter:blur(5px) hue-rotate(0deg) saturate(1.7) brightness(1.3);} 50%{ filter:blur(7px) hue-rotate(180deg) saturate(2) brightness(1.4);} } @keyframes holoShiftB { 0%,100%{ filter:blur(6px) hue-rotate(90deg) saturate(1.8) brightness(1.35);} 50%{ filter:blur(5px) hue-rotate(300deg) saturate(2.1) brightness(1.45);} } @keyframes holoShiftC { 0%,100%{ filter:blur(5px) hue-rotate(220deg) saturate(1.7) brightness(1.3);} 50%{ filter:blur(8px) hue-rotate(40deg) saturate(2) brightness(1.4);} } @keyframes holoSpin { from{ transform:rotate(0deg);} to{ transform:rotate(360deg);} } input[type=range]{ -webkit-appearance:none; height:4px; background:${PALETTE.line}; border-radius:99px; } input[type=range]::-webkit-slider-thumb{ -webkit-appearance:none; width:22px; height:22px; border-radius:50%; background:${PALETTE.bone}; border:3px solid ${PALETTE.sage}; cursor:pointer; } input[type=range]::-moz-range-thumb{ width:22px; height:22px; border-radius:50%; background:${PALETTE.bone}; border:3px solid ${PALETTE.sage}; cursor:pointer; } * { box-sizing:border-box; } @media (prefers-reduced-motion: reduce){ *{ animation:none !important; transition:none !important; } } `;