From 3fbab2b83139e9eae1ab7c286a3fdaf326306ff6 Mon Sep 17 00:00:00 2001 From: Keith Smith Date: Sat, 5 Sep 2026 00:05:39 -0600 Subject: [PATCH] Add sidebar Tags section to offline mode Offline mode had tag chips and per-task tag assignment, but was missing the dashboard sidebar's Tags section entirely: filtering the task list by tag, per-tag task counts, and tag create/rename/delete. Ports that section over (reusing the same tag modal pattern as the online create/edit flow), combinable with the existing status filters the same way it works online. Co-Authored-By: Claude Sonnet 5 --- static/js/offline-tasks.js | 180 ++++++++++++++++++++++++++++++++++- templates/offline_tasks.html | 44 +++++++++ templates/sw.js | 2 +- 3 files changed, 221 insertions(+), 5 deletions(-) diff --git a/static/js/offline-tasks.js b/static/js/offline-tasks.js index 52eac39..fb4a9ad 100644 --- a/static/js/offline-tasks.js +++ b/static/js/offline-tasks.js @@ -13,8 +13,10 @@ const TAG_COLOR_PRESETS = ['#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6' let currentFilter = 'all'; let currentSort = 'due_date'; +let currentTagId = null; let openTaskSyncId = null; let timerDisplayInterval = null; +let currentTagModalSyncId = null; /* ============================================ Date/format helpers @@ -64,6 +66,11 @@ function applyFilter(tasks, filter) { } } +function applyTagFilter(tasks, tagId) { + if (!tagId) return tasks; + return tasks.filter((t) => (t.tag_sync_ids || []).includes(tagId)); +} + function applySort(tasks, sort) { const sorted = [...tasks]; const dueOrDefault = (t) => t.due_date || '9999-99-99'; @@ -217,7 +224,17 @@ async function renderOfflineTasks() { // Only top-level tasks in the main list - subtasks appear inside their // parent's detail modal, matching the online dashboard. const topLevel = allTasks.filter((t) => !t.is_deleted && !t.parent_sync_id); - const visibleTasks = applySort(applyFilter(topLevel, currentFilter), currentSort); + const statusFiltered = applyFilter(topLevel, currentFilter); + const visibleTasks = applySort(applyTagFilter(statusFiltered, currentTagId), currentSort); + + setActiveFilterNav(currentFilter); + renderTagSidebarNav(allTags, topLevel); + + const titleEl = document.getElementById('offline-page-title'); + if (titleEl) { + const tagName = currentTagId && tagsBySyncId[currentTagId] ? tagsBySyncId[currentTagId].name : null; + titleEl.textContent = (FILTER_TITLES[currentFilter] || 'All Tasks') + (tagName ? ` • ${tagName}` : ''); + } listEl.innerHTML = ''; @@ -244,8 +261,150 @@ function setActiveFilterNav(filter) { document.querySelectorAll('#offline-filter-nav .sidebar-item').forEach((item) => { item.classList.toggle('active', item.dataset.filter === filter); }); - const titleEl = document.getElementById('offline-page-title'); - if (titleEl) titleEl.textContent = FILTER_TITLES[filter] || 'All Tasks'; +} + +/* ============================================ + Sidebar Tags section (filter-by-tag + manage) + ============================================ */ + +function renderTagSidebarNav(allTags, topLevelTasks) { + const nav = document.getElementById('offline-tag-nav'); + if (!nav) return; + + const visibleTags = allTags.filter((t) => !t.is_deleted && !t.is_archived); + // Counts match the sidebar Filters' "all" semantics: top-level, not completed. + const countable = topLevelTasks.filter((t) => t.status !== 'completed'); + + nav.innerHTML = visibleTags.map((tag) => { + const count = countable.filter((t) => (t.tag_sync_ids || []).includes(tag.sync_id)).length; + return ` + + `; + }).join('') + ` + + + All Tags + + `; + + nav.querySelectorAll('.sidebar-group-item[data-tag-id]').forEach((link) => { + link.addEventListener('click', (e) => { + e.preventDefault(); + currentTagId = link.dataset.tagId; + renderOfflineTasks(); + }); + }); + nav.querySelectorAll('.sidebar-group-edit').forEach((btn) => { + btn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + const tag = visibleTags.find((t) => t.sync_id === btn.dataset.editTagId); + if (tag) openTagModal(tag.sync_id, tag.name, tag.color); + }); + }); + const allTagsLink = document.getElementById('offline-all-tags-link'); + if (allTagsLink) { + allTagsLink.addEventListener('click', (e) => { + e.preventDefault(); + currentTagId = null; + renderOfflineTasks(); + }); + } +} + +function renderTagColorPresets(selectedColor) { + const container = document.getElementById('tag-modal-color-presets'); + if (!container) return; + container.innerHTML = TAG_COLOR_PRESETS.map((c) => ` + + `).join(''); + container.querySelectorAll('.color-preset').forEach((btn) => { + btn.addEventListener('click', () => { + document.getElementById('tag-modal-color').value = btn.dataset.color; + container.querySelectorAll('.color-preset').forEach((b) => b.classList.toggle('selected', b === btn)); + }); + }); +} + +function openTagModal(tagSyncId = null, name = '', color = '#3b82f6') { + currentTagModalSyncId = tagSyncId; + document.getElementById('tag-modal-title').textContent = tagSyncId ? 'Edit Tag' : 'New Tag'; + document.getElementById('tag-modal-name').value = name; + document.getElementById('tag-modal-color').value = color; + document.getElementById('tag-modal-submit-btn').textContent = tagSyncId ? 'Save' : 'Create'; + document.getElementById('tag-modal-delete-btn').style.display = tagSyncId ? 'inline-flex' : 'none'; + renderTagColorPresets(color); + document.getElementById('tag-modal-backdrop').classList.add('open'); + document.getElementById('tag-modal-name').focus(); +} + +function closeTagModal() { + document.getElementById('tag-modal-backdrop').classList.remove('open'); + currentTagModalSyncId = null; +} + +async function saveTagModal() { + const name = document.getElementById('tag-modal-name').value.trim(); + if (!name) return; + const color = document.getElementById('tag-modal-color').value; + const now = new Date().toISOString(); + + if (currentTagModalSyncId) { + const tags = await getAllTags(); + const tag = tags.find((t) => t.sync_id === currentTagModalSyncId); + if (tag) { + tag.name = name; + tag.color = color; + tag.updated_at = now; + tag._dirty = true; + await putLocalTag(tag); + } + } else { + const syncId = crypto.randomUUID(); + await putLocalTag({ + id: syncId, sync_id: syncId, name, description: '', color, icon: '', + sort_order: 0, is_archived: false, is_deleted: false, + created_at: now, updated_at: now, _dirty: true, _pending_conflict_id: null, + }); + } + + closeTagModal(); + renderOfflineTasks(); + if (openTaskSyncId) { + renderTaskDetailPane(); + } +} + +async function deleteCurrentTag() { + if (!currentTagModalSyncId) return; + if (!confirm('Are you sure you want to delete this tag?\n\nTasks with this tag will not be deleted.')) { + return; + } + const tags = await getAllTags(); + const tag = tags.find((t) => t.sync_id === currentTagModalSyncId); + if (tag) { + tag.is_deleted = true; + tag.updated_at = new Date().toISOString(); + tag._dirty = true; + await putLocalTag(tag); + } + if (currentTagId === currentTagModalSyncId) { + currentTagId = null; + } + closeTagModal(); + renderOfflineTasks(); } /* ============================================ @@ -569,7 +728,6 @@ document.addEventListener('DOMContentLoaded', () => { item.addEventListener('click', (e) => { e.preventDefault(); currentFilter = item.dataset.filter; - setActiveFilterNav(currentFilter); renderOfflineTasks(); }); }); @@ -582,6 +740,20 @@ document.addEventListener('DOMContentLoaded', () => { renderOfflineTasks(); }); } + + document.getElementById('offline-add-tag-btn').addEventListener('click', () => openTagModal()); + document.getElementById('tag-modal-close-btn').addEventListener('click', closeTagModal); + document.getElementById('tag-modal-cancel-btn').addEventListener('click', closeTagModal); + document.getElementById('tag-modal-delete-btn').addEventListener('click', deleteCurrentTag); + document.getElementById('tag-edit-form').addEventListener('submit', (e) => { + e.preventDefault(); + saveTagModal(); + }); + document.getElementById('tag-modal-backdrop').addEventListener('click', (e) => { + if (e.target.id === 'tag-modal-backdrop') { + closeTagModal(); + } + }); }); /* ============================================ diff --git a/templates/offline_tasks.html b/templates/offline_tasks.html index 6bea065..481d81d 100644 --- a/templates/offline_tasks.html +++ b/templates/offline_tasks.html @@ -80,6 +80,17 @@ + + @@ -116,6 +127,39 @@
+ + + diff --git a/templates/sw.js b/templates/sw.js index 2fa17d4..026ef13 100644 --- a/templates/sw.js +++ b/templates/sw.js @@ -1,4 +1,4 @@ -{% load static %}const CACHE_NAME = 'keepitgoing-shell-v5'; +{% load static %}const CACHE_NAME = 'keepitgoing-shell-v6'; const OFFLINE_URL = '{% url "offline" %}'; const OFFLINE_TASKS_URL = '{% url "offline-tasks" %}';