/** * KeepItGoing - Main Application JavaScript * Handles theme toggle, task selection, mobile menus, and AJAX operations */ document.addEventListener('DOMContentLoaded', function() { initTheme(); initTaskSelection(); initTimerDisplays(); }); /* ============================================ Theme Toggle ============================================ */ function initTheme() { // Check for saved theme preference or system preference const savedTheme = localStorage.getItem('theme'); const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches; if (savedTheme) { setTheme(savedTheme); } else if (systemDark) { setTheme('dark'); } // Listen for system theme changes window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => { if (!localStorage.getItem('theme')) { setTheme(e.matches ? 'dark' : 'light'); } }); } function toggleTheme() { const currentTheme = document.documentElement.getAttribute('data-theme') || 'light'; const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; setTheme(newTheme); localStorage.setItem('theme', newTheme); } function setTheme(theme) { document.documentElement.setAttribute('data-theme', theme); // Update theme toggle icon const lightIcon = document.getElementById('theme-icon-light'); const darkIcon = document.getElementById('theme-icon-dark'); if (lightIcon && darkIcon) { if (theme === 'dark') { lightIcon.style.display = 'none'; darkIcon.style.display = 'block'; } else { lightIcon.style.display = 'block'; darkIcon.style.display = 'none'; } } } /* ============================================ Task Selection ============================================ */ function initTaskSelection() { // Add click handlers to task items const taskItems = document.querySelectorAll('.task-item'); taskItems.forEach(item => { item.addEventListener('click', function(e) { // Don't select if clicking checkbox form if (e.target.closest('.task-toggle')) { return; } const taskId = this.dataset.taskId; if (taskId) { selectTask(taskId); } }); }); } function selectTask(taskId) { // Update URL without full page reload const url = new URL(window.location); url.searchParams.set('selected', taskId); window.history.pushState({}, '', url); // Update visual selection document.querySelectorAll('.task-item').forEach(item => { item.classList.remove('selected'); }); const selectedItem = document.querySelector(`.task-item[data-task-id="${taskId}"]`); if (selectedItem) { selectedItem.classList.add('selected'); } // Load task detail via AJAX loadTaskDetail(taskId); } function loadTaskDetail(taskId) { const detailPane = document.getElementById('detail-pane'); const appLayout = document.getElementById('app'); if (!detailPane) return; // Show loading state detailPane.innerHTML = '

Loading...

'; detailPane.classList.remove('hidden'); if (appLayout) { appLayout.classList.remove('detail-closed'); } // On mobile, show detail pane if (window.innerWidth <= 1024) { detailPane.classList.add('open'); document.getElementById('mobile-overlay').classList.add('visible'); } // Fetch task detail HTML fetch(`/tasks/${taskId}/detail-partial/`) .then(response => { if (!response.ok) throw new Error('Failed to load task'); return response.text(); }) .then(html => { detailPane.innerHTML = html; }) .catch(error => { console.error('Error loading task detail:', error); detailPane.innerHTML = `

Failed to load task details

Please refresh the page

`; }); } function closeDetail() { const detailPane = document.getElementById('detail-pane'); const appLayout = document.getElementById('app'); if (detailPane) { detailPane.classList.add('hidden'); detailPane.classList.remove('open'); } if (appLayout) { appLayout.classList.add('detail-closed'); } // Clear selection document.querySelectorAll('.task-item').forEach(item => { item.classList.remove('selected'); }); // Update URL const url = new URL(window.location); url.searchParams.delete('selected'); window.history.pushState({}, '', url); // Close mobile overlay closeMobileMenus(); } /* ============================================ Mobile Navigation ============================================ */ function toggleSidebar() { const sidebar = document.getElementById('sidebar'); const overlay = document.getElementById('mobile-overlay'); if (sidebar) { sidebar.classList.toggle('open'); } if (overlay) { overlay.classList.toggle('visible', sidebar.classList.contains('open')); } } function closeMobileMenus() { const sidebar = document.getElementById('sidebar'); const detailPane = document.getElementById('detail-pane'); const overlay = document.getElementById('mobile-overlay'); if (sidebar) { sidebar.classList.remove('open'); } if (detailPane) { detailPane.classList.remove('open'); } if (overlay) { overlay.classList.remove('visible'); } } /* ============================================ Timer Displays ============================================ */ function initTimerDisplays() { // Handle running timer in header/dashboard const runningTimer = document.getElementById('running-timer'); if (runningTimer) { const startedAt = new Date(runningTimer.dataset.started); updateTimerDisplay(runningTimer, startedAt); setInterval(() => updateTimerDisplay(runningTimer, startedAt), 1000); } } function updateTimerDisplay(element, startedAt) { const now = new Date(); const diff = Math.floor((now - startedAt) / 1000); const hours = Math.floor(diff / 3600).toString().padStart(2, '0'); const minutes = Math.floor((diff % 3600) / 60).toString().padStart(2, '0'); const seconds = (diff % 60).toString().padStart(2, '0'); element.textContent = `${hours}:${minutes}:${seconds}`; } /* ============================================ Utility Functions ============================================ */ function formatDuration(seconds) { if (seconds < 60) { return `${seconds}s`; } else if (seconds < 3600) { const mins = Math.floor(seconds / 60); return `${mins}m`; } else { const hours = Math.floor(seconds / 3600); const mins = Math.floor((seconds % 3600) / 60); return `${hours}h ${mins}m`; } } function formatRelativeDate(dateString) { const date = new Date(dateString); const today = new Date(); today.setHours(0, 0, 0, 0); const tomorrow = new Date(today); tomorrow.setDate(tomorrow.getDate() + 1); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); const dateOnly = new Date(date); dateOnly.setHours(0, 0, 0, 0); if (dateOnly.getTime() === today.getTime()) { return 'Today'; } else if (dateOnly.getTime() === tomorrow.getTime()) { return 'Tomorrow'; } else if (dateOnly.getTime() === yesterday.getTime()) { return 'Yesterday'; } else { return date.toLocaleDateString(); } } /* ============================================ Handle Browser Back/Forward ============================================ */ window.addEventListener('popstate', function(e) { const url = new URL(window.location); const selectedTaskId = url.searchParams.get('selected'); if (selectedTaskId) { // Show the selected task document.querySelectorAll('.task-item').forEach(item => { item.classList.remove('selected'); }); const selectedItem = document.querySelector(`.task-item[data-task-id="${selectedTaskId}"]`); if (selectedItem) { selectedItem.classList.add('selected'); } loadTaskDetail(selectedTaskId); } else { // Close detail panel closeDetail(); } }); /* ============================================ Group Modal ============================================ */ let currentGroupId = null; let selectedColor = '#3b82f6'; document.addEventListener('DOMContentLoaded', function() { initGroupModal(); }); function initGroupModal() { // Setup backdrop click to close const backdrop = document.getElementById('group-modal-backdrop'); if (backdrop) { backdrop.addEventListener('click', function(e) { if (e.target === backdrop) { closeGroupModal(); } }); } // Setup color preset buttons document.querySelectorAll('.color-preset').forEach(btn => { btn.addEventListener('click', function() { selectColor(this.dataset.color); }); }); // Setup custom color picker const colorPicker = document.getElementById('group-color'); if (colorPicker) { colorPicker.addEventListener('input', function() { selectColor(this.value, true); }); } // Setup name field for preview const nameField = document.getElementById('group-name'); if (nameField) { nameField.addEventListener('input', updateGroupPreview); } // Setup form submission const form = document.getElementById('group-form'); if (form) { form.addEventListener('submit', handleGroupSubmit); } } // Make openGroupModal globally available for onclick handlers window.openGroupModal = function(groupId = null, name = '', color = '#3b82f6') { console.log('openGroupModal called:', { groupId, name, color }); const backdrop = document.getElementById('group-modal-backdrop'); const title = document.getElementById('group-modal-title'); const nameField = document.getElementById('group-name'); const deleteBtn = document.getElementById('group-delete-btn'); const submitBtn = document.getElementById('group-submit-btn'); const groupIdField = document.getElementById('group-id'); if (!backdrop) { console.error('Modal backdrop not found!'); return; } currentGroupId = groupId; if (groupId) { // Edit mode if (title) title.textContent = 'Edit Group'; if (nameField) nameField.value = name; if (deleteBtn) deleteBtn.style.display = 'inline-flex'; if (submitBtn) submitBtn.textContent = 'Save'; if (groupIdField) groupIdField.value = groupId; } else { // Create mode if (title) title.textContent = 'New Group'; if (nameField) nameField.value = ''; if (deleteBtn) deleteBtn.style.display = 'none'; if (submitBtn) submitBtn.textContent = 'Create'; if (groupIdField) groupIdField.value = ''; color = '#3b82f6'; } selectColor(color); updateGroupPreview(); backdrop.classList.add('open'); console.log('Modal should now be open'); if (nameField) nameField.focus(); }; // Make closeGroupModal globally available for onclick handlers window.closeGroupModal = function() { const backdrop = document.getElementById('group-modal-backdrop'); if (backdrop) { backdrop.classList.remove('open'); } currentGroupId = null; }; function selectColor(color, isCustom = false) { selectedColor = color; // Update preset button selection document.querySelectorAll('.color-preset').forEach(btn => { btn.classList.toggle('selected', btn.dataset.color.toLowerCase() === color.toLowerCase()); }); // Update custom color picker if not already from custom if (!isCustom) { const colorPicker = document.getElementById('group-color'); if (colorPicker) { colorPicker.value = color; } } // Update hidden field const hiddenField = document.getElementById('group-color-hidden'); if (hiddenField) { hiddenField.value = color; } // Update preview updateGroupPreview(); } function updateGroupPreview() { const nameField = document.getElementById('group-name'); const previewDot = document.getElementById('group-preview-dot'); const previewName = document.getElementById('group-preview-name'); if (previewDot) { previewDot.style.backgroundColor = selectedColor; } if (previewName && nameField) { previewName.textContent = nameField.value || 'Group Preview'; previewName.style.color = nameField.value ? 'var(--text-primary)' : 'var(--text-muted)'; } } function handleGroupSubmit(e) { e.preventDefault(); const nameField = document.getElementById('group-name'); const name = nameField.value.trim(); if (!name) { nameField.focus(); return; } const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value; const formData = new FormData(); formData.append('name', name); formData.append('color', selectedColor); formData.append('csrfmiddlewaretoken', csrfToken); let url, method; if (currentGroupId) { // Update existing group url = `/groups/${currentGroupId}/`; formData.append('next', window.location.pathname + window.location.search); } else { // Create new group url = '/groups/'; formData.append('next', window.location.pathname + window.location.search); } fetch(url, { method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then(response => { if (response.redirected) { window.location.href = response.url; } else if (response.ok) { window.location.reload(); } else { throw new Error('Failed to save group'); } }) .catch(error => { console.error('Error saving group:', error); alert('Failed to save group. Please try again.'); }); } // Make deleteGroup globally available for onclick handlers window.deleteGroup = function() { if (!currentGroupId) return; if (!confirm('Are you sure you want to delete this group?\n\nTasks in this group will not be deleted.')) { return; } const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value; const formData = new FormData(); formData.append('csrfmiddlewaretoken', csrfToken); formData.append('next', window.location.pathname + window.location.search); fetch(`/groups/${currentGroupId}/delete/`, { method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then(response => { if (response.redirected) { window.location.href = response.url; } else if (response.ok) { window.location.reload(); } else { throw new Error('Failed to delete group'); } }) .catch(error => { console.error('Error deleting group:', error); alert('Failed to delete group. Please try again.'); }); }; /* ============================================ Tag Modal ============================================ */ let currentTagId = null; let selectedTagColor = '#3b82f6'; document.addEventListener('DOMContentLoaded', function() { initTagModal(); }); function initTagModal() { // Setup backdrop click to close const backdrop = document.getElementById('tag-modal-backdrop'); if (backdrop) { backdrop.addEventListener('click', function(e) { if (e.target === backdrop) { closeTagModal(); } }); } // Setup color preset buttons document.querySelectorAll('.color-preset').forEach(btn => { btn.addEventListener('click', function() { selectTagColor(this.dataset.color); }); }); // Setup custom color picker const colorPicker = document.getElementById('tag-color'); if (colorPicker) { colorPicker.addEventListener('input', function() { selectTagColor(this.value, true); }); } // Setup form submission const form = document.getElementById('tag-form'); if (form) { form.addEventListener('submit', handleTagSubmit); } } function handleTagSubmit(e) { e.preventDefault(); const nameField = document.getElementById('tag-name'); const name = nameField.value.trim(); if (!name) { nameField.focus(); return; } const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value; const formData = new FormData(); formData.append('name', name); formData.append('color', selectedTagColor); formData.append('csrfmiddlewaretoken', csrfToken); let url; if (currentTagId) { // Update existing tag url = `/tags/${currentTagId}/`; formData.append('next', window.location.pathname + window.location.search); } else { // Create new tag url = '/tags/'; formData.append('next', window.location.pathname + window.location.search); } fetch(url, { method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then(response => { if (response.redirected) { window.location.href = response.url; } else if (response.ok) { window.location.reload(); } else { throw new Error('Failed to save tag'); } }) .catch(error => { console.error('Error saving tag:', error); alert('Failed to save tag. Please try again.'); }); } // Make openTagModal globally available for onclick handlers window.openTagModal = function(tagId = null, name = '', color = '#3b82f6') { console.log('openTagModal called:', { tagId, name, color }); const backdrop = document.getElementById('tag-modal-backdrop'); const title = document.getElementById('tag-modal-title'); const nameField = document.getElementById('tag-name'); const deleteBtn = document.getElementById('tag-delete-btn'); const submitBtn = document.getElementById('tag-submit-btn'); const tagIdField = document.getElementById('tag-id'); if (!backdrop) { console.error('Tag modal backdrop not found!'); return; } currentTagId = tagId; if (tagId) { // Edit mode if (title) title.textContent = 'Edit Tag'; if (nameField) nameField.value = name; if (deleteBtn) deleteBtn.style.display = 'inline-flex'; if (submitBtn) submitBtn.textContent = 'Save'; if (tagIdField) tagIdField.value = tagId; } else { // Create mode if (title) title.textContent = 'New Tag'; if (nameField) nameField.value = ''; if (deleteBtn) deleteBtn.style.display = 'none'; if (submitBtn) submitBtn.textContent = 'Create'; if (tagIdField) tagIdField.value = ''; color = '#3b82f6'; } selectTagColor(color); backdrop.classList.add('open'); if (nameField) nameField.focus(); }; // Make closeTagModal globally available for onclick handlers window.closeTagModal = function() { const backdrop = document.getElementById('tag-modal-backdrop'); if (backdrop) { backdrop.classList.remove('open'); } currentTagId = null; }; function selectTagColor(color) { selectedTagColor = color; // Update preset button selection document.querySelectorAll('.color-preset').forEach(btn => { btn.classList.toggle('selected', btn.dataset.color.toLowerCase() === color.toLowerCase()); }); // Update custom color picker const colorPicker = document.getElementById('tag-color'); if (colorPicker) { colorPicker.value = color; } // Update hidden field const hiddenField = document.getElementById('tag-color-hidden'); if (hiddenField) { hiddenField.value = color; } } // Make deleteTag globally available for onclick handlers window.deleteTag = function() { if (!currentTagId) return; if (!confirm('Are you sure you want to delete this tag?\n\nTasks with this tag will not be deleted.')) { return; } const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value; const formData = new FormData(); formData.append('csrfmiddlewaretoken', csrfToken); formData.append('next', window.location.pathname + window.location.search); fetch(`/tags/${currentTagId}/delete/`, { method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then(response => { if (response.redirected) { window.location.href = response.url; } else if (response.ok) { window.location.reload(); } else { throw new Error('Failed to delete tag'); } }) .catch(error => { console.error('Error deleting tag:', error); alert('Failed to delete tag. Please try again.'); }); };