zkt26/z1/frontend/public/app.js

253 lines
8.0 KiB
JavaScript

// ===== Task Manager Frontend Application =====
const API_BASE = '/api';
let tasks = [];
let currentFilter = 'all';
// ===== DOM Elements =====
const taskList = document.getElementById('task-list');
const emptyState = document.getElementById('empty-state');
const loadingState = document.getElementById('loading-state');
const addTaskForm = document.getElementById('add-task-form');
const taskTitleInput = document.getElementById('task-title');
const taskDescInput = document.getElementById('task-description');
const filterTabs = document.querySelectorAll('.filter-tab');
const toast = document.getElementById('toast');
// ===== API Functions =====
async function apiRequest(url, options = {}) {
try {
const response = await fetch(`${API_BASE}${url}`, {
headers: { 'Content-Type': 'application/json' },
...options,
});
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(err.error || `HTTP ${response.status}`);
}
return await response.json();
} catch (err) {
if (err.name === 'TypeError') {
throw new Error('Cannot connect to server. Is the API running?');
}
throw err;
}
}
async function fetchTasks() {
const data = await apiRequest('/tasks');
tasks = data.tasks || [];
// Update source indicator
const sourceEl = document.querySelector('#stat-source .stat-value');
if (sourceEl) {
sourceEl.textContent = data.source === 'cache' ? '⚡ Cache' : '🗄️ DB';
}
return tasks;
}
async function createTask(title, description) {
const data = await apiRequest('/tasks', {
method: 'POST',
body: JSON.stringify({ title, description }),
});
return data.task;
}
async function updateTask(id, updates) {
const data = await apiRequest(`/tasks/${id}`, {
method: 'PUT',
body: JSON.stringify(updates),
});
return data.task;
}
async function deleteTask(id) {
await apiRequest(`/tasks/${id}`, { method: 'DELETE' });
}
// ===== UI Rendering =====
function getFilteredTasks() {
switch (currentFilter) {
case 'active':
return tasks.filter(t => !t.completed);
case 'completed':
return tasks.filter(t => t.completed);
default:
return tasks;
}
}
function formatDate(dateStr) {
const date = new Date(dateStr);
return date.toLocaleDateString('en-US', {
month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'
});
}
function renderTasks() {
const filtered = getFilteredTasks();
// Update stats
const total = tasks.length;
const completed = tasks.filter(t => t.completed).length;
const active = total - completed;
document.querySelector('#stat-total .stat-value').textContent = total;
document.querySelector('#stat-active .stat-value').textContent = active;
document.querySelector('#stat-completed .stat-value').textContent = completed;
// Show/hide states
loadingState.style.display = 'none';
if (filtered.length === 0) {
taskList.innerHTML = '';
emptyState.style.display = 'block';
if (tasks.length > 0) {
emptyState.querySelector('p').textContent = `No ${currentFilter} tasks.`;
} else {
emptyState.querySelector('p').textContent = 'No tasks yet. Add your first task above!';
}
return;
}
emptyState.style.display = 'none';
taskList.innerHTML = filtered.map(task => `
<div class="task-item ${task.completed ? 'completed' : ''}" data-id="${task.id}">
<label class="task-checkbox">
<input type="checkbox"
${task.completed ? 'checked' : ''}
onchange="handleToggle(${task.id}, this.checked)"
aria-label="Mark task as ${task.completed ? 'incomplete' : 'complete'}">
<span class="checkmark">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
</span>
</label>
<div class="task-content">
<div class="task-title">${escapeHtml(task.title)}</div>
${task.description ? `<div class="task-description">${escapeHtml(task.description)}</div>` : ''}
<div class="task-meta">${formatDate(task.created_at)}</div>
</div>
<div class="task-actions">
<button class="btn-icon" onclick="handleDelete(${task.id})" title="Delete task" aria-label="Delete task">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
<line x1="10" y1="11" x2="10" y2="17"/>
<line x1="14" y1="11" x2="14" y2="17"/>
</svg>
</button>
</div>
</div>
`).join('');
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// ===== Toast Notifications =====
let toastTimeout;
function showToast(message, type = 'info') {
clearTimeout(toastTimeout);
toast.textContent = message;
toast.className = `toast ${type} show`;
toastTimeout = setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
// ===== Event Handlers =====
async function handleToggle(id, completed) {
try {
await updateTask(id, { completed });
await fetchTasks();
renderTasks();
showToast(completed ? '✅ Task completed!' : '🔄 Task reopened', 'success');
} catch (err) {
showToast(err.message, 'error');
await fetchTasks();
renderTasks();
}
}
async function handleDelete(id) {
try {
await deleteTask(id);
const el = document.querySelector(`.task-item[data-id="${id}"]`);
if (el) {
el.style.transform = 'translateX(100px)';
el.style.opacity = '0';
await new Promise(r => setTimeout(r, 250));
}
await fetchTasks();
renderTasks();
showToast('🗑️ Task deleted', 'success');
} catch (err) {
showToast(err.message, 'error');
}
}
// ===== Form Submit =====
addTaskForm.addEventListener('submit', async (e) => {
e.preventDefault();
const title = taskTitleInput.value.trim();
const description = taskDescInput.value.trim();
if (!title) return;
try {
await createTask(title, description);
taskTitleInput.value = '';
taskDescInput.value = '';
taskTitleInput.focus();
await fetchTasks();
renderTasks();
showToast('✨ Task created!', 'success');
} catch (err) {
showToast(err.message, 'error');
}
});
// ===== Filter Tabs =====
filterTabs.forEach(tab => {
tab.addEventListener('click', () => {
filterTabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
currentFilter = tab.dataset.filter;
renderTasks();
});
});
// ===== Initialize =====
async function init() {
loadingState.style.display = 'block';
emptyState.style.display = 'none';
try {
await fetchTasks();
renderTasks();
} catch (err) {
loadingState.style.display = 'none';
showToast('Failed to load tasks: ' + err.message, 'error');
emptyState.style.display = 'block';
emptyState.querySelector('p').textContent = 'Unable to connect to the server.';
}
}
// Start the application
init();
// Auto-refresh every 30 seconds
setInterval(async () => {
try {
await fetchTasks();
renderTasks();
} catch (err) {
// Silent fail on auto-refresh
}
}, 30000);