Yuyao Huang 5e827e7d99 fix: refresh side panel after loadTasks in non-landscape mode
After saveTask or setTaskStatus triggers loadTasks(), the side panel
now refreshes from the updated server data. If the selected task no
longer exists (deleted or goal changed), the side panel closes.
This ensures Edit Task always matches selectedTaskId.
2026-05-09 16:37:26 +08:00

437 lines
14 KiB
JavaScript

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 =>
`<option value="${goal.id}">${escapeHtml(goal.title)}</option>`
).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);
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);
}
}
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");
}
}
// Refresh side panel if a task was selected
if (selectedTaskId) {
const currentTask = tasks.find(t => t.id === selectedTaskId);
if (currentTask) {
document.getElementById("edit-task-title").value = currentTask.title;
document.getElementById("edit-task-desc").value = currentTask.desc || "";
document.querySelectorAll(".status-btn").forEach(btn => {
btn.classList.toggle("active", btn.dataset.status === currentTask.status);
});
updateSaveButton();
} else {
closeSidePanel();
}
}
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 = `
<div class="empty-state">
<h3>No tasks yet</h3>
<p>Create your first task for this goal!</p>
</div>
`;
return;
}
container.innerHTML = tasks.map(task => `
<div class="task-item ${task.status}" data-task-id="${task.id}" onclick="selectTask(${task.id})">
<div class="task-title">${escapeHtml(task.title)}</div>
<span class="task-status-badge ${task.status}">${task.status}</span>
</div>
`).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.querySelectorAll(".task-item.in-focus").forEach(el => el.classList.remove("in-focus"));
const taskEl = document.querySelector(`[data-task-id="${taskId}"]`);
if (taskEl) {
taskEl.classList.add("in-focus");
}
scrollToTask(taskId);
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();
});