let goals = []; let tasks = []; let selectedGoalId = null; let selectedTaskId = null; let sortableInstance = null; let persistTimer = null; let currentUser = null; async function loadGoals() { try { goals = await get("/api/goals"); currentUser = await get("/api/auth/me"); const selector = document.getElementById("goal-selector"); const activatedGoals = goals.filter(g => g.activated); selector.innerHTML = activatedGoals.map(goal => `` ).join(""); const savedGoalId = currentUser.selected_goal_id; const savedGoalExists = savedGoalId && activatedGoals.some(g => g.id === savedGoalId); if (savedGoalExists) { selectedGoalId = savedGoalId; selector.value = savedGoalId; } else if (activatedGoals.length > 0) { selectedGoalId = activatedGoals[0].id; selector.value = selectedGoalId; } await loadTasks(); } catch (error) { console.error("Failed to load goals:", error); } } async function loadTasks() { if (!selectedGoalId) return; try { tasks = await get(`/api/tasks?goal_id=${selectedGoalId}`); renderTasks(); initSortable(); const currentGoal = goals.find(g => g.id === selectedGoalId); const savedTaskId = currentGoal ? currentGoal.selected_task_id : null; const savedTaskExists = savedTaskId && tasks.some(t => t.id === savedTaskId); let focusTaskId = null; if (savedTaskExists) { focusTaskId = savedTaskId; scrollToTask(savedTaskId); selectedTaskId = savedTaskId; if (isLandscapeMode()) { selectTask(savedTaskId); } } else { const doingTask = tasks.find(t => t.status === "doing"); if (doingTask) { focusTaskId = doingTask.id; scrollToTask(doingTask.id); if (isLandscapeMode()) { selectTask(doingTask.id); } } else if (isLandscapeMode() && tasks.length > 0) { selectTask(tasks[0].id); } } // Wait one frame so the async scroll event from scrollToTask fires first, // then set in-focus and bind handlers after it has been consumed. requestAnimationFrame(() => { if (focusTaskId) { document.querySelectorAll(".task-item.in-focus").forEach(el => el.classList.remove("in-focus")); const focusEl = document.querySelector(`[data-task-id="${focusTaskId}"]`); if (focusEl) { focusEl.classList.add("in-focus"); } } initScrollFocus(); }); } catch (error) { console.error("Failed to load tasks:", error); } } async function persistSelectedTask(taskId) { if (!selectedGoalId) return; try { await patch(`/api/goals/${selectedGoalId}/selected-task`, { task_id: taskId }); const goal = goals.find(g => g.id === selectedGoalId); if (goal) { goal.selected_task_id = taskId; } console.log("Saved selected task:", taskId); } catch (error) { console.error("Failed to persist selected task:", error); } } function renderTasks() { const container = document.getElementById("tasks-list"); if (tasks.length === 0) { container.innerHTML = `

No tasks yet

Create your first task for this goal!

`; return; } container.innerHTML = tasks.map(task => `
${escapeHtml(task.title)}
${task.status}
`).join(""); } function initSortable() { const container = document.getElementById("tasks-list"); if (sortableInstance) { sortableInstance.destroy(); } sortableInstance = Sortable.create(container, { animation: 150, ghostClass: "sortable-ghost", chosenClass: "sortable-chosen", filter: ".task-item.done", onEnd: async function(evt) { const taskId = evt.item.dataset.taskId; const newIndex = evt.newIndex; const prevTask = tasks[newIndex - 1]; const nextTask = tasks[newIndex + 1]; let prevOrder = prevTask ? prevTask.order : 0; let nextOrder = nextTask ? nextTask.order : prevOrder + 2; const newOrder = (prevOrder + nextOrder) / 2; try { await patch(`/api/tasks/${taskId}/order`, { order: newOrder }); await loadTasks(); } catch (error) { console.error("Failed to update order:", error); await loadTasks(); } } }); } function scrollToTask(taskId) { const taskElement = document.querySelector(`[data-task-id="${taskId}"]`); if (taskElement) { const scrollView = document.getElementById("scroll-view"); const taskTop = taskElement.offsetTop; const scrollViewHeight = scrollView.clientHeight; const taskHeight = taskElement.offsetHeight; scrollView.scrollTop = taskTop - (scrollViewHeight / 2) + (taskHeight / 2); } } function initScrollFocus() { const scrollView = document.getElementById("scroll-view"); scrollView.removeEventListener("scroll", handleScrollFocus); scrollView.removeEventListener("scroll", handleScrollSave); scrollView.addEventListener("scroll", handleScrollFocus); scrollView.addEventListener("scroll", handleScrollSave); } function handleScrollFocus() { const scrollView = document.getElementById("scroll-view"); const taskItems = document.querySelectorAll(".task-item"); const scrollViewRect = scrollView.getBoundingClientRect(); const focusCenter = scrollViewRect.top + scrollViewRect.height / 2; let closestItem = null; let closestDistance = Infinity; taskItems.forEach(item => { const itemRect = item.getBoundingClientRect(); const itemCenter = itemRect.top + itemRect.height / 2; const distance = Math.abs(itemCenter - focusCenter); item.classList.remove("in-focus"); if (distance < closestDistance) { closestDistance = distance; closestItem = item; } }); if (closestItem) { closestItem.classList.add("in-focus"); const taskId = parseInt(closestItem.dataset.taskId); if (isLandscapeMode()) { if (taskId !== selectedTaskId) { selectTask(taskId); } } } } function handleScrollSave() { const inFocusTask = document.querySelector(".task-item.in-focus"); if (inFocusTask) { const taskId = parseInt(inFocusTask.dataset.taskId); clearTimeout(persistTimer); persistTimer = setTimeout(() => persistSelectedTask(taskId), 400); } } function isLandscapeMode() { return window.innerWidth > window.innerHeight && window.innerWidth >= 1024; } function selectTask(taskId) { selectedTaskId = taskId; const task = tasks.find(t => t.id === taskId); if (!task) return; document.getElementById("edit-task-title").value = task.title; document.getElementById("edit-task-desc").value = task.desc || ""; document.getElementById("edit-task-status").value = task.status; document.getElementById("side-panel-error").textContent = ""; const sidePanel = document.getElementById("side-panel"); if (!sidePanel.classList.contains("active")) { sidePanel.classList.add("active"); } } function closeSidePanel() { if (isLandscapeMode()) return; document.getElementById("side-panel").classList.remove("active"); selectedTaskId = null; } async function saveTask() { if (!selectedTaskId) return; const error = document.getElementById("side-panel-error"); error.textContent = ""; const title = document.getElementById("edit-task-title").value.trim(); const desc = document.getElementById("edit-task-desc").value; const status = document.getElementById("edit-task-status").value; if (!title) { error.textContent = "Title is required"; return; } try { await put(`/api/tasks/${selectedTaskId}`, { title, desc }); const currentTask = tasks.find(t => t.id === selectedTaskId); if (status !== currentTask?.status) { await patch(`/api/tasks/${selectedTaskId}/status`, { status }); } await loadTasks(); } catch (err) { error.textContent = err.message; } } async function deleteTask() { if (!selectedTaskId) return; if (!confirm("Are you sure you want to delete this task?")) return; try { await del(`/api/tasks/${selectedTaskId}`); closeSidePanel(); await loadTasks(); } catch (error) { console.error("Failed to delete task:", error); } } function openTaskModal() { document.getElementById("task-id").value = ""; document.getElementById("task-title").value = ""; document.getElementById("task-desc").value = ""; document.getElementById("task-error").textContent = ""; document.getElementById("task-modal-title").textContent = "Create Task"; document.getElementById("task-modal").classList.add("active"); } function closeTaskModal() { document.getElementById("task-modal").classList.remove("active"); } async function handleTaskSubmit(event) { event.preventDefault(); const error = document.getElementById("task-error"); error.textContent = ""; const title = document.getElementById("task-title").value.trim(); const desc = document.getElementById("task-desc").value; if (!title) { error.textContent = "Title is required"; return; } const goalId = parseInt(document.getElementById("goal-selector").value); if (!goalId) { error.textContent = "Please select a goal first"; return; } try { await post("/api/tasks", { goal_id: goalId, title, desc }); closeTaskModal(); await loadTasks(); } catch (err) { error.textContent = err.message; } } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function formatTime(isoString) { if (!isoString) return ""; const date = new Date(isoString); return date.toLocaleString(); } function initWheelScroll() { const scrollView = document.getElementById("scroll-view"); scrollView.addEventListener("wheel", (e) => { e.preventDefault(); const taskItem = scrollView.querySelector(".task-item"); if (!taskItem) return; const taskHeight = taskItem.offsetHeight + 8; const direction = e.deltaY > 0 ? 1 : -1; scrollView.scrollBy({ top: taskHeight * direction, behavior: "smooth" }); }, { passive: false }); } document.addEventListener("DOMContentLoaded", () => { document.getElementById("goal-selector").addEventListener("change", async (e) => { selectedGoalId = parseInt(e.target.value); await patch("/api/user/selected-goal", { goal_id: selectedGoalId }); loadTasks(); }); document.getElementById("create-task-btn").addEventListener("click", openTaskModal); document.getElementById("task-modal-close").addEventListener("click", closeTaskModal); document.getElementById("task-modal-cancel").addEventListener("click", closeTaskModal); document.getElementById("task-form").addEventListener("submit", handleTaskSubmit); document.getElementById("side-panel-close").addEventListener("click", closeSidePanel); document.getElementById("save-task-btn").addEventListener("click", saveTask); document.getElementById("delete-task-btn").addEventListener("click", deleteTask); initWheelScroll(); loadGoals(); });