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:
Keith Smith
2025-12-18 17:41:29 -07:00
co-authored by Claude Sonnet 4.5
commit 6a0b35c39c
84 changed files with 7763 additions and 0 deletions
+1028
View File
File diff suppressed because it is too large Load Diff
+549
View File
@@ -0,0 +1,549 @@
/* KeepItGoing - Main Stylesheet */
/* CSS Variables */
:root {
--primary-color: #3B82F6;
--primary-hover: #2563EB;
--secondary-color: #6B7280;
--success-color: #10B981;
--warning-color: #F59E0B;
--danger-color: #EF4444;
--background-color: #F3F4F6;
--card-background: #FFFFFF;
--text-color: #1F2937;
--text-muted: #6B7280;
--border-color: #E5E7EB;
--border-radius: 8px;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* Reset */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/* Base */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background-color: var(--background-color);
color: var(--text-color);
line-height: 1.5;
min-height: 100vh;
}
a {
color: var(--primary-color);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* Navbar */
.navbar {
background-color: var(--card-background);
border-bottom: 1px solid var(--border-color);
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
z-index: 100;
}
.nav-brand a {
font-size: 1.25rem;
font-weight: 700;
color: var(--primary-color);
}
.nav-links {
display: flex;
gap: 1.5rem;
}
.nav-links a {
color: var(--text-muted);
font-weight: 500;
}
.nav-links a:hover,
.nav-links a.active {
color: var(--primary-color);
text-decoration: none;
}
.nav-user {
display: flex;
gap: 1rem;
align-items: center;
}
/* Container */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
/* Cards */
.card {
background-color: var(--card-background);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
padding: 1.5rem;
margin-bottom: 1.5rem;
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.5rem 1rem;
border-radius: var(--border-radius);
font-weight: 500;
cursor: pointer;
border: none;
transition: all 0.2s;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-primary:hover {
background-color: var(--primary-hover);
text-decoration: none;
}
.btn-secondary {
background-color: var(--background-color);
color: var(--text-color);
border: 1px solid var(--border-color);
}
.btn-secondary:hover {
background-color: var(--border-color);
text-decoration: none;
}
.btn-danger {
background-color: var(--danger-color);
color: white;
}
.btn-danger:hover {
background-color: #DC2626;
}
.btn-full {
width: 100%;
}
.btn-small {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
/* Forms */
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
font-size: 1rem;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.form-group small {
color: var(--text-muted);
font-size: 0.875rem;
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
.checkbox-label input {
width: auto;
}
/* Alerts */
.alert {
padding: 1rem;
border-radius: var(--border-radius);
margin-bottom: 1rem;
}
.alert-error {
background-color: #FEE2E2;
color: #991B1B;
border: 1px solid #FECACA;
}
.alert-success {
background-color: #D1FAE5;
color: #065F46;
border: 1px solid #A7F3D0;
}
/* Auth Pages */
.auth-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 80vh;
}
.auth-card {
background-color: var(--card-background);
border-radius: var(--border-radius);
box-shadow: var(--shadow-lg);
padding: 2rem;
width: 100%;
max-width: 400px;
}
.auth-card h1 {
margin-bottom: 1.5rem;
text-align: center;
}
.auth-footer {
text-align: center;
margin-top: 1.5rem;
color: var(--text-muted);
}
/* Page Header */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.page-header h1 {
font-size: 1.5rem;
}
/* Task List */
.task-section {
margin-bottom: 2rem;
}
.section-title {
font-size: 1rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.75rem;
}
.section-overdue {
color: var(--danger-color);
}
.task-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.task-item {
display: flex;
align-items: center;
gap: 1rem;
background-color: var(--card-background);
border-radius: var(--border-radius);
padding: 1rem;
box-shadow: var(--shadow);
border-left: 4px solid transparent;
}
.task-item.priority-urgent {
border-left-color: var(--danger-color);
}
.task-item.priority-high {
border-left-color: var(--warning-color);
}
.task-item.priority-medium {
border-left-color: var(--primary-color);
}
.task-item.priority-low {
border-left-color: var(--secondary-color);
}
.task-item.task-overdue {
background-color: #FEF2F2;
}
.task-checkbox {
width: 24px;
height: 24px;
border: 2px solid var(--border-color);
border-radius: 50%;
background: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: white;
flex-shrink: 0;
}
.task-checkbox.checked {
background-color: var(--success-color);
border-color: var(--success-color);
}
.task-content {
flex: 1;
min-width: 0;
}
.task-title {
font-weight: 500;
color: var(--text-color);
display: block;
margin-bottom: 0.25rem;
}
.task-title:hover {
color: var(--primary-color);
}
.task-meta {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
font-size: 0.875rem;
}
.task-due {
color: var(--text-muted);
}
.task-due.overdue {
color: var(--danger-color);
font-weight: 500;
}
.task-group,
.task-tag {
padding: 0.125rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
}
.task-priority {
font-size: 0.75rem;
text-transform: uppercase;
font-weight: 500;
padding: 0.25rem 0.5rem;
border-radius: var(--border-radius);
}
.priority-urgent {
color: var(--danger-color);
}
.priority-high {
color: var(--warning-color);
}
.priority-medium {
color: var(--primary-color);
}
.priority-low {
color: var(--secondary-color);
}
/* Quick Add */
.quick-add-form {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.quick-add-form input[type="text"] {
flex: 1;
min-width: 200px;
}
.quick-add-form input[type="date"],
.quick-add-form select {
width: auto;
}
/* Timer */
.timer-card {
display: flex;
justify-content: space-between;
align-items: center;
background-color: var(--primary-color);
color: white;
}
.timer-display {
font-size: 1.5rem;
font-weight: 700;
font-family: monospace;
}
/* Filters */
.filter-form {
display: flex;
gap: 1rem;
flex-wrap: wrap;
align-items: center;
}
.filter-form select {
width: auto;
min-width: 150px;
}
/* Groups */
.group-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
}
.group-card {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.group-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.group-description {
color: var(--text-muted);
font-size: 0.875rem;
}
.group-stats {
display: flex;
gap: 1rem;
font-size: 0.875rem;
color: var(--text-muted);
}
.group-actions {
margin-top: 0.5rem;
}
/* Tags */
.tag-select {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
/* Empty State */
.empty-state {
color: var(--text-muted);
text-align: center;
padding: 2rem;
}
/* Time Entries */
.time-entries {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.time-entry {
display: flex;
gap: 0.5rem;
padding: 0.5rem;
background-color: var(--background-color);
border-radius: var(--border-radius);
font-size: 0.875rem;
}
.time-total {
font-weight: 600;
margin-bottom: 1rem;
}
/* Utilities */
.inline-form {
display: inline;
}
/* Footer */
.footer {
text-align: center;
padding: 2rem;
color: var(--text-muted);
font-size: 0.875rem;
}
/* Responsive */
@media (max-width: 768px) {
.navbar {
flex-direction: column;
gap: 1rem;
}
.nav-links {
order: 3;
}
.page-header {
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
.form-row {
grid-template-columns: 1fr;
}
}
+710
View File
@@ -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.');
});
};
+99
View File
@@ -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();
}
}