Internal
Public Access
Initial commit: KeepItGoing task management server
Features: - Django-based REST API with web interface - Task management with tags, priorities, and due dates - Time tracking with start/stop timers - Subtasks support - Task filtering (all, today, upcoming, overdue, completed) - Tag-based organization with color coding - Sorting by due date and priority - Auto-assign tags when filtering - Responsive 3-pane layout (sidebar, task list, detail panel) - Task sharing between users - Mobile-responsive design with dark mode support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,710 @@
|
||||
/**
|
||||
* 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 = '<div class="empty-state"><p>Loading...</p></div>';
|
||||
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 = `
|
||||
<div class="empty-state">
|
||||
<p class="text-danger">Failed to load task details</p>
|
||||
<p class="text-muted">Please refresh the page</p>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
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.');
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* KeepItGoing - Main JavaScript
|
||||
*/
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize components
|
||||
initTimerDisplay();
|
||||
initTaskToggle();
|
||||
initConfirmDialogs();
|
||||
});
|
||||
|
||||
/**
|
||||
* Update timer display
|
||||
*/
|
||||
function initTimerDisplay() {
|
||||
const timerDisplay = document.querySelector('.timer-display');
|
||||
if (!timerDisplay) return;
|
||||
|
||||
const startedAt = new Date(timerDisplay.dataset.started);
|
||||
|
||||
function updateTimer() {
|
||||
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');
|
||||
timerDisplay.textContent = `${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
updateTimer();
|
||||
setInterval(updateTimer, 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle task toggle without page reload (optional enhancement)
|
||||
*/
|
||||
function initTaskToggle() {
|
||||
// For now, we let the form submit normally
|
||||
// This could be enhanced with AJAX in the future
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize confirmation dialogs
|
||||
*/
|
||||
function initConfirmDialogs() {
|
||||
const deleteButtons = document.querySelectorAll('[data-confirm]');
|
||||
deleteButtons.forEach(button => {
|
||||
button.addEventListener('click', function(e) {
|
||||
const message = this.dataset.confirm || 'Are you sure?';
|
||||
if (!confirm(message)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration in seconds to human-readable string
|
||||
*/
|
||||
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`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date to relative string (today, tomorrow, etc.)
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user