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("side-panel-error").textContent = "";
updateSaveButton();
document.querySelectorAll(".status-btn").forEach(btn => {
btn.classList.toggle("active", btn.dataset.status === task.status);
});
const sidePanel = document.getElementById("side-panel");
if (!sidePanel.classList.contains("active")) {
sidePanel.classList.add("active");
}
}
function updateSaveButton() {
const task = tasks.find(t => t.id === selectedTaskId);
if (!task) return;
const title = document.getElementById("edit-task-title").value.trim();
const desc = document.getElementById("edit-task-desc").value;
const changed = title !== task.title || desc !== (task.desc || "");
document.getElementById("save-task-btn").disabled = !changed;
}
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;
if (!title) {
error.textContent = "Title is required";
return;
}
try {
await put(`/api/tasks/${selectedTaskId}`, { title, desc });
await loadTasks();
} catch (err) {
error.textContent = err.message;
}
}
async function setTaskStatus(status) {
if (!selectedTaskId) return;
const error = document.getElementById("side-panel-error");
error.textContent = "";
document.querySelectorAll(".status-btn").forEach(btn => {
btn.classList.toggle("active", btn.dataset.status === status);
});
try {
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("save-task-btn").disabled = true;
document.getElementById("delete-task-btn").addEventListener("click", deleteTask);
document.getElementById("edit-task-title").addEventListener("input", updateSaveButton);
document.getElementById("edit-task-desc").addEventListener("input", updateSaveButton);
initWheelScroll();
loadGoals();
});