251 lines
7.7 KiB
JavaScript
251 lines
7.7 KiB
JavaScript
let globalBlur = false;
|
|
let currentDateFilter = "all"; // 'all' | 'today'
|
|
let currentStatusFilter = "all"; // 'all' | 'pending' | 'completed' | 'delayed' | 'pend_delay'
|
|
let editingId = null;
|
|
|
|
async function fetchTodos() {
|
|
const res = await fetch("/api/todos");
|
|
return res.json();
|
|
}
|
|
let searchTitle = "";
|
|
let searchDescription = "";
|
|
let searchBoth = "";
|
|
function autoResize(el) {
|
|
el.style.height = "auto";
|
|
el.style.height = el.scrollHeight + "px";
|
|
}
|
|
|
|
document.getElementById("todoText").addEventListener("input", (e) => {
|
|
autoResize(e.target);
|
|
});
|
|
document.getElementById("searchTitle").addEventListener("input", (e) => {
|
|
searchTitle = e.target.value.toLowerCase();
|
|
renderTodos();
|
|
});
|
|
|
|
document.getElementById("searchDescription").addEventListener("input", (e) => {
|
|
searchDescription = e.target.value.toLowerCase();
|
|
renderTodos();
|
|
});
|
|
|
|
document.getElementById("searchBoth").addEventListener("input", (e) => {
|
|
searchBoth = e.target.value.toLowerCase();
|
|
renderTodos();
|
|
});
|
|
function toggleSidebar() {
|
|
const sidebar = document.getElementById("sidebar");
|
|
sidebar.classList.toggle("collapsed");
|
|
|
|
const icon = document.getElementById("collapseIcon");
|
|
icon.textContent = sidebar.classList.contains("collapsed") ? "▶" : "◀";
|
|
}
|
|
|
|
function toggleDarkMode() {
|
|
document.body.classList.toggle("dark");
|
|
}
|
|
|
|
// Sidebar "Today / All" buttons
|
|
function filterTodos(type) {
|
|
currentDateFilter = type;
|
|
renderTodos();
|
|
}
|
|
|
|
// Status dropdown
|
|
function filterByStatus(status) {
|
|
currentStatusFilter = status;
|
|
renderTodos();
|
|
}
|
|
|
|
async function toggleGlobalBlur() {
|
|
globalBlur = !globalBlur;
|
|
const todos = await fetchTodos();
|
|
// Wait for ALL updates to actually finish before re-rendering
|
|
await Promise.all(
|
|
todos.map((todo) =>
|
|
updateTodo(todo.id, { private: globalBlur }, { render: false }),
|
|
),
|
|
);
|
|
renderTodos();
|
|
}
|
|
|
|
async function deleteTodo(id) {
|
|
await fetch(`/api/todos/${id}`, { method: "DELETE" });
|
|
renderTodos();
|
|
}
|
|
|
|
async function updateTodo(id, data, { render = true } = {}) {
|
|
await fetch(`/api/todos/${id}`, {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(data),
|
|
});
|
|
if (render) await renderTodos();
|
|
}
|
|
|
|
document.getElementById("todoForm").addEventListener("submit", async (e) => {
|
|
e.preventDefault();
|
|
const today = new Date().toISOString().split("T")[0];
|
|
const newTodo = {
|
|
title: document.getElementById("todoTitle").value || "",
|
|
text: document.getElementById("todoText").value,
|
|
date: today,
|
|
status: "pending",
|
|
private: globalBlur,
|
|
};
|
|
await fetch("/api/todos", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(newTodo),
|
|
});
|
|
e.target.reset();
|
|
document.getElementById("todoText").style.height = "auto"; // 👈 add this
|
|
renderTodos();
|
|
document.getElementById("todoText").focus();
|
|
});
|
|
function startEdit(id) {
|
|
editingId = id;
|
|
renderTodos();
|
|
}
|
|
|
|
function cancelEdit() {
|
|
editingId = null;
|
|
renderTodos();
|
|
}
|
|
|
|
async function saveEdit(id) {
|
|
const title = document.getElementById(`editTitle-${id}`).value;
|
|
const text = document.getElementById(`editText-${id}`).value;
|
|
editingId = null;
|
|
await updateTodo(id, { title, text });
|
|
}
|
|
|
|
// Prevent broken HTML if the todo text/title contains quotes, <, etc.
|
|
function escapeHtml(str) {
|
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
}
|
|
function escapeAttr(str) {
|
|
return escapeHtml(str).replace(/"/g, """);
|
|
}
|
|
|
|
function renderTodoCard(todo) {
|
|
if (editingId === todo.id) {
|
|
return `
|
|
<div class="todo ${todo.status} editing">
|
|
<div class="todo-content">
|
|
<input type="text" id="editTitle-${todo.id}" value="${escapeAttr(todo.title || "")}" placeholder="Title">
|
|
<textarea id="editText-${todo.id}">${escapeHtml(todo.text || "")}</textarea>
|
|
</div>
|
|
<div class="todo-actions">
|
|
<button onclick="saveEdit(${todo.id})">Save</button>
|
|
<button onclick="cancelEdit()">Cancel</button>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
return `
|
|
<div class="todo ${todo.status}">
|
|
<div class="todo-content ${todo.private ? "blur" : ""}">
|
|
${todo.title ? `<h5>${todo.title}</h5>` : ""}
|
|
<p class="todo-description">${escapeHtml(todo.text || "")}</p>
|
|
<small>${todo.date} - ${todo.status}</small> </div>
|
|
<div class="todo-actions">
|
|
<button onclick="updateTodo(${todo.id}, {status:'pending'})">Pending</button>
|
|
<button onclick="updateTodo(${todo.id}, {status:'completed'})">Done</button>
|
|
<button onclick="updateTodo(${todo.id}, {status:'delayed'})">Urgent</button>
|
|
<button onclick="startEdit(${todo.id})">Edit</button>
|
|
<button onclick="deleteTodo(${todo.id})">Delete</button>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
document.getElementById("todoText").addEventListener("keydown", (e) => {
|
|
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
|
|
e.preventDefault();
|
|
document.getElementById("todoForm").requestSubmit();
|
|
}
|
|
});
|
|
async function renderTodos() {
|
|
const todos = await fetchTodos();
|
|
const today = new Date().toISOString().split("T")[0];
|
|
|
|
let filtered = todos;
|
|
if (currentDateFilter === "today") {
|
|
filtered = filtered.filter((t) => t.date === today);
|
|
}
|
|
if (currentStatusFilter !== "all") {
|
|
if (currentStatusFilter === "pend_delay") {
|
|
filtered = filtered.filter((t) => t.status != "completed");
|
|
} else {
|
|
filtered = filtered.filter((t) => t.status === currentStatusFilter);
|
|
}
|
|
}
|
|
if (searchTitle) {
|
|
filtered = filtered.filter((t) =>
|
|
(t.title || "").toLowerCase().includes(searchTitle),
|
|
);
|
|
}
|
|
if (searchDescription) {
|
|
filtered = filtered.filter((t) =>
|
|
(t.text || "").toLowerCase().includes(searchDescription),
|
|
);
|
|
}
|
|
if (searchBoth) {
|
|
filtered = filtered.filter(
|
|
(t) =>
|
|
(t.title || "").toLowerCase().includes(searchBoth) ||
|
|
(t.text || "").toLowerCase().includes(searchBoth),
|
|
);
|
|
}
|
|
|
|
filtered = filtered.slice().reverse(); // newest first
|
|
|
|
document.getElementById("todoList").innerHTML = filtered
|
|
.map(renderTodoCard)
|
|
.join("");
|
|
updateAnalytics(todos);
|
|
}
|
|
|
|
function updateAnalytics(todos) {
|
|
const today = new Date().toISOString().split("T")[0];
|
|
const todayTodos = todos.filter((t) => t.date === today);
|
|
|
|
const completedToday = todayTodos.filter(
|
|
(t) => t.status === "completed",
|
|
).length;
|
|
const pendingToday = todayTodos.filter((t) => t.status === "pending").length;
|
|
const delayedToday = todayTodos.filter((t) => t.status === "delayed").length;
|
|
const pend_delay = todayTodos.filter((t) => t.status != "completed").length;
|
|
|
|
const allCompleted = todos.filter((t) => t.status === "completed").length;
|
|
const allPending = todos.filter((t) => t.status === "pending").length;
|
|
const allDelayed = todos.filter((t) => t.status === "delayed").length;
|
|
const all_pend_delay = todos.filter((t) => t.status != "completed").length;
|
|
|
|
document.getElementById("analytics").innerHTML = `
|
|
<p><strong>Today</strong></p>
|
|
<p>Total Today: ${todayTodos.length}</p>
|
|
<p>Completed Today: ${completedToday}</p>
|
|
<p>pending+delayed Today: ${pend_delay}</p>
|
|
<p>Pending Today: ${pendingToday}</p>
|
|
<p>Delayed Today: ${delayedToday}</p>
|
|
<hr>
|
|
<p><strong>Overall</strong></p>
|
|
<p>All Total: ${todos.length}</p>
|
|
<p>All Completed: ${allCompleted}</p>
|
|
<p>All pending+delayed: ${all_pend_delay}</p>
|
|
<p>All Pending: ${allPending}</p>
|
|
<p>All Delayed: ${allDelayed}</p>
|
|
`;
|
|
}
|
|
renderTodos();
|
|
|
|
window.addEventListener("DOMContentLoaded", async () => {
|
|
// ✅ add async
|
|
document.getElementById("sidebar").classList.add("collapsed");
|
|
document.body.classList.add("dark");
|
|
document.getElementById("todoText").focus();
|
|
|
|
const todos = await fetchTodos();
|
|
globalBlur = todos.length > 0 && todos.every((t) => t.private);
|
|
renderTodos(); // also make sure the UI actually reflects restored state
|
|
});
|