Files
NanoJason/MindShift-Windows/src/app.js

1777 lines
69 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { authAPI, moodAPI, thoughtAPI, gratitudeAPI, progressAPI, notificationAPI, exerciseAPI, isAuthenticated, initializeAPI } from './offline-api.js';
import { translations } from './translations.js';
// Language Management
let currentLang = localStorage.getItem('appLang') || 'en';
function t(key) {
const langObj = translations[currentLang] || translations['en'];
return langObj[key] || key;
}
function setLanguage(lang) {
currentLang = lang;
localStorage.setItem('appLang', lang);
document.documentElement.lang = lang;
document.documentElement.dir = lang === 'he' ? 'rtl' : 'ltr';
// Update Header
updateHeaderTranslations();
// Re-render current section
const activeNav = document.querySelector('.nav-item.active');
if (activeNav) {
// Identify section from onclick attribute or class
// Simple re-render of home if unsure
const onclick = activeNav.getAttribute('onclick');
if (onclick && onclick.includes("'")) {
const section = onclick.split("'")[1];
showSection(section);
} else {
showSection('home');
}
} else {
showSection('home');
}
// Update nav labels
document.querySelectorAll('.nav-label').forEach((el, index) => {
const keys = ['nav_home', 'nav_mood', 'nav_thoughts', 'nav_gratitude', 'nav_progress'];
if (keys[index]) el.textContent = t(keys[index]);
});
// Update Proactive Badge
const badge = document.getElementById('proactive-badge');
if (badge) badge.textContent = t('proactive_badge');
}
function updateHeaderTranslations() {
// Update app title if dynamic, currently static in HTML but let's allow JS update
// const title = document.querySelector('.app-title');
// if (title) title.innerHTML = `<span class="material-icons">self_improvement</span> ${t('app_title')}`;
}
// Sound Manager using Web Audio API
class SoundManager {
constructor() {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
this.enabled = true;
}
playTone(freq, type, duration, vol = 0.1) {
if (!this.enabled) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
gain.gain.setValueAtTime(vol, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.ctx.currentTime + duration);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + duration);
}
playSuccess() {
// Happy ascending chime
this.playTone(523.25, 'sine', 0.3, 0.1); // C5
setTimeout(() => this.playTone(659.25, 'sine', 0.3, 0.1), 100); // E5
setTimeout(() => this.playTone(783.99, 'sine', 0.6, 0.1), 200); // G5
}
playBreathIn() {
// Gentle swelling sound
if (!this.enabled) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.frequency.setValueAtTime(200, this.ctx.currentTime);
osc.frequency.linearRampToValueAtTime(300, this.ctx.currentTime + 4);
gain.gain.setValueAtTime(0, this.ctx.currentTime);
gain.gain.linearRampToValueAtTime(0.1, this.ctx.currentTime + 2);
gain.gain.linearRampToValueAtTime(0, this.ctx.currentTime + 4);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + 4);
}
playBreathOut() {
// Gentle descending sound
if (!this.enabled) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.frequency.setValueAtTime(300, this.ctx.currentTime);
osc.frequency.linearRampToValueAtTime(200, this.ctx.currentTime + 4);
gain.gain.setValueAtTime(0, this.ctx.currentTime);
gain.gain.linearRampToValueAtTime(0.1, this.ctx.currentTime + 2);
gain.gain.linearRampToValueAtTime(0, this.ctx.currentTime + 4);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + 4);
}
}
const soundManager = new SoundManager();
// Initialize app when DOM is loaded
document.addEventListener('DOMContentLoaded', async function() {
try {
console.log('App initialization started');
// Initial Language Setup
document.documentElement.lang = currentLang;
document.documentElement.dir = currentLang === 'he' ? 'rtl' : 'ltr';
setLanguage(currentLang); // Updates Nav labels immediately
// Check authentication
if (!isAuthenticated()) {
console.log('User not authenticated, showing login modal');
showLoginModal();
// Hide initial loader if login modal is shown
const loader = document.getElementById('initial-loader');
if (loader) loader.style.display = 'none';
return;
}
// Initialize API
await initializeAPI();
// Create floating particles
createParticles();
// Add fade-in animation to cards
document.querySelectorAll('.card').forEach((card, index) => {
card.style.animationDelay = `${index * 0.1}s`;
card.classList.add('fade-in');
});
// Add motivational quotes rotation
startQuoteRotation();
// Initialize emotion sliders
initializeEmotionSliders();
// Initialize belief rating slider
initializeBeliefSlider();
// Load saved data from API
await loadSavedData();
// Render Home Page initially
showSection('home');
// Initialize inactivity tracker
initInactivityTracker();
console.log('App initialization complete');
} catch (error) {
console.error('Initialization error:', error);
const loader = document.getElementById('initial-loader');
if (loader) {
loader.innerHTML = `<div style="color: red; padding: 20px;"><h3>${t('init_error')}</h3><p>${error.message}</p></div>`;
}
}
});
// Inactivity Tracker
let inactivityTimer;
function initInactivityTracker() {
const resetTimer = () => {
clearTimeout(inactivityTimer);
const badge = document.getElementById('proactive-badge');
if (badge) badge.classList.remove('visible');
inactivityTimer = setTimeout(() => {
if (badge) badge.classList.add('visible');
}, 30000); // 30 seconds
};
document.addEventListener('mousemove', resetTimer);
document.addEventListener('keydown', resetTimer);
document.addEventListener('click', resetTimer);
resetTimer();
}
// Success Ping Animation
function triggerSuccessPing() {
soundManager.playSuccess();
const ping = document.getElementById('success-ping');
if (!ping) return;
// Clone and replace to restart animation
const newPing = ping.cloneNode(true);
newPing.innerHTML = '<div class="ping-circle"></div>';
ping.parentNode.replaceChild(newPing, ping);
}
// Quick Action Menu & Language Selector
function showQuickActionMenu() {
const menu = document.createElement('div');
menu.className = 'exercise-modal';
menu.style.display = 'block';
menu.innerHTML = `
<div class="card">
<h3>${t('quick_title')}</h3>
<div style="margin-bottom: 20px; padding: 10px; background: rgba(0,0,0,0.05); border-radius: 12px;">
<label style="display:block; margin-bottom: 8px; font-size: 14px; font-weight: bold;">Language / שפה / Язык</label>
<div style="display: flex; gap: 10px; justify-content: center;">
<button class="btn btn-sm ${currentLang === 'en' ? 'btn-primary' : 'btn-secondary'}" onclick="setLanguage('en'); this.closest('.exercise-modal').remove()">English</button>
<button class="btn btn-sm ${currentLang === 'ru' ? 'btn-primary' : 'btn-secondary'}" onclick="setLanguage('ru'); this.closest('.exercise-modal').remove()">Русский</button>
<button class="btn btn-sm ${currentLang === 'he' ? 'btn-primary' : 'btn-secondary'}" onclick="setLanguage('he'); this.closest('.exercise-modal').remove()">עברית</button>
</div>
</div>
<div class="mood-grid" style="grid-template-columns: 1fr; gap: 10px;">
<div class="mood-card" onclick="navigateTo('mood'); this.closest('.exercise-modal').remove()">
<span class="mood-label">📝 ${t('home_log_mood')}</span>
</div>
<div class="mood-card" onclick="startBreathing(); this.closest('.exercise-modal').remove()">
<span class="mood-label">🌬️ ${t('home_breathe')}</span>
</div>
<div class="mood-card" onclick="quickRelax(); this.closest('.exercise-modal').remove()">
<span class="mood-label">🧘 ${t('quick_relax_now')}</span>
</div>
</div>
<div class="exercise-actions">
<button class="btn btn-secondary" onclick="this.closest('.exercise-modal').remove()">${t('close')}</button>
</div>
</div>
`;
document.body.appendChild(menu);
}
// Login Modal
function showLoginModal() {
const loginModal = document.createElement('div');
loginModal.id = 'login-modal';
loginModal.innerHTML = `
<div class="modal-overlay">
<div class="modal-card">
<h2 class="auth-title">${t('auth_welcome')}</h2>
<div style="margin-bottom: 20px; display: flex; justify-content: center; gap: 10px;">
<button class="btn btn-sm ${currentLang === 'en' ? 'btn-primary' : 'btn-secondary'}" onclick="setLanguage('en'); showLoginModal(); document.getElementById('login-modal').remove();">EN</button>
<button class="btn btn-sm ${currentLang === 'ru' ? 'btn-primary' : 'btn-secondary'}" onclick="setLanguage('ru'); showLoginModal(); document.getElementById('login-modal').remove();">RU</button>
<button class="btn btn-sm ${currentLang === 'he' ? 'btn-primary' : 'btn-secondary'}" onclick="setLanguage('he'); showLoginModal(); document.getElementById('login-modal').remove();">HE</button>
</div>
<form id="login-form">
<div class="form-group">
<label>${t('auth_email')}</label>
<input type="email" id="email" class="form-input" required>
</div>
<div class="form-group">
<label>${t('auth_password')}</label>
<input type="password" id="password" class="form-input" required>
</div>
<button type="submit" class="btn btn-primary">${t('auth_login')}</button>
<p class="switch-form">
${t('auth_no_account')}
<a href="#" onclick="showRegisterForm()">${t('auth_register')}</a>
</p>
</form>
<form id="register-form" style="display: none;">
<div class="form-group">
<label>${t('auth_name')}</label>
<input type="text" id="reg-name" class="form-input" required>
</div>
<div class="form-group">
<label>${t('auth_email')}</label>
<input type="email" id="reg-email" class="form-input" required>
</div>
<div class="form-group">
<label>${t('auth_password')}</label>
<input type="password" id="reg-password" class="form-input" required>
</div>
<button type="submit" class="btn btn-primary">${t('auth_register')}</button>
<p class="switch-form">
${t('auth_has_account')}
<a href="#" onclick="showLoginForm()">${t('auth_login')}</a>
</p>
</form>
</div>
</div>
`;
document.body.appendChild(loginModal);
// Add event listeners
document.getElementById('login-form').addEventListener('submit', handleLogin);
document.getElementById('register-form').addEventListener('submit', handleRegister);
}
async function handleLogin(e) {
e.preventDefault();
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
try {
await authAPI.login(email, password);
document.getElementById('login-modal').remove();
location.reload();
} catch (error) {
showToast(t('auth_login_failed') + ': ' + error.message, 'error');
}
}
async function handleRegister(e) {
e.preventDefault();
const name = document.getElementById('reg-name').value;
const email = document.getElementById('reg-email').value;
const password = document.getElementById('reg-password').value;
try {
await authAPI.register(name, email, password);
document.getElementById('login-modal').remove();
location.reload();
} catch (error) {
showToast(t('auth_reg_failed') + ': ' + error.message, 'error');
}
}
function showRegisterForm() {
document.getElementById('login-form').style.display = 'none';
document.getElementById('register-form').style.display = 'block';
}
function showLoginForm() {
document.getElementById('register-form').style.display = 'none';
document.getElementById('login-form').style.display = 'block';
}
// Mood tracking with API
async function selectMood(element, mood) {
// Remove previous selection
document.querySelectorAll('.mood-card').forEach(card => {
card.classList.remove('selected');
});
// Add selection to clicked mood
element.classList.add('selected');
// Store selected mood
localStorage.setItem('selectedMood', mood);
}
async function updateIntensity(value) {
document.getElementById('intensityValue').textContent = value;
localStorage.setItem('moodIntensity', value);
}
async function saveMoodEntry() {
// Try to get mood from DOM first (more reliable than localStorage if user just clicked)
const selectedCard = document.querySelector('.mood-card.selected');
let moodType = selectedCard ? selectedCard.dataset.mood : localStorage.getItem('selectedMood');
// Default intensity to 5 if not set
let intensity = localStorage.getItem('moodIntensity');
if (!intensity) {
const slider = document.querySelector('.slider');
intensity = slider ? slider.value : '5';
}
const notesInput = document.getElementById('moodNotes');
const notes = notesInput ? notesInput.value : '';
if (!moodType) {
showToast(t('mood_select_warning'), 'warning');
return;
}
try {
await moodAPI.trackMood(moodType, parseInt(intensity), notes);
triggerSuccessPing(); // Alive Feedback
showToast(t('mood_saved_success'));
// Clear form
document.querySelectorAll('.mood-card').forEach(card => {
card.classList.remove('selected');
});
// Reset slider
const slider = document.querySelector('.slider');
if (slider) slider.value = 5;
document.getElementById('intensityValue').textContent = '5';
if (document.getElementById('moodNotes')) {
document.getElementById('moodNotes').value = '';
}
localStorage.removeItem('selectedMood');
localStorage.removeItem('moodIntensity');
// Update progress
await updateProgress();
} catch (error) {
showToast(t('mood_save') + ' failed: ' + error.message, 'error');
}
}
// Thought record with API
async function saveThoughtRecord() {
const situation = document.getElementById('situation') ? document.getElementById('situation').value : '';
const thoughts = document.getElementById('thoughts') ? document.getElementById('thoughts').value : '';
const evidence = document.getElementById('evidence') ? document.getElementById('evidence').value : '';
const alternative = document.getElementById('alternative') ? document.getElementById('alternative').value : '';
if (!situation || !thoughts) {
showToast(t('thought_fill_warning'), 'warning');
return;
}
// Get emotions
const emotions = [];
document.querySelectorAll('.emotion-inputs').forEach(input => {
const nameInput = input.querySelector('.emotion-name');
const valueInput = input.querySelector('.emotion-slider');
const name = nameInput ? nameInput.value : '';
const value = valueInput ? valueInput.value : '';
if (name && value) {
emotions.push({ name, intensity: parseInt(value) });
}
});
try {
await thoughtAPI.saveThoughtRecord({
situation,
thoughts,
emotions,
evidence,
alternativeThought: alternative,
date: new Date().toISOString()
});
triggerSuccessPing(); // Alive Feedback
showToast(t('thought_saved_success'));
closeExercise('thought-record-exercise');
// Update progress
await updateProgress();
} catch (error) {
showToast(t('thought_save') + ' failed: ' + error.message, 'error');
}
}
// Gratitude entry with API
async function saveGratitudeEntry() {
const entries = [];
document.querySelectorAll('.gratitude-input').forEach(input => {
if (input.value.trim()) {
entries.push(input.value.trim());
}
});
if (entries.length === 0) {
showToast(t('gratitude_empty_warning'), 'warning');
return;
}
try {
await gratitudeAPI.saveGratitudeEntry({
entries,
date: new Date().toISOString()
});
triggerSuccessPing(); // Alive Feedback
showToast(t('gratitude_saved_success'));
closeExercise('gratitude-exercise');
// Update progress
await updateProgress();
} catch (error) {
showToast(t('gratitude_save') + ' failed: ' + error.message, 'error');
}
}
// Load saved data from API
async function loadSavedData() {
try {
// Initialize notification system
initializeNotifications();
// Update progress based on saved data
await updateProgress();
} catch (error) {
console.error('Failed to load saved data:', error);
}
}
// Progress with API
async function updateProgress() {
console.log('updateProgress: Starting update...');
try {
// Add timeout to prevent infinite loading
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout loading stats')), 5000)
);
const statsPromise = progressAPI.getProgressStats();
const historyPromise = progressAPI.getProgressHistory();
const [stats, history] = await Promise.race([
Promise.all([statsPromise, historyPromise]),
timeoutPromise
]);
console.log('updateProgress: Data received', stats);
const statsHTML = `
<div class="progress-card">
<div class="progress-value">${(stats.today && stats.today.mood_score) ? stats.today.mood_score : '-'}</div>
<div class="progress-label">${t('home_stats_mood')}</div>
</div>
<div class="progress-card">
<div class="progress-value">${(stats.totals && stats.totals.totalSessions) ? stats.totals.totalSessions : 0}</div>
<div class="progress-label">${t('home_stats_sessions')}</div>
</div>
<div class="progress-card">
<div class="progress-value">${(stats.week && stats.week.avgMood) ? (Math.round(stats.week.avgMood * 10) / 10) : '-'}</div>
<div class="progress-label">${t('home_stats_avg')}</div>
</div>
<div class="progress-card">
<div class="progress-value">${(stats.totals && stats.totals.totalGratitude) ? stats.totals.totalGratitude : 0}</div>
<div class="progress-label">${t('home_stats_gratitude')}</div>
</div>
`;
// Update progress page stats
const progressContainer = document.getElementById('progress-stats');
if (progressContainer) {
progressContainer.innerHTML = statsHTML;
}
// Update home page stats
const homeStatsContainer = document.getElementById('home-stats-container');
if (homeStatsContainer) {
homeStatsContainer.innerHTML = `<div class="progress-container">${statsHTML}</div>`;
}
// Draw weekly chart
drawWeeklyChart(history);
console.log('updateProgress: Update complete');
} catch (error) {
console.error('Failed to update progress:', error);
// Fallback UI to remove spinner
const errorHTML = `
<div class="progress-container">
<div class="progress-card" onclick="updateProgress()">
<div class="progress-value">⚠️</div>
<div class="progress-label">${t('home_retry')}</div>
</div>
</div>
`;
const homeStatsContainer = document.getElementById('home-stats-container');
if (homeStatsContainer) {
homeStatsContainer.innerHTML = errorHTML;
}
}
}
// Analytics with API
async function loadAnalyticsData() {
try {
const moodHistory = await moodAPI.getMoodHistory();
const thoughtRecords = await thoughtAPI.getThoughtRecords();
const gratitudeEntries = await gratitudeAPI.getGratitudeEntries();
// Update analytics displays
updateAnalyticsDisplay(moodHistory, thoughtRecords, gratitudeEntries);
} catch (error) {
console.error('Failed to load analytics data:', error);
}
}
function updateAnalyticsDisplay(moodHistory, thoughtRecords, gratitudeEntries) {
// Implementation for updating analytics charts and displays
// This would use the data from the API to populate charts
console.log('Updating analytics with:', { moodHistory, thoughtRecords, gratitudeEntries });
}
// Export these functions to global scope for HTML onclick handlers
window.selectMood = selectMood;
window.updateIntensity = updateIntensity;
window.saveMoodEntry = saveMoodEntry;
window.saveThoughtRecord = saveThoughtRecord;
window.saveGratitudeEntry = saveGratitudeEntry;
window.showLoginModal = showLoginModal;
window.showRegisterForm = showRegisterForm;
window.showLoginForm = showLoginForm;
window.handleLogin = handleLogin;
window.handleRegister = handleRegister;
window.deleteNotification = deleteNotification;
window.markNotificationAsRead = markNotificationAsRead;
window.addNotification = addNotification;
window.logout = logout;
window.toggleNotifications = toggleNotifications;
window.renderHistory = renderHistory;
window.setLanguage = setLanguage;
// Logout function
async function logout() {
try {
await authAPI.logout();
location.reload();
} catch (error) {
console.error('Logout failed:', error);
location.reload(); // Reload anyway to clear state
}
}
// Toggle Notifications
function toggleNotifications() {
const list = document.getElementById('notification-list');
if (list) {
list.style.display = list.style.display === 'block' ? 'none' : 'block';
}
}
// Enhanced Toast Notification
function showToast(message, type = 'success') {
const toast = document.createElement('div');
toast.className = 'success-message'; // Keep class for animation
toast.textContent = message;
const bgColor = type === 'error' ? 'var(--error)' :
type === 'warning' ? 'var(--warning)' :
'var(--success)';
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${bgColor};
color: white;
padding: 16px 24px;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0,0,0,0.2);
z-index: 2100;
animation: slideInDown 0.3s ease;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
`;
// Add icon based on type
const icon = type === 'error' ? '⚠️' : type === 'warning' ? '🔔' : '✅';
toast.innerHTML = `<span style="font-size: 1.2em">${icon}</span> ${message}`;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'fadeOut 0.3s ease forwards';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// Replace showSuccessMessage with showToast alias for backward compatibility
const showSuccessMessage = (msg) => showToast(msg, 'success');
function createParticles() {
const particlesContainer = document.getElementById('particles');
if (!particlesContainer) return;
const particleCount = 20;
for (let i = 0; i < particleCount; i++) {
const particle = document.createElement('div');
particle.className = 'particle';
particle.style.left = Math.random() * 100 + '%';
particle.style.animationDelay = Math.random() * 6 + 's';
particle.style.animationDuration = (Math.random() * 3 + 3) + 's';
particlesContainer.appendChild(particle);
}
}
function startQuoteRotation() {
// Quotes could also be translated, but keeping them static or random is fine for now
// Ideally, add quotes to translations.js
}
function initializeEmotionSliders() {
document.querySelectorAll('.emotion-slider').forEach(slider => {
slider.addEventListener('input', function() {
const valueDisplay = this.nextElementSibling;
if (valueDisplay && valueDisplay.classList.contains('emotion-value')) {
valueDisplay.textContent = this.value;
}
});
});
}
function initializeBeliefSlider() {
const beliefSlider = document.getElementById('belief-slider');
const beliefValue = document.getElementById('belief-value');
if (beliefSlider && beliefValue) {
beliefSlider.addEventListener('input', function() {
beliefValue.textContent = this.value;
});
}
}
function drawWeeklyChart(history) {
const canvas = document.getElementById('weeklyChart');
if (!canvas || !history) return;
const ctx = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
// Clear canvas
ctx.clearRect(0, 0, width, height);
// Get last 7 days from history
const days = [];
const scores = [];
for (let i = 6; i >= 0; i--) {
const date = new Date();
date.setDate(date.getDate() - i);
const dateStr = date.toISOString().split('T')[0];
const dayEntry = history.find(entry => entry.date === dateStr);
const score = dayEntry ? dayEntry.mood_score : 0;
days.push(date.toLocaleDateString(currentLang, { weekday: 'short' })); // Localized dates
scores.push(score);
}
// Draw chart
const maxValue = 10; // Mood intensity max is 10
const barWidth = width / 7 * 0.6;
const spacing = width / 7;
ctx.fillStyle = '#FF6B6B';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
scores.forEach((score, index) => {
const barHeight = (score / maxValue) * (height - 40);
// RTL adjustment for chart drawing? No, canvas is coordinate based.
// But the order of days might need to be right-to-left for Hebrew visually?
// Standard charts usually go left-to-right (past -> future) even in RTL.
const x = index * spacing + (spacing - barWidth) / 2;
const y = height - barHeight - 20;
// Draw bar
// Color based on score
if (score >= 7) ctx.fillStyle = '#66BB6A'; // Green
else if (score >= 4) ctx.fillStyle = '#FFB74D'; // Orange
else ctx.fillStyle = '#FF5252'; // Red
if (score > 0) {
ctx.fillRect(x, y, barWidth, barHeight);
ctx.fillStyle = '#666';
ctx.fillText(score, x + barWidth / 2, y - 5);
}
// Draw day label
ctx.fillStyle = '#666';
ctx.fillText(days[index], x + barWidth / 2, height - 5);
});
}
// Notification system with API integration
async function initializeNotifications() {
try {
// Load notifications from API
const notifications = await notificationAPI.getNotifications();
// Update notification badge
const badge = document.getElementById('notification-badge');
if (badge) {
const unreadCount = notifications.filter(n => !n.read).length;
badge.textContent = unreadCount;
badge.style.display = unreadCount > 0 ? 'block' : 'none';
}
// Display notifications in dropdown
const dropdown = document.getElementById('notification-list');
if (dropdown) {
dropdown.innerHTML = notifications.length > 0
? notifications.map(n => `
<div class="notification-item ${n.read ? 'read' : ''}">
<div class="notification-content">
<div class="notification-title">${n.title}</div>
<div class="notification-message">${n.message}</div>
<div class="notification-time">${formatTime(n.timestamp)}</div>
</div>
<button class="notification-delete" onclick="deleteNotification('${n.id}')">
<span class="material-icons">close</span>
</button>
</div>
`).join('')
: `<div class="no-notifications">${t('notifications_empty')}</div>`;
}
} catch (error) {
console.error('Failed to load notifications:', error);
// Fallback to localStorage if API fails
const savedNotifications = JSON.parse(localStorage.getItem('notifications') || '[]');
const notifications = savedNotifications;
// Update notification badge
const badge = document.getElementById('notification-badge');
if (badge) {
const unreadCount = notifications.filter(n => !n.read).length;
badge.textContent = unreadCount;
badge.style.display = unreadCount > 0 ? 'block' : 'none';
}
// Display notifications in dropdown
const dropdown = document.getElementById('notification-list');
if (dropdown) {
dropdown.innerHTML = notifications.length > 0
? notifications.map(n => `
<div class="notification-item ${n.read ? 'read' : ''}">
<div class="notification-content">
<div class="notification-title">${n.title}</div>
<div class="notification-message">${n.message}</div>
<div class="notification-time">${formatTime(n.timestamp)}</div>
</div>
<button class="notification-delete" onclick="deleteNotification('${n.id}')">
<span class="material-icons">close</span>
</button>
</div>
`).join('')
: `<div class="no-notifications">${t('notifications_empty')}</div>`;
}
}
}
function formatTime(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60000) return t('just_now');
if (diff < 3600000) return `${Math.floor(diff / 60000)}${t('ago_m')}`;
if (diff < 86400000) return `${Math.floor(diff / 3600000)}${t('ago_h')}`;
return date.toLocaleDateString(currentLang);
}
async function deleteNotification(id) {
try {
await notificationAPI.deleteNotification(id);
await initializeNotifications();
} catch (error) {
console.error('Failed to delete notification:', error);
// Fallback to localStorage
let notifications = JSON.parse(localStorage.getItem('notifications') || '[]');
notifications = notifications.filter(n => n.id !== id);
localStorage.setItem('notifications', JSON.stringify(notifications));
initializeNotifications();
}
}
async function markNotificationAsRead(id) {
try {
await notificationAPI.markAsRead(id);
await initializeNotifications();
} catch (error) {
console.error('Failed to mark notification as read:', error);
}
}
async function addNotification(title, message, type = 'info') {
try {
const notification = {
title,
message,
type,
timestamp: new Date().toISOString(),
read: false
};
await notificationAPI.addNotification(notification);
await initializeNotifications();
} catch (error) {
console.error('Failed to add notification:', error);
// Fallback to localStorage
let notifications = JSON.parse(localStorage.getItem('notifications') || '[]');
const newNotification = {
id: Date.now().toString(),
title,
message,
type,
timestamp: new Date().toISOString(),
read: false
};
notifications.push(newNotification);
localStorage.setItem('notifications', JSON.stringify(notifications));
initializeNotifications();
}
}
// Render History
async function renderHistory(type) {
const container = document.getElementById('history-container');
if (!container) return;
// Clear existing content
container.innerHTML = '<div class="spinner"></div>';
try {
let data = [];
let content = '';
switch(type) {
case 'mood':
data = await moodAPI.getMoodHistory();
if (data.length === 0) {
content = `<div class="empty-state">${t('history_empty_mood')}</div>`;
} else {
content = data.map(entry => `
<div class="history-item" style="border-inline-start: 4px solid var(--${entry.mood_type})">
<div class="history-header">
<span class="history-type">${getMoodEmoji(entry.mood_type)} ${t('mood_' + entry.mood_type)}</span>
<span class="history-date">${formatDate(entry.created_at)}</span>
</div>
<div class="history-details">
${t('mood_intensity')}: <strong>${entry.intensity}/10</strong>
${entry.notes ? `<p class="history-notes">"${entry.notes}"</p>` : ''}
</div>
</div>
`).join('');
}
break;
case 'thoughts':
data = await thoughtAPI.getThoughtRecords();
if (data.length === 0) {
content = `<div class="empty-state">${t('history_empty_thoughts')}</div>`;
} else {
content = data.map(entry => `
<div class="history-item" style="border-inline-start: 4px solid var(--primary)">
<div class="history-header">
<span class="history-type">${t('thought_title')}</span>
<span class="history-date">${formatDate(entry.created_at)}</span>
</div>
<div class="history-details">
<div style="margin-bottom: 4px;"><strong>${t('thought_situation').split('(')[0]}:</strong> ${entry.situation}</div>
<div style="margin-bottom: 4px;"><strong>${t('thought_automatic').split('(')[0]}:</strong> ${entry.automatic_thought}</div>
<div><strong>${t('thought_emotions')}:</strong> ${entry.emotion} (${entry.emotion_intensity}%)</div>
</div>
</div>
`).join('');
}
break;
case 'gratitude':
data = await gratitudeAPI.getGratitudeEntries();
if (data.length === 0) {
content = `<div class="empty-state">${t('history_empty_gratitude')}</div>`;
} else {
content = data.map(entry => `
<div class="history-item" style="border-inline-start: 4px solid var(--joy)">
<div class="history-header">
<span class="history-type">${t('nav_gratitude')}</span>
<span class="history-date">${formatDate(entry.created_at)}</span>
</div>
<div class="history-details">
<p class="history-notes">"${entry.entry}"</p>
</div>
</div>
`).join('');
}
break;
}
container.innerHTML = content;
// Animate items
document.querySelectorAll('.history-item').forEach((item, index) => {
item.style.opacity = '0';
item.style.animation = `slideInUp 0.3s ease forwards ${index * 0.05}s`;
});
} catch (error) {
container.innerHTML = `<div class="error-state">${error.message}</div>`;
}
}
function getMoodEmoji(type) {
const map = {
'joy': '😊', 'peace': '😌', 'energy': '⚡',
'anxiety': '😰', 'sadness': '😢', 'anger': '😠'
};
return map[type] || '😐';
}
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
function formatDate(dateStr) {
const date = new Date(dateStr);
return date.toLocaleDateString(currentLang) + ' ' + date.toLocaleTimeString(currentLang, {hour: '2-digit', minute:'2-digit'});
}
// Render Dynamic Content
function showSection(sectionId) {
const mainContent = document.getElementById('main-content');
// Update active nav
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.remove('active');
if (item.onclick && item.onclick.toString().includes(sectionId)) {
item.classList.add('active');
}
});
switch(sectionId) {
case 'home':
mainContent.innerHTML = `
<div class="card" style="animation: slideInUp 0.5s ease-out;">
<h2 class="card-title">${t('home_welcome')}</h2>
<p style="margin-bottom: 20px;">${t('home_subtitle')}</p>
<div class="mood-grid" style="grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));">
<div class="mood-card" onclick="navigateTo('mood')">
<span class="mood-emoji">📝</span>
<span class="mood-label">${t('home_log_mood')}</span>
</div>
<div class="mood-card" onclick="navigateTo('thoughts')">
<span class="mood-emoji">🧠</span>
<span class="mood-label">${t('home_record_thought')}</span>
</div>
<div class="mood-card" onclick="navigateTo('gratitude')">
<span class="mood-emoji">🙏</span>
<span class="mood-label">${t('home_gratitude')}</span>
</div>
</div>
</div>
<div class="card" style="animation: slideInUp 0.6s ease-out;">
<h2 class="card-title">${t('home_daily_vibe')}</h2>
<div id="home-stats-container">
<!-- Stats loaded dynamically -->
<div class="progress-container">
<div class="progress-card">
<div class="progress-value"><div class="spinner" style="width: 24px; height: 24px; border-width: 3px;"></div></div>
</div>
</div>
</div>
</div>
<div class="card" style="animation: slideInUp 0.7s ease-out;">
<h2 class="card-title">${t('home_quick_relief')}</h2>
<div class="mood-grid" style="grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));">
<div class="mood-card" onclick="startBreathing()">
<span class="mood-emoji">🌬️</span>
<span class="mood-label">${t('home_breathe')}</span>
</div>
<div class="mood-card" onclick="quickRelax()">
<span class="mood-emoji">🧘</span>
<span class="mood-label">${t('home_relax')}</span>
</div>
</div>
</div>
`;
updateProgress(); // Refresh stats
break;
case 'mood':
mainContent.innerHTML = `
<div class="card" style="animation: fadeIn 0.5s;">
<h2 class="card-title">${t('mood_title')}</h2>
<div class="mood-grid">
<div class="mood-card" data-mood="joy" onclick="selectMood(this, 'joy')">
<span class="mood-emoji">😊</span>
<span class="mood-label">${t('mood_joy')}</span>
</div>
<div class="mood-card" data-mood="peace" onclick="selectMood(this, 'peace')">
<span class="mood-emoji">😌</span>
<span class="mood-label">${t('mood_peace')}</span>
</div>
<div class="mood-card" data-mood="energy" onclick="selectMood(this, 'energy')">
<span class="mood-emoji">⚡</span>
<span class="mood-label">${t('mood_energy')}</span>
</div>
<div class="mood-card" data-mood="anxiety" onclick="selectMood(this, 'anxiety')">
<span class="mood-emoji">😰</span>
<span class="mood-label">${t('mood_anxiety')}</span>
</div>
<div class="mood-card" data-mood="sadness" onclick="selectMood(this, 'sadness')">
<span class="mood-emoji">😢</span>
<span class="mood-label">${t('mood_sadness')}</span>
</div>
<div class="mood-card" data-mood="anger" onclick="selectMood(this, 'anger')">
<span class="mood-emoji">😠</span>
<span class="mood-label">${t('mood_anger')}</span>
</div>
</div>
<div class="intensity-container">
<div class="intensity-label">
<span>${t('mood_intensity')}</span>
<span id="intensityValue" style="color: var(--primary); font-weight: bold;">5</span>
</div>
<input type="range" class="slider" min="1" max="10" value="5" oninput="updateIntensity(this.value)">
</div>
<textarea id="moodNotes" class="form-input" placeholder="${t('mood_notes_placeholder')}" style="width: 100%; margin: 20px 0; min-height: 100px;"></textarea>
<button class="btn btn-primary btn-block" onclick="saveMoodEntry()">
${t('mood_save')}
</button>
</div>
`;
break;
case 'thoughts':
mainContent.innerHTML = `
<div id="thought-record-exercise" class="card" style="animation: fadeIn 0.5s;">
<h2 class="card-title">${t('thought_title')}</h2>
<div class="form-group">
<label>${t('thought_situation')}</label>
<textarea id="situation" class="form-input" rows="2"></textarea>
</div>
<div class="form-group">
<label>${t('thought_automatic')}</label>
<textarea id="thoughts" class="form-input" rows="2"></textarea>
</div>
<div class="form-group">
<label>${t('thought_emotions')}</label>
<div id="emotion-inputs-container">
<div class="emotion-inputs">
<input type="text" class="form-input emotion-name" placeholder="${t('thought_emotions')}">
<input type="range" class="emotion-slider" min="0" max="100" value="50">
<span class="emotion-value">50</span>
</div>
</div>
<button type="button" class="btn btn-secondary btn-sm" onclick="addEmotionInput()">${t('thought_add_emotion')}</button>
</div>
<div class="form-group">
<label>${t('thought_evidence')}</label>
<textarea id="evidence" class="form-input" rows="2"></textarea>
</div>
<div class="form-group">
<label>${t('thought_alternative')}</label>
<textarea id="alternative" class="form-input" rows="2"></textarea>
</div>
<div class="exercise-actions">
<button class="btn btn-primary" onclick="saveThoughtRecord()">${t('thought_save')}</button>
</div>
</div>
`;
initializeEmotionSliders();
break;
case 'gratitude':
mainContent.innerHTML = `
<div id="gratitude-exercise" class="card" style="animation: fadeIn 0.5s;">
<h2 class="card-title">${t('gratitude_title')}</h2>
<p class="exercise-intro">${t('gratitude_intro')}</p>
<div class="gratitude-list gratitude-inputs">
<input type="text" class="form-input gratitude-input" placeholder="${t('gratitude_placeholder')}">
<input type="text" class="form-input gratitude-input" placeholder="${t('gratitude_placeholder')}">
<input type="text" class="form-input gratitude-input" placeholder="${t('gratitude_placeholder')}">
</div>
<button class="btn btn-secondary" onclick="addGratitudeInput()" style="margin-bottom: 20px;">${t('gratitude_add')}</button>
<div class="exercise-actions">
<button class="btn btn-primary" onclick="saveGratitudeEntry()">${t('gratitude_save')}</button>
</div>
</div>
`;
break;
case 'progress':
mainContent.innerHTML = `
<div class="card" style="animation: slideInUp 0.5s;">
<h2 class="card-title">${t('progress_title')}</h2>
<div id="progress-stats" class="progress-container">
<!-- Stats will be loaded here -->
<div class="progress-card"><div class="progress-value"><div class="spinner" style="width: 24px; height: 24px; border-width: 3px;"></div></div></div>
</div>
</div>
<div class="card" style="animation: slideInUp 0.6s;">
<h2 class="card-title">${t('progress_weekly')}</h2>
<div class="chart-container">
<canvas id="weeklyChart"></canvas>
</div>
</div>
<div class="card" style="animation: slideInUp 0.7s;">
<h2 class="card-title">${t('progress_history')}</h2>
<div class="tabs" style="display: flex; gap: 10px; margin-bottom: 20px;">
<button class="btn btn-secondary btn-sm" onclick="renderHistory('mood')">${t('history_tab_moods')}</button>
<button class="btn btn-secondary btn-sm" onclick="renderHistory('thoughts')">${t('history_tab_thoughts')}</button>
<button class="btn btn-secondary btn-sm" onclick="renderHistory('gratitude')">${t('history_tab_gratitude')}</button>
</div>
<div id="history-container" class="history-list">
<div style="text-align: center; color: var(--on-surface-variant);">${t('history_select_prompt')}</div>
</div>
</div>
`;
updateProgress();
renderHistory('mood'); // Default to mood
break;
}
}
function quickRelax() {
// Launch the new Guided Grounding Experience
startGuidedRelaxation();
}
// --- Guided Relaxation (Mindfulness System) ---
let relaxationState = {
mode: null, // 'grounding', 'body_scan', 'visualization'
step: 0,
isActive: false,
isPaused: false,
steps: [],
timer: null,
duration: 0
};
// Data Providers
function getGroundingSteps() {
return [
{ title: t('guided_sight_title'), instruction: t('guided_sight_instruction'), sub: t('guided_sight_sub'), count: 5, icon: "👁️", color: "#64B5F6" },
{ title: t('guided_touch_title'), instruction: t('guided_touch_instruction'), sub: t('guided_touch_sub'), count: 4, icon: "✋", color: "#81C784" },
{ title: t('guided_sound_title'), instruction: t('guided_sound_instruction'), sub: t('guided_sound_sub'), count: 3, icon: "👂", color: "#FFB74D" },
{ title: t('guided_smell_title'), instruction: t('guided_smell_instruction'), sub: t('guided_smell_sub'), count: 2, icon: "👃", color: "#BA68C8" },
{ title: t('guided_taste_title'), instruction: t('guided_taste_instruction'), sub: t('guided_taste_sub'), count: 1, icon: "👅", color: "#E57373" }
];
}
function getBodyScanSteps() {
return [
{ instruction: t('scan_intro'), duration: 5000 },
{ instruction: t('scan_feet'), duration: 8000 },
{ instruction: t('scan_legs'), duration: 8000 },
{ instruction: t('scan_stomach'), duration: 8000 },
{ instruction: t('scan_chest'), duration: 8000 },
{ instruction: t('scan_shoulders'), duration: 8000 },
{ instruction: t('scan_face'), duration: 8000 },
{ instruction: t('scan_outro'), duration: 6000 }
];
}
function getVisualizationSteps() {
return [
{ instruction: t('vis_intro'), duration: 6000 },
{ instruction: t('vis_step1'), duration: 10000 },
{ instruction: t('vis_step2'), duration: 10000 },
{ instruction: t('vis_step3'), duration: 10000 },
{ instruction: t('vis_step4'), duration: 10000 },
{ instruction: t('vis_outro'), duration: 8000 }
];
}
// Entry Point
function startGuidedRelaxation() {
relaxationState = { mode: null, step: 0, isActive: true, isPaused: false, steps: [], timer: null, duration: 0 };
const overlay = document.createElement('div');
overlay.id = 'guided-relaxation-overlay';
overlay.className = 'guided-overlay';
document.body.appendChild(overlay);
// Add styles dynamically if not present (using the styles we wrote to guided-styles.css but injecting here for simplicity in single file context if needed, but best to rely on CSS file)
// Assuming guided-styles.css is linked or merged. For safety, we rely on the styles.css update.
renderModeSelection();
}
function renderModeSelection() {
const overlay = document.getElementById('guided-relaxation-overlay');
if (!overlay) return;
overlay.innerHTML = `
<div class="guided-controls">
<button class="icon-btn" onclick="closeGuidedRelaxation()">
<span class="material-icons">close</span>
</button>
</div>
<h2 class="guided-title-large">${t('guided_select_mode')}</h2>
<div class="guided-mode-grid">
<div class="guided-mode-card" onclick="startSession('grounding')">
<span class="guided-mode-icon">🌿</span>
<span class="guided-mode-title">${t('mode_grounding')}</span>
</div>
<div class="guided-mode-card" onclick="startSession('body_scan')">
<span class="guided-mode-icon">🧘</span>
<span class="guided-mode-title">${t('mode_body_scan')}</span>
</div>
<div class="guided-mode-card" onclick="startSession('visualization')">
<span class="guided-mode-icon">🌄</span>
<span class="guided-mode-title">${t('mode_visualization')}</span>
</div>
</div>
`;
}
function startSession(mode) {
relaxationState.mode = mode;
relaxationState.step = 0;
relaxationState.isActive = true;
const overlay = document.getElementById('guided-relaxation-overlay');
if (overlay) overlay.className = `guided-overlay mode-${mode}`;
if (mode === 'grounding') {
relaxationState.steps = getGroundingSteps();
renderGroundingStep();
} else if (mode === 'body_scan') {
relaxationState.steps = getBodyScanSteps();
runAutoSession();
} else if (mode === 'visualization') {
relaxationState.steps = getVisualizationSteps();
runAutoSession();
}
}
// --- Grounding Logic (Interactive) ---
function renderGroundingStep() {
const overlay = document.getElementById('guided-relaxation-overlay');
if (!overlay || !relaxationState.isActive) return;
const currentStep = relaxationState.steps[relaxationState.step];
const progressDots = Array(currentStep.count).fill('<div class="progress-dot"></div>').join('');
overlay.innerHTML = `
<div class="guided-controls">
<button class="icon-btn" onclick="closeGuidedRelaxation()">
<span class="material-icons">close</span>
</button>
</div>
<div class="guided-step-icon" style="color: ${currentStep.color}">
${currentStep.icon}
</div>
<div class="guided-instruction">${currentStep.instruction}</div>
<div class="guided-sub">${currentStep.sub}</div>
<div class="guided-progress-dots" id="step-dots">
${progressDots}
</div>
<button class="guided-action-btn" onclick="nextGroundingSubStep()">
${t('guided_found_btn')}
</button>
`;
speakText(`${currentStep.instruction} ${currentStep.sub}`);
}
let currentDotIndex = 0;
function nextGroundingSubStep() {
const dots = document.querySelectorAll('.progress-dot');
if (currentDotIndex < dots.length) {
dots[currentDotIndex].classList.add('active');
if (navigator.vibrate) navigator.vibrate(50);
soundManager.playTone(400 + (currentDotIndex * 50), 'sine', 0.1, 0.1);
currentDotIndex++;
if (currentDotIndex === dots.length) {
setTimeout(() => {
currentDotIndex = 0;
relaxationState.step++;
if (relaxationState.step < relaxationState.steps.length) {
renderGroundingStep();
} else {
finishSession();
}
}, 1000);
}
}
}
// --- Auto Session Logic (Body Scan & Visualization) ---
function runAutoSession() {
const overlay = document.getElementById('guided-relaxation-overlay');
if (!overlay || !relaxationState.isActive) return;
const step = relaxationState.steps[relaxationState.step];
const totalSteps = relaxationState.steps.length;
const progress = ((relaxationState.step) / totalSteps) * 100;
// Visuals based on mode
let visualHTML = '';
if (relaxationState.mode === 'body_scan') {
visualHTML = `<div class="body-scan-pulse"></div><div class="guided-step-icon">🧘</div>`;
} else {
visualHTML = `<div class="guided-step-icon">🌄</div>`;
}
overlay.innerHTML = `
<div class="guided-controls">
<button class="icon-btn" onclick="closeGuidedRelaxation()">
<span class="material-icons">close</span>
</button>
</div>
${visualHTML}
<div class="guided-instruction">${step.instruction}</div>
<div class="guided-progress-bar-container">
<div class="guided-progress-bar" style="width: ${progress}%"></div>
</div>
<div class="guided-bottom-controls">
<!-- Could add Play/Pause here if needed -->
</div>
`;
// Speak and advance
speakText(step.instruction, () => {
// On end of speech, wait remainder of duration
if (!relaxationState.isActive) return;
relaxationState.timer = setTimeout(() => {
relaxationState.step++;
if (relaxationState.step < relaxationState.steps.length) {
runAutoSession();
} else {
finishSession();
}
}, 2000); // Short pause after speech
});
// Animate progress bar
setTimeout(() => {
const bar = document.querySelector('.guided-progress-bar');
if (bar) bar.style.width = `${((relaxationState.step + 1) / totalSteps) * 100}%`;
}, 100);
}
function finishSession() {
const overlay = document.getElementById('guided-relaxation-overlay');
if (overlay) {
overlay.innerHTML = `
<div class="guided-step-icon">🌟</div>
<div class="guided-instruction">${t('guided_complete_title')}</div>
<div class="guided-sub">${t('guided_complete_sub')}</div>
<button class="guided-action-btn" onclick="closeGuidedRelaxation()">
${t('guided_complete_btn')}
</button>
`;
speakText(`${t('guided_complete_title')} ${t('guided_complete_sub')}`);
triggerSuccessPing();
// Log stats
const duration = relaxationState.mode === 'grounding' ? 180 :
relaxationState.mode === 'body_scan' ? 300 : 240;
exerciseAPI.logSession(relaxationState.mode, duration).then(() => updateProgress());
}
}
function closeGuidedRelaxation() {
relaxationState.isActive = false;
if (relaxationState.timer) clearTimeout(relaxationState.timer);
currentDotIndex = 0;
const overlay = document.getElementById('guided-relaxation-overlay');
if (overlay) overlay.remove();
if (window.speechSynthesis) {
window.speechSynthesis.cancel();
}
}
// Enhanced TTS Wrapper
function speakText(text, onEndCallback) {
if ('speechSynthesis' in window) {
window.speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utterance.rate = 0.85; // Slightly slower for relaxation
utterance.pitch = 1.0;
const langMap = { 'en': 'en-US', 'ru': 'ru-RU', 'he': 'he-IL' };
utterance.lang = langMap[currentLang] || 'en-US';
if (onEndCallback) {
utterance.onend = onEndCallback;
}
window.speechSynthesis.speak(utterance);
} else {
// Fallback if no TTS
if (onEndCallback) setTimeout(onEndCallback, 3000);
}
}
function finishRelaxSession(btn) {
btn.closest('.exercise-modal').remove();
exerciseAPI.logSession('relaxation', 120).then(() => {
triggerSuccessPing();
showToast(t('mood_saved_success')); // Reuse success message or create new
updateProgress();
});
}
function breathingExercise() {
startBreathing();
}
function emergencyHelp() {
alert('If you\'re in crisis, please call emergency services or a crisis hotline.');
}
function startThoughtRecord() {
showSection('thoughts');
}
function startMindfulness() {
// Implement or route
}
function startGratitude() {
showSection('gratitude');
}
function closeExercise(exerciseId) {
document.getElementById(exerciseId).style.display = 'none';
showSection('home');
}
function addEmotionInput() {
const emotionContainer = document.querySelector('.emotion-inputs').parentElement;
const newEmotionInput = document.createElement('div');
newEmotionInput.className = 'emotion-inputs';
newEmotionInput.innerHTML = `
<input type="text" class="form-input emotion-name" placeholder="${t('thought_emotions')}">
<input type="range" class="emotion-slider" min="0" max="100" value="50">
<span class="emotion-value">50</span>
<button type="button" class="btn-remove" onclick="this.parentElement.remove()">×</button>
`;
emotionContainer.insertBefore(newEmotionInput, emotionContainer.lastElementChild);
initializeEmotionSliders();
}
function addGratitudeInput() {
const gratitudeContainer = document.querySelector('.gratitude-inputs');
const newInput = document.createElement('input');
newInput.type = 'text';
newInput.className = 'form-input gratitude-input';
newInput.placeholder = t('gratitude_placeholder');
gratitudeContainer.appendChild(newInput);
}
// --- Smart Breathing System ---
let breathingState = {
isActive: false,
technique: 'balance',
timer: null
};
function getBreathingTechniques() {
return {
balance: {
name: 'Balance',
label: t('breath_balance'),
phases: [
{ name: 'inhale', duration: 5500, label: t('breath_in'), scale: 1.8 },
{ name: 'exhale', duration: 5500, label: t('breath_out'), scale: 1.0 }
]
},
relax: {
name: 'Relax',
label: t('breath_relax'),
phases: [
{ name: 'inhale', duration: 4000, label: t('breath_in'), scale: 1.8 },
{ name: 'hold', duration: 7000, label: t('breath_hold'), scale: 1.8 },
{ name: 'exhale', duration: 8000, label: t('breath_out'), scale: 1.0 }
]
},
focus: {
name: 'Focus',
label: t('breath_focus'),
phases: [
{ name: 'inhale', duration: 4000, label: t('breath_in'), scale: 1.8 },
{ name: 'hold', duration: 4000, label: t('breath_hold'), scale: 1.8 },
{ name: 'exhale', duration: 4000, label: t('breath_out'), scale: 1.0 },
{ name: 'hold', duration: 4000, label: t('breath_hold'), scale: 1.0 }
]
}
};
}
function startBreathing() {
// Create immersive overlay
const overlay = document.createElement('div');
overlay.id = 'smart-breathing-overlay';
overlay.className = 'breathing-overlay';
// Render techniques dynamically
const techniques = getBreathingTechniques();
overlay.innerHTML = `
<div class="technique-selector">
<button class="technique-btn" onclick="setBreathingTechnique('balance')">${techniques.balance.label}</button>
<button class="technique-btn" onclick="setBreathingTechnique('relax')">${techniques.relax.label}</button>
<button class="technique-btn" onclick="setBreathingTechnique('focus')">${techniques.focus.label}</button>
</div>
<div class="breathing-visual-container">
<div class="breath-particles"></div>
<div id="breath-circle" class="breath-circle-main"></div>
<div class="breath-circle-inner"></div>
</div>
<div id="breath-instruction" class="breath-instruction-text">${t('breath_ready')}</div>
<div id="breath-sub" class="breath-sub-text">${t('breath_sit')}</div>
<div class="breath-controls">
<button class="control-btn-icon" onclick="closeSmartBreathing()">
<span class="material-icons">close</span>
</button>
</div>
`;
document.body.appendChild(overlay);
// Start with default or last used
setBreathingTechnique('balance');
}
function setBreathingTechnique(tech) {
breathingState.technique = tech;
const techniques = getBreathingTechniques();
// Update buttons
document.querySelectorAll('.technique-btn').forEach(btn => {
btn.classList.remove('active');
// Match by label content
if(btn.textContent === techniques[tech].label) {
btn.classList.add('active');
}
});
stopSmartBreathingLoop();
startSmartBreathingLoop();
}
function startSmartBreathingLoop() {
breathingState.isActive = true;
let currentPhaseIndex = 0;
const loop = async () => {
if (!breathingState.isActive) return;
const techniques = getBreathingTechniques();
const technique = techniques[breathingState.technique];
const phase = technique.phases[currentPhaseIndex];
updateBreathingUI(phase.name, phase.label, phase.duration, phase.scale);
// Audio & Haptics
if (phase.name === 'inhale') {
soundManager.playBreathIn();
if (navigator.vibrate) navigator.vibrate(50);
} else if (phase.name === 'exhale') {
soundManager.playBreathOut();
if (navigator.vibrate) navigator.vibrate([30, 30]);
} else {
// Hold
if (navigator.vibrate) navigator.vibrate(20);
}
// Wait for phase duration
await new Promise(resolve => {
breathingState.timer = setTimeout(resolve, phase.duration);
});
// Move to next phase
currentPhaseIndex = (currentPhaseIndex + 1) % technique.phases.length;
if (breathingState.isActive) loop();
};
loop();
}
function updateBreathingUI(phaseName, label, duration, scale) {
const circle = document.getElementById('breath-circle');
const text = document.getElementById('breath-instruction');
const sub = document.getElementById('breath-sub');
if (!circle) return;
// Update Text
text.textContent = label;
text.style.animation = 'none';
text.offsetHeight; // Trigger reflow
text.style.animation = 'fadeIn 0.5s';
sub.textContent = (duration / 1000) + 's';
// Update Visuals
circle.className = 'breath-circle-main ' + phaseName;
circle.style.transition = `transform ${duration}ms linear`;
circle.style.transform = `scale(${scale})`;
}
function stopSmartBreathingLoop() {
breathingState.isActive = false;
if (breathingState.timer) clearTimeout(breathingState.timer);
}
function closeSmartBreathing() {
stopSmartBreathingLoop();
const overlay = document.getElementById('smart-breathing-overlay');
if (overlay) overlay.remove();
// Log session
exerciseAPI.logSession('breathing', 60).then(() => {
triggerSuccessPing();
showToast(t('breath_complete'));
updateProgress();
});
}
// Make global
window.setBreathingTechnique = setBreathingTechnique;
window.closeSmartBreathing = closeSmartBreathing;
// Legacy wrappers
function startBreathingExercise() { startBreathing(); }
function stopBreathingExercise() { closeSmartBreathing(); }
// Export additional functions
window.addEmotionInput = addEmotionInput;
window.addGratitudeInput = addGratitudeInput;
window.startBreathingExercise = startBreathingExercise;
window.stopBreathingExercise = stopBreathingExercise;
window.deleteNotification = deleteNotification;
window.quickRelax = quickRelax;
window.startBreathing = breathingExercise; // Alias for startBreathing in HTML
window.showQuickActionMenu = showQuickActionMenu;
window.navigateTo = showSection; // Alias for navigateTo in HTML
window.finishRelaxSession = finishRelaxSession;
function showLanguageModal() {
const menu = document.createElement('div');
menu.className = 'exercise-modal';
menu.style.display = 'block';
menu.innerHTML = `
<div class="card">
<h3>Language / שפה / Язык</h3>
<div style="display: flex; gap: 10px; justify-content: center; margin-bottom: 20px;">
<button class="btn btn-sm ${currentLang === 'en' ? 'btn-primary' : 'btn-secondary'}" onclick="setLanguage('en'); this.closest('.exercise-modal').remove()">English</button>
<button class="btn btn-sm ${currentLang === 'ru' ? 'btn-primary' : 'btn-secondary'}" onclick="setLanguage('ru'); this.closest('.exercise-modal').remove()">Русский</button>
<button class="btn btn-sm ${currentLang === 'he' ? 'btn-primary' : 'btn-secondary'}" onclick="setLanguage('he'); this.closest('.exercise-modal').remove()">עברית</button>
</div>
<div class="exercise-actions">
<button class="btn btn-secondary" onclick="this.closest('.exercise-modal').remove()">${t('close')}</button>
</div>
</div>
`;
document.body.appendChild(menu);
}
window.showLanguageModal = showLanguageModal;