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:
+1028
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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