Compare commits

..
6 Commits
Author SHA1 Message Date
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
Yuyao Huang fcee783ee5 feat: clicking a task now scrolls to and highlights it
selectTask() now also sets in-focus class and calls scrollToTask(),
making click behavior consistent with scroll-to-focus behavior.
Removed selectedTaskId assignment in loadTasks since selectTask
already sets it.
2026-05-09 16:34:04 +08:00
Yuyao Huang 3f0fccd218 feat: disable Save button when content unchanged
- Save button starts disabled and only enables when title or description
  differs from the original task values
- updateSaveButton() compares current input values against task data
- input event listeners on title and desc fields call updateSaveButton
- Add global button:disabled style (opacity 0.5, cursor not-allowed)
2026-05-09 16:26:41 +08:00
Yuyao Huang ea21b0c78c feat: status buttons as one-row group, wider side panel
- Status buttons now form a seamless button group (no gaps, shared borders,
  rounded ends) with flex-wrap: nowrap to keep them in one row
- Side panel width increased from 350px to 400px for more content space
2026-05-09 16:21:36 +08:00
Yuyao Huang 9c1d45506a feat: replace status dropdown with one-click toggle buttons
Replace <select> in side panel with 4 toggle buttons (To Do / Doing /
Pending / Done). Clicking a button immediately sends the PATCH status
API call. Active button is highlighted with status-specific colors and
shadow. saveTask now only handles title/description changes.
2026-05-09 16:16:58 +08:00
Yuyao Huang 43ca6b8462 fix: correct task sort order per status
DONE: finished_time ASC, PENDING: start_time ASC, TODO: order ASC
2026-05-09 16:13:05 +08:00
5 changed files with 161 additions and 24 deletions
+13 -7
View File
@@ -202,19 +202,25 @@ def get_tasks_sorted(goal_id):
try:
cur = conn.execute(
"""SELECT * FROM tasks WHERE goal_id = ? AND status = 'done'
ORDER BY finished_time DESC""",
ORDER BY finished_time ASC""",
(goal_id,)
)
finished = [row_to_dict(r) for r in cur.fetchall()]
cur = conn.execute(
"""SELECT * FROM tasks WHERE goal_id = ? AND status != 'done'
ORDER BY CASE status
WHEN 'doing' THEN 1
WHEN 'pending' THEN 2
WHEN 'todo' THEN 3
ELSE 4
END, "order" ASC""",
ORDER BY
CASE status
WHEN 'doing' THEN 0
WHEN 'pending' THEN 1
WHEN 'todo' THEN 2
END ASC,
CASE status
WHEN 'pending' THEN start_time
END ASC,
CASE status
WHEN 'todo' THEN "order"
END ASC""",
(goal_id,)
)
unfinished = [row_to_dict(r) for r in cur.fetchall()]
+5
View File
@@ -182,6 +182,11 @@ body {
background-color: #229954;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.error-message {
color: #e74c3c;
margin-bottom: 1rem;
+76 -1
View File
@@ -97,6 +97,81 @@
border-color: #28a745;
}
.status-toggle {
display: flex;
gap: 0;
flex-wrap: nowrap;
}
.status-btn {
padding: 0.5rem 1rem;
border: 2px solid #ddd;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.15s ease;
background: #f8f9fa;
color: #666;
margin-left: -1px;
}
.status-btn:first-child {
border-radius: 6px 0 0 6px;
margin-left: 0;
}
.status-btn:last-child {
border-radius: 0 6px 6px 0;
}
.status-btn.todo:hover {
border-color: #667eea;
color: #667eea;
}
.status-btn.todo.active {
background: #f8f9fa;
border-color: #667eea;
color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
}
.status-btn.doing:hover {
border-color: #ffc107;
color: #b8860b;
}
.status-btn.doing.active {
background: #fff3cd;
border-color: #ffc107;
color: #856404;
box-shadow: 0 0 0 2px rgba(255, 193, 7, 0.2);
}
.status-btn.pending:hover {
border-color: #2196F3;
color: #2196F3;
}
.status-btn.pending.active {
background: #e7f3ff;
border-color: #2196F3;
color: #0d6efd;
box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.2);
}
.status-btn.done:hover {
border-color: #28a745;
color: #28a745;
}
.status-btn.done.active {
background: #d4edda;
border-color: #28a745;
color: #155724;
box-shadow: 0 0 0 2px rgba(40, 167, 69, 0.2);
}
.task-item:hover {
background: #e9ecef;
}
@@ -285,7 +360,7 @@
.side-panel {
position: relative;
right: 0;
width: 350px;
width: 400px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
border-radius: 8px;
z-index: 1;
+60 -9
View File
@@ -52,7 +52,6 @@ async function loadTasks() {
if (savedTaskExists) {
focusTaskId = savedTaskId;
scrollToTask(savedTaskId);
selectedTaskId = savedTaskId;
if (isLandscapeMode()) {
selectTask(savedTaskId);
}
@@ -69,8 +68,6 @@ async function loadTasks() {
}
}
// 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"));
@@ -79,6 +76,22 @@ async function loadTasks() {
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) {
@@ -232,10 +245,22 @@ function selectTask(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("edit-task-status").value = task.status;
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")) {
@@ -243,6 +268,17 @@ function selectTask(taskId) {
}
}
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");
@@ -257,7 +293,6 @@ async function saveTask() {
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";
@@ -266,12 +301,24 @@ async function saveTask() {
try {
await put(`/api/tasks/${selectedTaskId}`, { title, desc });
await loadTasks();
} catch (err) {
error.textContent = err.message;
}
}
const currentTask = tasks.find(t => t.id === selectedTaskId);
if (status !== currentTask?.status) {
await patch(`/api/tasks/${selectedTaskId}/status`, { status });
}
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;
@@ -378,8 +425,12 @@ document.addEventListener("DOMContentLoaded", () => {
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();
});
+7 -7
View File
@@ -39,13 +39,13 @@
<textarea id="edit-task-desc" rows="12" autocomplete="off"></textarea>
</div>
<div class="form-group">
<label for="edit-task-status">Status</label>
<select id="edit-task-status">
<option value="todo">To Do</option>
<option value="doing">Doing</option>
<option value="pending">Pending</option>
<option value="done">Done</option>
</select>
<label>Status</label>
<div class="status-toggle" id="status-toggle">
<button class="status-btn todo" data-status="todo" onclick="setTaskStatus('todo')">To Do</button>
<button class="status-btn doing" data-status="doing" onclick="setTaskStatus('doing')">Doing</button>
<button class="status-btn pending" data-status="pending" onclick="setTaskStatus('pending')">Pending</button>
<button class="status-btn done" data-status="done" onclick="setTaskStatus('done')">Done</button>
</div>
</div>
<div id="side-panel-error" class="error-message"></div>
<div class="side-panel-actions">