1777 lines
69 KiB
JavaScript
1777 lines
69 KiB
JavaScript
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;
|