Improve tasks UI: scroll-to-focus picker, landscape layout, height alignment
- Complete tasks now displayed in scroll view alongside unfinished tasks - Priority order: completed tasks first (by finished_time desc), then unfinished (by order asc) - Time picker-style scroll: wheel scroll snaps per task, center item gets visual focus - Landscape mode (>=1024px): scroll view + edit panel side by side, panel always visible - Portrait mode: edit panel slides in from right on tap - Fixed flex layout so scroll view and edit panel align perfectly in height
This commit is contained in:
parent
79fde447e9
commit
6b05ba3e2c
@ -134,4 +134,4 @@ def get_tasks_sorted(goal_id):
|
|||||||
unfinished.sort(key=lambda t: t.get("order", 0))
|
unfinished.sort(key=lambda t: t.get("order", 0))
|
||||||
finished.sort(key=lambda t: t.get("finished_time", ""), reverse=True)
|
finished.sort(key=lambda t: t.get("finished_time", ""), reverse=True)
|
||||||
|
|
||||||
return unfinished + finished
|
return finished + unfinished
|
||||||
|
|||||||
@ -42,7 +42,7 @@
|
|||||||
|
|
||||||
.scroll-view {
|
.scroll-view {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 300px;
|
height: 600px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@ -50,7 +50,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tasks-list {
|
.tasks-list {
|
||||||
padding: 120px 1rem;
|
padding: 45vh 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-item {
|
.task-item {
|
||||||
@ -165,63 +165,6 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.finished-section {
|
|
||||||
flex: 0 0 300px;
|
|
||||||
background: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
||||||
padding: 1rem;
|
|
||||||
max-height: 600px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.finished-section h2 {
|
|
||||||
color: #2c3e50;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.finished-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.finished-item {
|
|
||||||
padding: 0.75rem;
|
|
||||||
background: #f8f9fa;
|
|
||||||
border-radius: 4px;
|
|
||||||
border-left: 3px solid #28a745;
|
|
||||||
}
|
|
||||||
|
|
||||||
.finished-item-title {
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.finished-item-time {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #6c757d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.side-panel {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
right: -400px;
|
|
||||||
width: 400px;
|
|
||||||
height: 100%;
|
|
||||||
background: white;
|
|
||||||
box-shadow: -2px 0 8px rgba(0,0,0,0.1);
|
|
||||||
transition: right 0.3s ease;
|
|
||||||
z-index: 1000;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.side-panel.active {
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.side-panel-header {
|
.side-panel-header {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid #ddd;
|
||||||
@ -259,6 +202,82 @@
|
|||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (orientation: portrait), (max-width: 1023px) {
|
||||||
|
.side-panel {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: -400px;
|
||||||
|
width: 400px;
|
||||||
|
height: 100%;
|
||||||
|
background: white;
|
||||||
|
box-shadow: -2px 0 8px rgba(0,0,0,0.1);
|
||||||
|
transition: right 0.3s ease;
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-panel.active {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (orientation: landscape) and (min-width: 1024px) {
|
||||||
|
.tasks-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
height: calc(100vh - 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tasks-header {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tasks-main {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tasks-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-view {
|
||||||
|
height: auto;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-panel {
|
||||||
|
position: relative;
|
||||||
|
right: 0;
|
||||||
|
width: 350px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
z-index: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-panel-close {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-task-container {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 3rem;
|
padding: 3rem;
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
let goals = [];
|
let goals = [];
|
||||||
let tasks = [];
|
let tasks = [];
|
||||||
let finishedTasks = [];
|
|
||||||
let selectedGoalId = null;
|
let selectedGoalId = null;
|
||||||
let selectedTaskId = null;
|
let selectedTaskId = null;
|
||||||
let sortableInstance = null;
|
let sortableInstance = null;
|
||||||
@ -29,18 +28,20 @@ async function loadTasks() {
|
|||||||
if (!selectedGoalId) return;
|
if (!selectedGoalId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const allTasks = await get(`/api/tasks?goal_id=${selectedGoalId}`);
|
tasks = await get(`/api/tasks?goal_id=${selectedGoalId}`);
|
||||||
tasks = allTasks.filter(t => t.status !== "done");
|
|
||||||
finishedTasks = allTasks.filter(t => t.status === "done");
|
|
||||||
|
|
||||||
renderTasks();
|
renderTasks();
|
||||||
renderFinishedTasks();
|
|
||||||
initSortable();
|
initSortable();
|
||||||
initScrollFocus();
|
initScrollFocus();
|
||||||
|
|
||||||
const doingTask = tasks.find(t => t.status === "doing");
|
const doingTask = tasks.find(t => t.status === "doing");
|
||||||
if (doingTask) {
|
if (doingTask) {
|
||||||
scrollToTask(doingTask.id);
|
scrollToTask(doingTask.id);
|
||||||
|
if (isLandscapeMode()) {
|
||||||
|
selectTask(doingTask.id);
|
||||||
|
}
|
||||||
|
} else if (isLandscapeMode() && tasks.length > 0) {
|
||||||
|
selectTask(tasks[0].id);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load tasks:", error);
|
console.error("Failed to load tasks:", error);
|
||||||
@ -68,22 +69,6 @@ function renderTasks() {
|
|||||||
`).join("");
|
`).join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFinishedTasks() {
|
|
||||||
const container = document.getElementById("finished-list");
|
|
||||||
|
|
||||||
if (finishedTasks.length === 0) {
|
|
||||||
container.innerHTML = '<p style="color: #7f8c8d;">No completed tasks yet</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
container.innerHTML = finishedTasks.map(task => `
|
|
||||||
<div class="finished-item" onclick="selectTask(${task.id})">
|
|
||||||
<div class="finished-item-title">${escapeHtml(task.title)}</div>
|
|
||||||
<div class="finished-item-time">Completed: ${formatTime(task.finished_time)}</div>
|
|
||||||
</div>
|
|
||||||
`).join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
function initSortable() {
|
function initSortable() {
|
||||||
const container = document.getElementById("tasks-list");
|
const container = document.getElementById("tasks-list");
|
||||||
|
|
||||||
@ -164,12 +149,23 @@ function handleScrollFocus() {
|
|||||||
|
|
||||||
if (closestItem) {
|
if (closestItem) {
|
||||||
closestItem.classList.add("in-focus");
|
closestItem.classList.add("in-focus");
|
||||||
|
|
||||||
|
if (isLandscapeMode()) {
|
||||||
|
const taskId = parseInt(closestItem.dataset.taskId);
|
||||||
|
if (taskId !== selectedTaskId) {
|
||||||
|
selectTask(taskId);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLandscapeMode() {
|
||||||
|
return window.innerWidth > window.innerHeight && window.innerWidth >= 1024;
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectTask(taskId) {
|
function selectTask(taskId) {
|
||||||
selectedTaskId = taskId;
|
selectedTaskId = taskId;
|
||||||
const task = [...tasks, ...finishedTasks].find(t => t.id === taskId);
|
const task = tasks.find(t => t.id === taskId);
|
||||||
|
|
||||||
if (!task) return;
|
if (!task) return;
|
||||||
|
|
||||||
@ -178,10 +174,14 @@ function selectTask(taskId) {
|
|||||||
document.getElementById("edit-task-status").value = task.status;
|
document.getElementById("edit-task-status").value = task.status;
|
||||||
document.getElementById("side-panel-error").textContent = "";
|
document.getElementById("side-panel-error").textContent = "";
|
||||||
|
|
||||||
document.getElementById("side-panel").classList.add("active");
|
const sidePanel = document.getElementById("side-panel");
|
||||||
|
if (!sidePanel.classList.contains("active")) {
|
||||||
|
sidePanel.classList.add("active");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeSidePanel() {
|
function closeSidePanel() {
|
||||||
|
if (isLandscapeMode()) return;
|
||||||
document.getElementById("side-panel").classList.remove("active");
|
document.getElementById("side-panel").classList.remove("active");
|
||||||
selectedTaskId = null;
|
selectedTaskId = null;
|
||||||
}
|
}
|
||||||
@ -204,7 +204,7 @@ async function saveTask() {
|
|||||||
try {
|
try {
|
||||||
await put(`/api/tasks/${selectedTaskId}`, { title, desc });
|
await put(`/api/tasks/${selectedTaskId}`, { title, desc });
|
||||||
|
|
||||||
const currentTask = [...tasks, ...finishedTasks].find(t => t.id === selectedTaskId);
|
const currentTask = tasks.find(t => t.id === selectedTaskId);
|
||||||
if (status !== currentTask?.status) {
|
if (status !== currentTask?.status) {
|
||||||
await patch(`/api/tasks/${selectedTaskId}/status`, { status });
|
await patch(`/api/tasks/${selectedTaskId}/status`, { status });
|
||||||
}
|
}
|
||||||
@ -282,6 +282,25 @@ function formatTime(isoString) {
|
|||||||
return date.toLocaleString();
|
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.addEventListener("DOMContentLoaded", () => {
|
||||||
document.getElementById("goal-selector").addEventListener("change", (e) => {
|
document.getElementById("goal-selector").addEventListener("change", (e) => {
|
||||||
selectedGoalId = parseInt(e.target.value);
|
selectedGoalId = parseInt(e.target.value);
|
||||||
@ -297,5 +316,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
document.getElementById("save-task-btn").addEventListener("click", saveTask);
|
document.getElementById("save-task-btn").addEventListener("click", saveTask);
|
||||||
document.getElementById("delete-task-btn").addEventListener("click", deleteTask);
|
document.getElementById("delete-task-btn").addEventListener("click", deleteTask);
|
||||||
|
|
||||||
|
initWheelScroll();
|
||||||
loadGoals();
|
loadGoals();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -16,46 +16,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="tasks-main">
|
||||||
<div class="tasks-container">
|
<div class="tasks-container">
|
||||||
<div id="scroll-view" class="scroll-view">
|
<div id="scroll-view" class="scroll-view">
|
||||||
<div id="tasks-list" class="tasks-list"></div>
|
<div id="tasks-list" class="tasks-list"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="finished-section" class="finished-section">
|
|
||||||
<h2>Completed Tasks</h2>
|
|
||||||
<div id="finished-list" class="finished-list"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="create-task-btn" class="btn-success">+ Create Task</button>
|
<div id="side-panel" class="side-panel">
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="task-modal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2 id="task-modal-title">Create Task</h2>
|
|
||||||
<button class="modal-close" id="task-modal-close">×</button>
|
|
||||||
</div>
|
|
||||||
<form id="task-form">
|
|
||||||
<input type="hidden" id="task-id">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="task-title">Task Title</label>
|
|
||||||
<input type="text" id="task-title" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="task-desc">Description</label>
|
|
||||||
<textarea id="task-desc" rows="3"></textarea>
|
|
||||||
</div>
|
|
||||||
<div id="task-error" class="error-message"></div>
|
|
||||||
<div class="modal-actions">
|
|
||||||
<button type="submit" class="btn-primary">Save</button>
|
|
||||||
<button type="button" class="btn-secondary" id="task-modal-cancel">Cancel</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="side-panel" class="side-panel">
|
|
||||||
<div class="side-panel-header">
|
<div class="side-panel-header">
|
||||||
<h2>Edit Task</h2>
|
<h2>Edit Task</h2>
|
||||||
<button class="side-panel-close" id="side-panel-close">×</button>
|
<button class="side-panel-close" id="side-panel-close">×</button>
|
||||||
@ -84,6 +52,37 @@
|
|||||||
<button class="btn-danger" id="delete-task-btn">Delete</button>
|
<button class="btn-danger" id="delete-task-btn">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="create-task-container">
|
||||||
|
<button id="create-task-btn" class="btn-success">+ Create Task</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="task-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 id="task-modal-title">Create Task</h2>
|
||||||
|
<button class="modal-close" id="task-modal-close">×</button>
|
||||||
|
</div>
|
||||||
|
<form id="task-form">
|
||||||
|
<input type="hidden" id="task-id">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="task-title">Task Title</label>
|
||||||
|
<input type="text" id="task-title" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="task-desc">Description</label>
|
||||||
|
<textarea id="task-desc" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
<div id="task-error" class="error-message"></div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button type="submit" class="btn-primary">Save</button>
|
||||||
|
<button type="button" class="btn-secondary" id="task-modal-cancel">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user