Internal
Public Access
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>
711 lines
21 KiB
JavaScript
711 lines
21 KiB
JavaScript
/**
|
|
* 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.');
|
|
});
|
|
};
|