const SUPABASE_URL = "https://fokhpkrzbonieyvcujlw.supabase.co"; const SUPABASE_KEY = "sb_publishable_4vtENh_jt7CcxhYdHk-BJw_H9quDyht"; let dbClient; try { const supabaseLib = window.supabase || (typeof supabase !== 'undefined' ? supabase : null); if (supabaseLib) { dbClient = supabaseLib.createClient(SUPABASE_URL, SUPABASE_KEY); console.log('✅ Supabase client initialized correctly.'); } else { console.error('❌ Supabase library not found. Make sure the CDN script loads before app.js.'); } } catch (e) { console.error("❌ Supabase initialization error:", e); } const AppState = { currentStep: 0, totalSteps: 7, data: { liberacion: "", gratitud: [] }, breathPattern: [4, 2, 6], currentSongUrl: null, isAuthenticated: false, userEmail: null, notificationsEnabled: localStorage.getItem('ritual_notifications_enabled') === 'true', notificationTime: localStorage.getItem('ritual_notification_time') || "22:00", feedbackMode: localStorage.getItem('ritual_feedback_mode') || 'vibration', activeUsers: 1, // Por defecto 1 (el usuario actual) dailyPrompt: null }; const RITUAL_PROMPTS = [ "¿Qué pequeño momento te hizo sonreír hoy?", "¿De qué te sientes más orgulloso/a en este día?", "¿Qué situación difícil lograste manejar hoy?", "¿Qué persona hizo tu día más agradable hoy?", "¿Qué aprendiste sobre ti mismo/a en las últimas 24 horas?", "¿Qué aroma, sonido o imagen te trajo paz hoy?", "¿Qué carga innecesaria decides soltar antes de dormir?", "¿Qué es aquello que esperas con ilusión de mañana?", "¿Cómo cuidaste de tu cuerpo hoy?", "¿Qué palabra define mejor tu intención para el descanso de esta noche?" ]; // Los datos de acceso ahora se validan dinámicamente en el servidor vía Ritual Admin API // Temas Visuales por Paso (Colores de fondo y acentos) const StepThemes = { 'login': { bg: '#14141e', glow1: 'rgba(45, 40, 68, 0.2)', glow2: 'rgba(205, 147, 115, 0.1)' }, 'intro': { bg: '#1a1826', glow1: 'rgba(45, 40, 68, 0.2)', glow2: 'rgba(205, 147, 115, 0.15)' }, 'paso1': { bg: '#1c1a2e', glow1: 'rgba(60, 50, 80, 0.25)', glow2: 'rgba(205, 147, 115, 0.1)' }, 'paso2': { bg: '#181b32', glow1: 'rgba(80, 70, 120, 0.3)', glow2: 'rgba(100, 150, 200, 0.1)' }, // Más azulado para respirar 'paso3': { bg: '#231c26', glow1: 'rgba(120, 60, 60, 0.2)', glow2: 'rgba(205, 147, 115, 0.15)' }, // Cálido para liberar 'paso4': { bg: '#26221c', glow1: 'rgba(205, 147, 115, 0.25)', glow2: 'rgba(255, 230, 200, 0.1)' }, // Dorado para gratitud 'paso5': { bg: '#1c1c2b', glow1: 'rgba(100, 100, 180, 0.2)', glow2: 'rgba(205, 147, 115, 0.1)' }, 'paso6': { bg: '#14141e', glow1: 'rgba(45, 40, 68, 0.3)', glow2: 'rgba(205, 147, 115, 0.2)' }, 'cierre': { bg: '#0a0a0f', glow1: 'rgba(20, 20, 30, 0.4)', glow2: 'rgba(0, 0, 0, 0)' } }; // La playlist se cargará dinámicamente o desde el fallback local let playlist = []; const loadPlaylist = async () => { try { // 1. Intentar cargar desde la API del servidor (Dreamhost) const response = await fetch('api/get_playlist.php'); if (response.ok) { const data = await response.json(); if (data && data.length > 0) { playlist = data; console.log("Playlist cargada desde el servidor"); return; } } } catch (e) { console.log("No se pudo contactar con la API del servidor, usando modo local."); } // 2. Fallback: Cargar desde playlist.js generado por el .bat (local) if (typeof window.playlistData !== 'undefined' && Array.isArray(window.playlistData)) { playlist = window.playlistData; console.log("Playlist cargada desde modo local (.bat)"); } }; // Utilidades de Racha (Streak) const StreakManager = { get: () => { const today = new Date().toDateString(); let lastDate = localStorage.getItem('ritual_last_date'); let streak = parseInt(localStorage.getItem('ritual_streak') || '0'); if (lastDate !== today) { const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); if (lastDate !== yesterday.toDateString() && lastDate) { // Streak broken streak = 0; localStorage.setItem('ritual_streak', 0); } } return streak; }, increment: () => { const today = new Date().toDateString(); let lastDate = localStorage.getItem('ritual_last_date'); let streak = parseInt(localStorage.getItem('ritual_streak') || '0'); if (lastDate !== today) { streak += 1; localStorage.setItem('ritual_last_date', today); localStorage.setItem('ritual_streak', streak); const memberId = sessionStorage.getItem('ritual_member_id'); if (dbClient && memberId) { dbClient.from('ritual_progress').update({ current_streak: streak, last_date: today }).eq('member_id', memberId).then(); } } } }; // Manejo del Frasco de Gratitud const JarManager = { save: (items) => { try { if (!items || items.length === 0) return; let jar = JSON.parse(localStorage.getItem('ritual_gratitude_jar') || '[]'); const newEntries = items.filter(i => i && i.trim() !== '').map(text => ({ text, date: new Date().toLocaleDateString('es-ES') })); if (newEntries.length === 0) return; jar = [...newEntries, ...jar].slice(0, 50); localStorage.setItem('ritual_gratitude_jar', JSON.stringify(jar)); const memberId = sessionStorage.getItem('ritual_member_id'); if (dbClient && memberId) { dbClient.from('ritual_progress').update({ gratitude_jar: jar }).eq('member_id', memberId).then(); } console.log("Datos guardados en el Frasco:", newEntries); } catch (e) { console.error("Error al guardar en el frasco:", e); } }, getAll: () => { try { const data = localStorage.getItem('ritual_gratitude_jar'); return data ? JSON.parse(data) : []; } catch (e) { console.error("Error al leer el frasco:", e); return []; } }, clear: () => localStorage.removeItem('ritual_gratitude_jar') }; // Manejo de Exportación de Gratitud (Imagen) const ExportManager = { generateImage: async () => { const items = JarManager.getAll().slice(0, 5); // Exportar los últimos 5 if (items.length === 0) { alert("No tienes recuerdos para exportar aún."); return; } const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); canvas.width = 1080; canvas.height = 1350; // Formato vertical elegante // 1. Fondo (Gradiente Nocturno) const grad = ctx.createLinearGradient(0, 0, 0, canvas.height); grad.addColorStop(0, '#1a1826'); grad.addColorStop(1, '#0a0a0f'); ctx.fillStyle = grad; ctx.fillRect(0, 0, canvas.width, canvas.height); // 2. Elementos Decorativos (Glows) ctx.globalAlpha = 0.2; ctx.fillStyle = '#cd9373'; ctx.beginPath(); ctx.arc(canvas.width * 0.2, canvas.height * 0.2, 300, 0, Math.PI * 2); ctx.fill(); ctx.filter = 'blur(100px)'; ctx.globalAlpha = 1; ctx.filter = 'none'; // 3. Título ctx.fillStyle = '#fdfbf7'; ctx.font = 'bold 64px serif'; ctx.textAlign = 'center'; ctx.fillText('Mis Recuerdos de Gratitud', canvas.width / 2, 200); ctx.fillStyle = '#cd9373'; ctx.font = 'italic 32px serif'; ctx.fillText('Ritual de Cierre Nocturno', canvas.width / 2, 260); // 4. Contenido (Entradas) let y = 450; ctx.textAlign = 'left'; items.forEach((item, idx) => { // Caja decorativa ctx.fillStyle = 'rgba(255, 255, 255, 0.05)'; ctx.roundRect ? ctx.roundRect(100, y - 60, 880, 160, 20) : ctx.fillRect(100, y - 60, 880, 160); ctx.fill(); // Texto de Gratitud ctx.fillStyle = '#fdfbf7'; ctx.font = '36px sans-serif'; const wrappedText = ExportManager.wrapText(ctx, `"${item.text}"`, 800); wrappedText.forEach((line, i) => { ctx.fillText(line, 140, y + (i * 45)); }); // Fecha ctx.fillStyle = 'rgba(205, 147, 115, 0.6)'; ctx.font = 'bold 24px sans-serif'; ctx.fillText(item.date.toUpperCase(), 140, y + (wrappedText.length * 45) + 20); y += 220; }); // 5. Marca de Agua / Footer ctx.fillStyle = 'rgba(255, 255, 255, 0.2)'; ctx.textAlign = 'center'; ctx.font = '20px sans-serif'; ctx.fillText('Hecho con Amor • app.redcursos.com', canvas.width / 2, canvas.height - 80); // Descarga const dataUrl = canvas.toDataURL('image/png'); const link = document.createElement('a'); link.download = `Gratitud_${new Date().toLocaleDateString().replace(/\//g,'-')}.png`; link.href = dataUrl; link.click(); }, wrapText: (ctx, text, maxWidth) => { const words = text.split(' '); const lines = []; let currentLine = words[0]; for (let i = 1; i < words.length; i++) { const word = words[i]; const width = ctx.measureText(currentLine + " " + word).width; if (width < maxWidth) { currentLine += " " + word; } else { lines.push(currentLine); currentLine = word; } } lines.push(currentLine); return lines; } }; // Sintetizador de sonido "Gong/Cuenco Tibetano" const AudioContext = window.AudioContext || window.webkitAudioContext; let audioCtx; const playTick = () => { try { if (!audioCtx) audioCtx = new AudioContext(); if (audioCtx.state === 'suspended') audioCtx.resume(); const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(880, audioCtx.currentTime); // Tono alto y sutil osc.frequency.exponentialRampToValueAtTime(440, audioCtx.currentTime + 0.1); gain.gain.setValueAtTime(0, audioCtx.currentTime); gain.gain.linearRampToValueAtTime(0.1, audioCtx.currentTime + 0.01); // Muy suave gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.1); osc.connect(gain); gain.connect(audioCtx.destination); osc.start(); osc.stop(audioCtx.currentTime + 0.1); } catch(e) { console.log("Tick audio disabled", e); } }; // Alias para compatibilidad con views.js (paso 3 - quemar texto) const playGong = playTick; // Manejo de Notificaciones PWA const NotificationManager = { init: async () => { if (!('Notification' in window)) return; if (AppState.notificationsEnabled) { NotificationManager.schedule(); } }, requestPermission: async () => { if (!('Notification' in window)) return false; const permission = await Notification.requestPermission(); return permission === 'granted'; }, schedule: () => { // En una PWA real, esto se haría via Push API o Background Sync para ser fiable. // Aquí simulamos la lógica: si el usuario entra y es la hora, o programamos un timeout si la pestaña sigue abierta. console.log("Recordatorio programado para las:", AppState.notificationTime); // Simulación: Guardamos en LS y el SW podría leerlo si tuviera el motor adecuado. // Por ahora, enviamos una de prueba si acaba de activarla. }, sendTest: () => { if (Notification.permission === 'granted') { new Notification("Ritual de Cierre", { body: "Es momento de soltar el día y descansar. 🌙", icon: "https://cdn-icons-png.flaticon.com/512/1154/1154448.png" }); } } }; // Manejo de Presencia (Comunidad Silenciosa) const PresenceManager = { heartbeat: async () => { AppState.activeUsers = Math.floor(Math.random() * 3) + 1; // Fake presence just to keep UI alive sin PHP PresenceManager.updateUI(); }, updateUI: () => { const el = $('#active-community-count'); if (el) { el.textContent = AppState.activeUsers; const container = $('#community-indicator'); if (AppState.activeUsers > 1) { container?.classList.remove('hidden'); container?.classList.add('animate-fade-in'); } } }, start: () => { PresenceManager.heartbeat(); setInterval(PresenceManager.heartbeat, 60000); // Cada minuto } }; const steps = [ { id: 'login', title: 'Acceso Seguro', label: '' }, { id: 'intro', title: 'Ritual de Cierre Nocturno', subtitle: 'Un espacio para soltar el día y descansar con intención', duration: '10 a 15 minutos' }, { id: 'paso1', title: 'Preparar el espacio', label: 'Paso 1' }, { id: 'paso2', title: 'Respiración para soltar', label: 'Paso 2' }, { id: 'paso3', title: 'Liberación emocional', label: 'Paso 3' }, { id: 'paso4', title: 'Gratitud suave', label: 'Paso 4' }, { id: 'paso5', title: 'Afirmaciones nocturnas', label: 'Paso 5' }, { id: 'paso6', title: 'Acompañamiento musical', label: 'Paso 6' }, { id: 'cierre', title: 'Cierre del Ritual', label: 'Fin' } ]; const $ = (selector) => document.querySelector(selector); const root = $('#view-root'); const progressContainer = $('#progress-container'); const progressBar = $('#progress-bar'); const stepIndicator = $('#step-indicator'); const btnPrev = $('#btn-prev'); // Manejador del Flujo de Vistas const FlowManager = { init: async () => { // Cargar preferencias guardadas const savedBreath = localStorage.getItem('ritual_breath_pattern'); if(savedBreath) AppState.breathPattern = JSON.parse(savedBreath); // Inicializar Notificaciones NotificationManager.init(); // Cargar Playlist (Servidor o Local) await loadPlaylist(); // Verificar sesión existente const session = sessionStorage.getItem('ritual_session'); if (session) { AppState.isAuthenticated = true; AppState.userEmail = session; AppState.currentStep = 1; } else { AppState.currentStep = 0; } // Iniciar Comunidad Silenciosa si ya está autenticado if (AppState.isAuthenticated) { PresenceManager.start(); } // Seleccionar pregunta del día si no existe if (!AppState.dailyPrompt) { const randomIndex = Math.floor(Math.random() * RITUAL_PROMPTS.length); AppState.dailyPrompt = RITUAL_PROMPTS[randomIndex]; } FlowManager.renderStep(AppState.currentStep); // Iniciar Comunidad Silenciosa si está autenticado if (AppState.isAuthenticated) { PresenceManager.start(); } // Manejar acciones de Shortcuts PWA const urlParams = new URLSearchParams(window.location.search); const action = urlParams.get('action'); if (AppState.isAuthenticated) { if (action === 'start') { FlowManager.goToStep(2); } else if (action === 'jar') { $('#btn-open-jar')?.click(); } } btnPrev.addEventListener('click', () => { if (AppState.currentStep > 1) { FlowManager.goToStep(AppState.currentStep - 1); } }); $('#btn-settings')?.addEventListener('click', () => { const settingsOverlay = document.createElement('div'); settingsOverlay.id = 'settings-overlay'; settingsOverlay.innerHTML = Views.Settings(); document.body.appendChild(settingsOverlay); if(window.feather) window.feather.replace(); // Eventos del Panel $('#btn-close-settings')?.addEventListener('click', () => settingsOverlay.remove()); $('#set-notify')?.addEventListener('change', async (e) => { const config = $('#notify-config'); if (e.target.checked) { const granted = await NotificationManager.requestPermission(); if (granted) { config.classList.remove('opacity-30', 'pointer-events-none'); NotificationManager.sendTest(); } else { e.target.checked = false; alert("Necesitas autorizar las notificaciones en tu navegador."); } } else { config.classList.add('opacity-30', 'pointer-events-none'); } }); $('#btn-save-settings')?.addEventListener('click', () => { const newPin = $('#set-pin').value; const i = parseInt($('#set-inhale').value); const h = parseInt($('#set-hold').value); const e = parseInt($('#set-exhale').value); const notifyEnabled = $('#set-notify').checked; const notifyTime = $('#set-notify-time').value; // Guardar en LocalStorage if(newPin) { localStorage.setItem('ritual_pin_kaoninja', newPin); // Actualizar PIN en Supabase const memberId = sessionStorage.getItem('ritual_member_id'); if (dbClient && memberId) { dbClient.from('ritual_members').update({ pin: newPin }).eq('id', memberId).then(); } } const pattern = [i, h, e]; localStorage.setItem('ritual_breath_pattern', JSON.stringify(pattern)); AppState.breathPattern = pattern; localStorage.setItem('ritual_notifications_enabled', notifyEnabled); localStorage.setItem('ritual_notification_time', notifyTime); AppState.notificationsEnabled = notifyEnabled; AppState.notificationTime = notifyTime; const feedbackMode = $('#set-feedback-mode').value; localStorage.setItem('ritual_feedback_mode', feedbackMode); AppState.feedbackMode = feedbackMode; if (notifyEnabled) NotificationManager.schedule(); settingsOverlay.remove(); alert("Configuración guardada correctamente."); }); }); }, goToStep: (stepIndex) => { if (stepIndex >= 0 && stepIndex <= steps.length - 1) { root.style.opacity = '0'; root.style.transform = 'translateY(10px)'; setTimeout(() => { AppState.currentStep = stepIndex; FlowManager.renderStep(stepIndex); FlowManager.updateProgress(); FlowManager.updateTheme(); // Actualizar fondo dinámico root.style.opacity = '1'; root.style.transform = 'translateY(0)'; }, 300); } }, updateTheme: () => { const stepId = steps[AppState.currentStep].id; const theme = StepThemes[stepId] || StepThemes['login']; const bg = $('#dynamic-bg'); const glow1 = $('#bg-glow-1'); const glow2 = $('#bg-glow-2'); if(bg) bg.style.backgroundColor = theme.bg; if(glow1) glow1.style.backgroundColor = theme.glow1; if(glow2) glow2.style.backgroundColor = theme.glow2; }, updateProgress: () => { const stepId = steps[AppState.currentStep].id; if (stepId === 'login' || stepId === 'intro') { progressContainer.style.opacity = '0'; setTimeout(() => progressContainer.classList.add('hidden'), 500); } else { progressContainer.classList.remove('hidden'); requestAnimationFrame(() => { progressContainer.style.opacity = '1'; const percentage = (AppState.currentStep / (steps.length - 1)) * 100; progressBar.style.width = `${percentage}%`; if(AppState.currentStep < steps.length - 1) { stepIndicator.textContent = `Paso ${AppState.currentStep - 1} de 6`; } else { stepIndicator.textContent = 'Cierre'; } if (AppState.currentStep > 0) { btnPrev.classList.remove('hidden'); setTimeout(() => btnPrev.style.opacity = '1', 50); } else { btnPrev.style.opacity = '0'; setTimeout(() => btnPrev.classList.add('hidden'), 300); } }); } }, renderStep: (stepIndex) => { const step = steps[stepIndex]; let contentHtml = ''; switch(step.id) { case 'login': contentHtml = Views.Login(step); break; case 'intro': contentHtml = Views.Intro(step); break; case 'paso1': contentHtml = Views.Paso1(step); break; case 'paso2': contentHtml = Views.Paso2(step); break; case 'paso3': contentHtml = Views.Paso3(step); break; case 'paso4': contentHtml = Views.Paso4(step); break; case 'paso5': contentHtml = Views.Paso5(step); break; case 'paso6': contentHtml = Views.Paso6(step); break; case 'cierre': contentHtml = Views.Cierre(step); break; } root.innerHTML = contentHtml; Views.attachListeners(step.id); if(window.feather) window.feather.replace(); } }; document.addEventListener('DOMContentLoaded', () => { // Registro del Service Worker para PWA if ('serviceWorker' in navigator) { navigator.serviceWorker.register('./sw.js') .then((reg) => console.log('PWA Service Worker Registrado')) .catch((err) => console.log('Error PWA: ', err)); } setTimeout(() => { FlowManager.init(); }, 800); });