Complete UI/UX overhaul: Polished login, fixed mood tracking, enhanced animations, and verified full-stack flows
This commit is contained in:
@@ -135,11 +135,21 @@ export const gratitudeAPI = {
|
||||
// Progress API
|
||||
export const progressAPI = {
|
||||
async getProgressStats() {
|
||||
return await apiCall('/progress/stats');
|
||||
return await apiCall('/dashboard/stats');
|
||||
},
|
||||
|
||||
async getProgressHistory() {
|
||||
return await apiCall('/progress/history');
|
||||
async getProgressHistory(days = 30) {
|
||||
return await apiCall(`/progress?days=${days}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Exercise API
|
||||
export const exerciseAPI = {
|
||||
async logSession(type, duration) {
|
||||
return await apiCall('/exercises', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ exerciseType: type, duration })
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { authAPI, moodAPI, thoughtAPI, gratitudeAPI, progressAPI, notificationAPI, isAuthenticated, initializeAPI } from './api.js';
|
||||
import { authAPI, moodAPI, thoughtAPI, gratitudeAPI, progressAPI, notificationAPI, exerciseAPI, isAuthenticated, initializeAPI } from './api.js';
|
||||
|
||||
// Sound Manager using Web Audio API
|
||||
class SoundManager {
|
||||
@@ -171,7 +171,7 @@ function showLoginModal() {
|
||||
loginModal.innerHTML = `
|
||||
<div class="modal-overlay">
|
||||
<div class="modal-card">
|
||||
<h2>Welcome to MindShift</h2>
|
||||
<h2 class="auth-title">Welcome to MindShift</h2>
|
||||
<form id="login-form">
|
||||
<div class="form-group">
|
||||
<label>Email</label>
|
||||
@@ -227,7 +227,7 @@ async function handleLogin(e) {
|
||||
document.getElementById('login-modal').remove();
|
||||
location.reload();
|
||||
} catch (error) {
|
||||
alert('Login failed: ' + error.message);
|
||||
showToast('Login failed: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,7 +242,7 @@ async function handleRegister(e) {
|
||||
document.getElementById('login-modal').remove();
|
||||
location.reload();
|
||||
} catch (error) {
|
||||
alert('Registration failed: ' + error.message);
|
||||
showToast('Registration failed: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,25 +276,39 @@ async function updateIntensity(value) {
|
||||
}
|
||||
|
||||
async function saveMoodEntry() {
|
||||
const moodType = localStorage.getItem('selectedMood');
|
||||
const intensity = localStorage.getItem('moodIntensity');
|
||||
// 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 notes = document.getElementById('moodNotes')?.value;
|
||||
|
||||
if (!moodType || !intensity) {
|
||||
alert('Please select a mood and intensity');
|
||||
if (!moodType) {
|
||||
showToast('Please select a mood', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await moodAPI.trackMood(moodType, parseInt(intensity), notes);
|
||||
triggerSuccessPing(); // Alive Feedback
|
||||
showSuccessMessage('Mood tracked successfully!');
|
||||
showToast('Mood tracked successfully!');
|
||||
|
||||
// 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 = '';
|
||||
}
|
||||
@@ -305,7 +319,7 @@ async function saveMoodEntry() {
|
||||
// Update progress
|
||||
await updateProgress();
|
||||
} catch (error) {
|
||||
alert('Failed to save mood: ' + error.message);
|
||||
showToast('Failed to save mood: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,7 +331,7 @@ async function saveThoughtRecord() {
|
||||
const alternative = document.getElementById('alternative')?.value;
|
||||
|
||||
if (!situation || !thoughts) {
|
||||
alert('Please fill in at least the situation and thoughts');
|
||||
showToast('Please fill in at least the situation and thoughts', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -342,13 +356,13 @@ async function saveThoughtRecord() {
|
||||
});
|
||||
|
||||
triggerSuccessPing(); // Alive Feedback
|
||||
showSuccessMessage('Thought record saved successfully!');
|
||||
showToast('Thought record saved successfully!');
|
||||
closeExercise('thought-record-exercise');
|
||||
|
||||
// Update progress
|
||||
await updateProgress();
|
||||
} catch (error) {
|
||||
alert('Failed to save thought record: ' + error.message);
|
||||
showToast('Failed to save thought record: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,7 +376,7 @@ async function saveGratitudeEntry() {
|
||||
});
|
||||
|
||||
if (entries.length === 0) {
|
||||
alert('Please add at least one gratitude entry');
|
||||
showToast('Please add at least one gratitude entry', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -373,13 +387,13 @@ async function saveGratitudeEntry() {
|
||||
});
|
||||
|
||||
triggerSuccessPing(); // Alive Feedback
|
||||
showSuccessMessage('Gratitude entries saved successfully!');
|
||||
showToast('Gratitude entries saved successfully!');
|
||||
closeExercise('gratitude-exercise');
|
||||
|
||||
// Update progress
|
||||
await updateProgress();
|
||||
} catch (error) {
|
||||
alert('Failed to save gratitude entries: ' + error.message);
|
||||
showToast('Failed to save gratitude entries: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,20 +421,20 @@ async function updateProgress() {
|
||||
if (progressContainer) {
|
||||
progressContainer.innerHTML = `
|
||||
<div class="progress-card">
|
||||
<div class="progress-value">${stats.dayStreak || 0}</div>
|
||||
<div class="progress-label">Day Streak</div>
|
||||
<div class="progress-value">${stats.today.mood_score || '-'}</div>
|
||||
<div class="progress-label">Today's Mood</div>
|
||||
</div>
|
||||
<div class="progress-card">
|
||||
<div class="progress-value">${stats.totalSessions || 0}</div>
|
||||
<div class="progress-value">${stats.totals.totalSessions || 0}</div>
|
||||
<div class="progress-label">Sessions</div>
|
||||
</div>
|
||||
<div class="progress-card">
|
||||
<div class="progress-value">${stats.improvement || 0}%</div>
|
||||
<div class="progress-label">Improvement</div>
|
||||
<div class="progress-value">${Math.round(stats.week.avgMood * 10) / 10 || '-'}</div>
|
||||
<div class="progress-label">Weekly Avg</div>
|
||||
</div>
|
||||
<div class="progress-card">
|
||||
<div class="progress-value">${stats.avgMood || 0}</div>
|
||||
<div class="progress-label">Avg Mood</div>
|
||||
<div class="progress-value">${stats.totals.totalGratitude || 0}</div>
|
||||
<div class="progress-label">Gratitude</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -466,8 +480,71 @@ window.handleRegister = handleRegister;
|
||||
window.deleteNotification = deleteNotification;
|
||||
window.markNotificationAsRead = markNotificationAsRead;
|
||||
window.addNotification = addNotification;
|
||||
window.logout = logout;
|
||||
window.toggleNotifications = toggleNotifications;
|
||||
window.renderHistory = renderHistory;
|
||||
|
||||
// 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');
|
||||
|
||||
// Keep existing utility functions
|
||||
function createParticles() {
|
||||
const particlesContainer = document.getElementById('particles');
|
||||
if (!particlesContainer) return;
|
||||
@@ -522,29 +599,6 @@ function initializeBeliefSlider() {
|
||||
}
|
||||
}
|
||||
|
||||
function showSuccessMessage(message) {
|
||||
const successDiv = document.createElement('div');
|
||||
successDiv.className = 'success-message';
|
||||
successDiv.textContent = message;
|
||||
successDiv.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: var(--success);
|
||||
color: white;
|
||||
padding: 16px 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
z-index: 2000;
|
||||
animation: slideIn 0.3s ease;
|
||||
`;
|
||||
document.body.appendChild(successDiv);
|
||||
|
||||
setTimeout(() => {
|
||||
successDiv.remove();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function drawWeeklyChart(history) {
|
||||
const canvas = document.getElementById('weeklyChart');
|
||||
if (!canvas || !history) return;
|
||||
@@ -558,23 +612,22 @@ function drawWeeklyChart(history) {
|
||||
|
||||
// Get last 7 days from history
|
||||
const days = [];
|
||||
const counts = [];
|
||||
const scores = [];
|
||||
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - i);
|
||||
const dateStr = date.toDateString();
|
||||
const dateStr = date.toISOString().split('T')[0];
|
||||
|
||||
const dayCount = history.filter(entry =>
|
||||
new Date(entry.date).toDateString() === dateStr
|
||||
).length;
|
||||
const dayEntry = history.find(entry => entry.date === dateStr);
|
||||
const score = dayEntry ? dayEntry.mood_score : 0;
|
||||
|
||||
days.push(date.toLocaleDateString('en', { weekday: 'short' }));
|
||||
counts.push(dayCount);
|
||||
scores.push(score);
|
||||
}
|
||||
|
||||
// Draw chart
|
||||
const maxValue = Math.max(...counts, 5);
|
||||
const maxValue = 10; // Mood intensity max is 10
|
||||
const barWidth = width / 7 * 0.6;
|
||||
const spacing = width / 7;
|
||||
|
||||
@@ -582,23 +635,26 @@ function drawWeeklyChart(history) {
|
||||
ctx.font = '12px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
|
||||
counts.forEach((count, index) => {
|
||||
const barHeight = (count / maxValue) * (height - 40);
|
||||
scores.forEach((score, index) => {
|
||||
const barHeight = (score / maxValue) * (height - 40);
|
||||
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);
|
||||
|
||||
// Draw count
|
||||
if (count > 0) {
|
||||
ctx.fillStyle = '#FF6B6B';
|
||||
ctx.fillText(count, x + barWidth / 2, y - 5);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -732,6 +788,109 @@ async function addNotification(title, message, type = 'info') {
|
||||
}
|
||||
}
|
||||
|
||||
// Render History
|
||||
async function renderHistory(type) {
|
||||
const container = document.getElementById('history-container');
|
||||
if (!container) return;
|
||||
|
||||
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">No mood entries yet. Start tracking! 📝</div>';
|
||||
} else {
|
||||
content = data.map(entry => `
|
||||
<div class="history-item" style="border-left: 4px solid var(--${entry.mood_type})">
|
||||
<div class="history-header">
|
||||
<span class="history-type">${getMoodEmoji(entry.mood_type)} ${capitalize(entry.mood_type)}</span>
|
||||
<span class="history-date">${formatDate(entry.created_at)}</span>
|
||||
</div>
|
||||
<div class="history-details">
|
||||
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">No thought records yet. 🧠</div>';
|
||||
} else {
|
||||
content = data.map(entry => `
|
||||
<div class="history-item" style="border-left: 4px solid var(--primary)">
|
||||
<div class="history-header">
|
||||
<span class="history-type">Thought Record</span>
|
||||
<span class="history-date">${formatDate(entry.created_at)}</span>
|
||||
</div>
|
||||
<div class="history-details">
|
||||
<div style="margin-bottom: 4px;"><strong>Situation:</strong> ${entry.situation}</div>
|
||||
<div style="margin-bottom: 4px;"><strong>Thought:</strong> ${entry.automatic_thought}</div>
|
||||
<div><strong>Emotion:</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">No gratitude entries yet. 🙏</div>';
|
||||
} else {
|
||||
content = data.map(entry => `
|
||||
<div class="history-item" style="border-left: 4px solid var(--joy)">
|
||||
<div class="history-header">
|
||||
<span class="history-type">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">Failed to load history: ${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() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
||||
}
|
||||
|
||||
// Render Dynamic Content
|
||||
function showSection(sectionId) {
|
||||
const mainContent = document.getElementById('main-content');
|
||||
@@ -772,7 +931,7 @@ function showSection(sectionId) {
|
||||
<!-- Stats loaded dynamically -->
|
||||
<div class="progress-container">
|
||||
<div class="progress-card">
|
||||
<div class="progress-value">Loading...</div>
|
||||
<div class="progress-value"><div class="spinner" style="width: 24px; height: 24px; border-width: 3px;"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -800,27 +959,27 @@ function showSection(sectionId) {
|
||||
<div class="card" style="animation: fadeIn 0.5s;">
|
||||
<h2 class="card-title">How are you feeling?</h2>
|
||||
<div class="mood-grid">
|
||||
<div class="mood-card" onclick="selectMood(this, 'joy')">
|
||||
<div class="mood-card" data-mood="joy" onclick="selectMood(this, 'joy')">
|
||||
<span class="mood-emoji">😊</span>
|
||||
<span class="mood-label">Joy</span>
|
||||
</div>
|
||||
<div class="mood-card" onclick="selectMood(this, 'peace')">
|
||||
<div class="mood-card" data-mood="peace" onclick="selectMood(this, 'peace')">
|
||||
<span class="mood-emoji">😌</span>
|
||||
<span class="mood-label">Peace</span>
|
||||
</div>
|
||||
<div class="mood-card" onclick="selectMood(this, 'energy')">
|
||||
<div class="mood-card" data-mood="energy" onclick="selectMood(this, 'energy')">
|
||||
<span class="mood-emoji">⚡</span>
|
||||
<span class="mood-label">Energy</span>
|
||||
</div>
|
||||
<div class="mood-card" onclick="selectMood(this, 'anxiety')">
|
||||
<div class="mood-card" data-mood="anxiety" onclick="selectMood(this, 'anxiety')">
|
||||
<span class="mood-emoji">😰</span>
|
||||
<span class="mood-label">Anxiety</span>
|
||||
</div>
|
||||
<div class="mood-card" onclick="selectMood(this, 'sadness')">
|
||||
<div class="mood-card" data-mood="sadness" onclick="selectMood(this, 'sadness')">
|
||||
<span class="mood-emoji">😢</span>
|
||||
<span class="mood-label">Sadness</span>
|
||||
</div>
|
||||
<div class="mood-card" onclick="selectMood(this, 'anger')">
|
||||
<div class="mood-card" data-mood="anger" onclick="selectMood(this, 'anger')">
|
||||
<span class="mood-emoji">😠</span>
|
||||
<span class="mood-label">Anger</span>
|
||||
</div>
|
||||
@@ -906,7 +1065,7 @@ function showSection(sectionId) {
|
||||
<h2 class="card-title">Your Progress 📈</h2>
|
||||
<div id="progress-stats" class="progress-container">
|
||||
<!-- Stats will be loaded here -->
|
||||
<div class="progress-card"><div class="progress-value">...</div></div>
|
||||
<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;">
|
||||
@@ -915,8 +1074,22 @@ function showSection(sectionId) {
|
||||
<canvas id="weeklyChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="animation: slideInUp 0.7s;">
|
||||
<h2 class="card-title">Recent History 📜</h2>
|
||||
<div class="tabs" style="display: flex; gap: 10px; margin-bottom: 20px;">
|
||||
<button class="btn btn-secondary btn-sm" onclick="renderHistory('mood')">Moods</button>
|
||||
<button class="btn btn-secondary btn-sm" onclick="renderHistory('thoughts')">Thoughts</button>
|
||||
<button class="btn btn-secondary btn-sm" onclick="renderHistory('gratitude')">Gratitude</button>
|
||||
</div>
|
||||
<div id="history-container" class="history-list">
|
||||
<!-- History items will be loaded here -->
|
||||
<div style="text-align: center; color: var(--on-surface-variant);">Select a category to view history</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
updateProgress();
|
||||
renderHistory('mood'); // Default to mood
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -978,13 +1151,22 @@ function quickRelax() {
|
||||
<p style="font-size: 18px; margin-bottom: 30px;">${randomTechnique.desc}</p>
|
||||
<div class="exercise-actions" style="justify-content: center;">
|
||||
<button class="btn btn-primary" onclick="this.closest('.exercise-modal').remove(); quickRelax()">Try Another</button>
|
||||
<button class="btn btn-secondary" onclick="this.closest('.exercise-modal').remove()">Done</button>
|
||||
<button class="btn btn-secondary" onclick="finishRelaxSession(this)">Done</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
function finishRelaxSession(btn) {
|
||||
btn.closest('.exercise-modal').remove();
|
||||
exerciseAPI.logSession('relaxation', 120).then(() => {
|
||||
triggerSuccessPing();
|
||||
showSuccessMessage('Relaxation session logged! 🧘');
|
||||
updateProgress();
|
||||
});
|
||||
}
|
||||
|
||||
function breathingExercise() {
|
||||
startBreathing();
|
||||
}
|
||||
@@ -1089,9 +1271,16 @@ function stopBreathingExercise() {
|
||||
clearInterval(breathingInterval);
|
||||
const circle = document.getElementById('breathing-circle');
|
||||
const text = document.getElementById('breathing-text');
|
||||
circle.classList.remove('inhale', 'exhale');
|
||||
text.textContent = 'Ready';
|
||||
if (circle) circle.classList.remove('inhale', 'exhale');
|
||||
if (text) text.textContent = 'Ready';
|
||||
breathingPhase = 'inhale';
|
||||
|
||||
// Log session (assuming ~1 min or track actual time)
|
||||
exerciseAPI.logSession('breathing', 60).then(() => {
|
||||
triggerSuccessPing();
|
||||
showSuccessMessage('Breathing session logged! 🌬️');
|
||||
updateProgress();
|
||||
});
|
||||
}
|
||||
|
||||
// Export additional functions
|
||||
@@ -1104,3 +1293,4 @@ 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;
|
||||
@@ -667,8 +667,286 @@ textarea:focus, input[type="text"]:focus {
|
||||
.nav-label { font-size: 10px; }
|
||||
}
|
||||
|
||||
/* Exercise Modals */
|
||||
.exercise-modal {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
z-index: 2000;
|
||||
animation: zoomIn 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
}
|
||||
|
||||
.exercise-modal .card {
|
||||
margin: 0;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
|
||||
border: 2px solid rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
@keyframes zoomIn {
|
||||
from { opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
|
||||
to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
|
||||
}
|
||||
|
||||
.exercise-modal h3 {
|
||||
color: var(--primary);
|
||||
margin-bottom: 24px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.exercise-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* Desktop Sidebar */
|
||||
@media (min-width: 1024px) {
|
||||
.bottom-nav { display: none; }
|
||||
/* ... (keep existing desktop styles if needed, but adapt to new look) ... */
|
||||
.bottom-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
top: 80px;
|
||||
left: 20px;
|
||||
right: auto;
|
||||
bottom: 20px;
|
||||
width: 100px;
|
||||
height: auto;
|
||||
border-radius: 20px;
|
||||
justify-content: flex-start;
|
||||
padding-top: 40px;
|
||||
gap: 20px;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 120px;
|
||||
max-width: calc(100% - 140px);
|
||||
}
|
||||
|
||||
.nav-item.active::after {
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
bottom: auto;
|
||||
width: 4px;
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* History Lists */
|
||||
.history-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.history-item:hover {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.history-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.history-type {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.history-date {
|
||||
font-size: 12px;
|
||||
color: var(--on-surface-variant);
|
||||
}
|
||||
|
||||
.history-details {
|
||||
font-size: 14px;
|
||||
color: var(--on-surface);
|
||||
}
|
||||
|
||||
.history-notes {
|
||||
font-style: italic;
|
||||
color: var(--on-surface-variant);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: var(--on-surface-variant);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.error-state {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: var(--error);
|
||||
background: rgba(255, 82, 82, 0.1);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Loading Spinner */
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid rgba(0,0,0,0.1);
|
||||
border-left-color: var(--primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 20px auto;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* Auth Modal Styles */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 3000;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(20px);
|
||||
padding: 40px;
|
||||
border-radius: 30px;
|
||||
width: 90%;
|
||||
max-width: 400px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||
text-align: center;
|
||||
animation: slideInUp 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Animated gradient border top */
|
||||
.modal-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: linear-gradient(90deg, var(--primary), var(--secondary), var(--tertiary));
|
||||
animation: gradientFlow 3s linear infinite;
|
||||
background-size: 200% 100%;
|
||||
}
|
||||
|
||||
@keyframes gradientFlow {
|
||||
0% { background-position: 100% 0; }
|
||||
100% { background-position: -100% 0; }
|
||||
}
|
||||
|
||||
.auth-title {
|
||||
font-size: 32px;
|
||||
font-weight: 800;
|
||||
margin-bottom: 30px;
|
||||
background: linear-gradient(45deg, var(--primary), var(--secondary));
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
animation: pulseText 3s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes pulseText {
|
||||
0%, 100% { transform: scale(1); filter: brightness(100%); }
|
||||
50% { transform: scale(1.05); filter: brightness(110%); }
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: var(--on-surface-variant);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
border: 2px solid rgba(0,0,0,0.05);
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
background: white;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 4px rgba(255, 107, 107, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.switch-form {
|
||||
margin-top: 24px;
|
||||
font-size: 14px;
|
||||
color: var(--on-surface-variant);
|
||||
}
|
||||
|
||||
.switch-form a {
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
margin-left: 4px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.switch-form a::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--primary);
|
||||
transform: scaleX(0);
|
||||
transition: transform 0.3s ease;
|
||||
transform-origin: right;
|
||||
}
|
||||
|
||||
.switch-form a:hover::after {
|
||||
transform: scaleX(1);
|
||||
transform-origin: left;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@@ -98,6 +98,17 @@ db.serialize(() => {
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||
)`);
|
||||
|
||||
// Exercise Logs table
|
||||
db.run(`CREATE TABLE IF NOT EXISTS exercise_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
exercise_type TEXT NOT NULL,
|
||||
duration INTEGER NOT NULL,
|
||||
completed BOOLEAN DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id)
|
||||
)`);
|
||||
});
|
||||
|
||||
// JWT authentication middleware
|
||||
@@ -376,6 +387,27 @@ app.post('/api/gratitude', authenticateToken, (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Log Exercise Session
|
||||
app.post('/api/exercises', authenticateToken, (req, res) => {
|
||||
try {
|
||||
const { exerciseType, duration } = req.body;
|
||||
|
||||
if (!exerciseType || !duration) {
|
||||
return res.status(400).json({ error: 'Type and duration required' });
|
||||
}
|
||||
|
||||
db.run('INSERT INTO exercise_logs (user_id, exercise_type, duration) VALUES (?, ?, ?)',
|
||||
[req.user.id, exerciseType, duration], function(err) {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: 'Database error' });
|
||||
}
|
||||
res.status(201).json({ success: true, id: this.lastID });
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get gratitude entries
|
||||
app.get('/api/gratitude', authenticateToken, (req, res) => {
|
||||
const limit = parseInt(req.query.limit) || 30;
|
||||
@@ -427,8 +459,9 @@ app.get('/api/dashboard/stats', authenticateToken, (req, res) => {
|
||||
db.get(`SELECT
|
||||
(SELECT COUNT(*) FROM mood_entries WHERE user_id = ?) as totalMoods,
|
||||
(SELECT COUNT(*) FROM thoughts WHERE user_id = ?) as totalThoughts,
|
||||
(SELECT COUNT(*) FROM gratitude_entries WHERE user_id = ?) as totalGratitude`,
|
||||
[req.user.id, req.user.id, req.user.id], (err, totals) => {
|
||||
(SELECT COUNT(*) FROM gratitude_entries WHERE user_id = ?) as totalGratitude,
|
||||
(SELECT COUNT(*) FROM exercise_logs WHERE user_id = ?) as totalSessions`,
|
||||
[req.user.id, req.user.id, req.user.id, req.user.id], (err, totals) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: 'Database error' });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user