commited\
This commit is contained in:
+250
@@ -0,0 +1,250 @@
|
||||
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
|
||||
});
|
||||
Reference in New Issue
Block a user