commit 452bd8ad04cc3c922b63366ccc2b673712a051de Author: Akshay Vadher Date: Thu Aug 6 09:34:37 2026 +0530 commited\ diff --git a/package.json b/package.json new file mode 100644 index 0000000..c7206b9 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "todo", + "version": "1.0.0", + "description": "Simple TODO app backend", + "license": "ISC", + "author": "akshay", + "type": "commonjs", + "main": "server.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node server.js" + }, + "dependencies": { + "express": "latest" + } +} diff --git a/public/app.js b/public/app.js new file mode 100644 index 0000000..154326f --- /dev/null +++ b/public/app.js @@ -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, ">"); +} +function escapeAttr(str) { + return escapeHtml(str).replace(/"/g, """); +} + +function renderTodoCard(todo) { + if (editingId === todo.id) { + return ` +
+
+ + +
+
+ + +
+
`; + } + + return ` +
+
+ ${todo.title ? `
${todo.title}
` : ""} +

${escapeHtml(todo.text || "")}

+ ${todo.date} - ${todo.status}
+
+ + + + + +
+
`; +} +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 = ` +

Today

+

Total Today: ${todayTodos.length}

+

Completed Today: ${completedToday}

+

pending+delayed Today: ${pend_delay}

+

Pending Today: ${pendingToday}

+

Delayed Today: ${delayedToday}

+
+

Overall

+

All Total: ${todos.length}

+

All Completed: ${allCompleted}

+

All pending+delayed: ${all_pend_delay}

+

All Pending: ${allPending}

+

All Delayed: ${allDelayed}

+ `; +} +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 +}); diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..10f3050 --- /dev/null +++ b/public/index.html @@ -0,0 +1,65 @@ + + + + + + Professional TODO App + + + + + + +
+
+ + + +
+
+
+ + +
+ +
+
+
+ + + + + \ No newline at end of file diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000..f33dd06 --- /dev/null +++ b/public/styles.css @@ -0,0 +1,604 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: "Segoe UI", Arial, sans-serif; + background: #f9f9f9; + color: #333; + display: flex; + min-height: 100vh; +} + +.headbar { + width: 100%; + background: #0078d7; + color: #fff; + padding: 15px 25px; + border-bottom: 2px solid #005a9e; +} + +.headbar h1 { + margin: 0; + font-size: 22px; + font-weight: 600; +} + +.headbar p { + margin: 4px 0 0; + font-size: 14px; + opacity: 0.9; +} + +.sidebar { + width: 220px; + background: #f0f0f0; + padding: 20px; + border-right: 1px solid #ddd; + transition: width 0.3s ease, padding 0.3s ease; +} + +.sidebar button { + display: flex; + align-items: center; + width: 100%; + margin-bottom: 10px; + padding: 10px; + border: none; + background: #0078d7; + color: #fff; + font-weight: 500; + border-radius: 4px; + cursor: pointer; + transition: background 0.3s ease; +} + +.sidebar button .icon { + margin-right: 8px; +} + +.sidebar button:hover { + background: #005a9e; +} + +#analytics { + margin-top: 20px; + font-size: 14px; + color: #555; +} + +.sidebar.collapsed { + width: 60px; + padding: 10px; +} + +.sidebar.collapsed .label, +.sidebar.collapsed #analytics { + display: none; +} + +.sidebar.collapsed button { + justify-content: center; + font-size: 18px; + padding: 8px; +} + +.main { + flex: 1; + padding: 30px; + padding-top: 15px; +} + +/* form#todoForm { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 20px; +} */ + +form#todoForm input, +form#todoForm textarea, +form#todoForm button, +form#todoForm label { + /* padding: 8px; */ + font-size: 14px; +} + +form#todoForm input[type="text"], +form#todoForm input[type="date"], +form#todoForm textarea { + flex: 1; + border: 1px solid #ccc; + border-radius: 4px; +} + + +form#todoForm button { + background: #28a745; + color: #fff; + border: none; + border-radius: 4px; + font-weight: 600; + cursor: pointer; + transition: background 0.3s ease; +} + +form#todoForm button:hover { + background: #1e7e34; +} + +.todo { + display: flex; + justify-content: space-between; + align-items: flex-start; + border: 1px solid #ddd; + border-radius: 6px; + margin: 6px 0; + padding: 3px 8px; + background: #fff; + transition: transform 0.2s ease; + max-width: 100%; + overflow-x: hidden; + word-wrap: break-word; + overflow-wrap: break-word; +} + +.todo:hover { + transform: translateY(-2px); +} + +.todo-content { + flex: 1; +} + +.todo-content p { + margin: 4px 0 2px; + font-size: 13px; + color: #333; + white-space: normal; + word-wrap: break-word; + overflow-wrap: break-word; +} + +.todo-content small { + font-size: 12px; + color: #666; +} + +.todo.pending { + border-left: 4px solid #ffc107; +} + +.todo.completed { + border-left: 4px solid #28a745; + opacity: 0.8; +} + +.todo.delayed { + border-left: 4px solid #dc3545; +} + +.todo.blur { + filter: blur(4px); +} + + +.todo-actions { + display: grid; + gap: 4px; + grid-template-columns: repeat(3, 1fr); + margin-top: 8px; +} + +.todo-actions button { + padding: 3px 6px; + font-size: 0.75rem; + border: none; + border-radius: 5px; + cursor: pointer; + background: #2a2a2a; + color: #eee; + transition: background 0.15s ease; + white-space: nowrap; +} + +.todo-actions button:nth-child(1) { + background: #ffc107; + color: #000; +} + +.todo-actions button:nth-child(2) { + background: #28a745; + color: #fff; +} + +.todo-actions button:nth-child(3) { + background: #dc3545; + color: #fff; +} + +.todo-actions button:nth-child(4) { + background: #6c757d; + color: #fff; +} + +.todo-actions button:nth-child(5) { + background: #0078d7; + color: #fff; +} + +.dark { + background: #1e1e1e; + color: #e0e0e0; +} + +.dark .sidebar { + background: #2c2c2c; + border-right: 1px solid #444; +} + +.dark .sidebar button { + background: #444; + color: #fff; +} + +.dark .sidebar button:hover { + background: #666; +} + +.dark .main { + background: #1e1e1e; +} + +.dark .todo { + background: #2c2c2c; + border: 1px solid #444; +} + +.dark .todo h5 { + color: #119272; +} + +.sidebar.collapsed .label { + display: none; +} + +.sidebar.collapsed #analytics { + display: none; +} + +.todo-content h3 { + margin: 0 0 4px; + font-size: 15px; + font-weight: 600; + color: #0078d7; +} + +.dark .todo-content h5 { + color: #349ca3; +} + +.dark .todo-content p { + color: #e0e0e0; +} + +.dark .todo-content small { + color: #aaa; +} + +.dark .todo.pending { + border-left: 4px solid #ffc107; +} + +.dark .todo.completed { + border-left: 4px solid #28a745; + + opacity: 0.9; +} + +.dark .todo.delayed { + border-left: 4px solid #dc3545; +} + +.todo-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.todo-header h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: #0078d7; +} + +.todo-header small { + font-size: 12px; + color: #666; + margin-left: 10px; +} + +.dark .todo-header h3 { + color: #4da3ff; +} + +.dark .todo-header small { + color: #aaa; +} + +#statusFilter { + /* background: #0078d7; */ + margin-top: 10px; + width: 100%; + padding: 6px; + border-radius: 4px; + border: 1px solid #ccc; + background: #444; + color: #fff; + +} + + +#todoText { + flex: 1; + min-height: 80px; + border: 1px solid #ccc; + border-radius: 4px; + padding: 8px; + font-size: 14px; + resize: vertical; + white-space: pre-wrap; + word-wrap: break-word; + overflow-wrap: break-word; +} + +.dark input[type="text"], +.dark textarea { + background-color: #222; + color: #e0e0e0; + border: 1px solid #555; +} + +.dark input[type="text"]::placeholder, +.dark textarea::placeholder { + color: #888; +} + +#analytics hr { + margin: 10px 0; + border: none; + border-top: 1px solid #ccc; +} + +.dark #analytics hr { + border-top: 1px solid #444; +} + +#analytics p { + margin: 2px 0; +} + +.todo-actions button:hover { + background: #3a3a3a; +} + +.todo.editing { + background: #1e1e1e; + border: 1px solid #444; + border-radius: 10px; + padding: 12px; +} + +.todo.editing .todo-content { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 10px; +} + +.todo.editing input, +.todo.editing textarea { + width: 100%; + box-sizing: border-box; + background: #2a2a2a; + color: #eee; + border: 1px solid #444; + border-radius: 6px; + padding: 8px 10px; + font-family: inherit; + font-size: 0.95rem; +} + +.todo.editing input:focus, +.todo.editing textarea:focus { + outline: none; + border-color: #666; +} + +.todo.editing textarea { + min-height: 70px; + resize: vertical; +} + +.todo.editing .todo-actions { + grid-template-columns: repeat(2, 1fr); +} + +.todo.editing .todo-actions button:first-child { + background: #2d6a4f; +} + +.todo.editing .todo-actions button:first-child:hover { + background: #37845f; +} + +.todo.editing .todo-actions button:last-child { + background: #6a2d2d; +} + +.todo.editing .todo-actions button:last-child:hover { + background: #843737; +} + +#todoForm { + display: flex; + flex-direction: column; + gap: 4px; + background: #1e1e1e; + border: 1px solid #444; + border-radius: 6px; + padding: 5px; + margin-bottom: 8px; +} + +#todoForm input, +#todoForm textarea { + width: 100%; + box-sizing: border-box; + background: #2a2a2a; + color: #eee; + border: 1px solid #444; + border-radius: 4px; + padding: 3px 6px; + font-family: inherit; + font-size: 0.75rem; + display: block; + align-self: stretch; +} + +#todoForm input:focus, +#todoForm textarea:focus { + outline: none; + border-color: #666; +} + +#todoForm textarea { + min-height: 26px; + resize: vertical; + max-height: 50px; + line-height: 1.2; + text-align: left; + vertical-align: top; + display: block; + align-self: flex-start; + width: 100%; +} + +#todoForm button { + align-self: flex-start; + padding: 3px 10px; + font-size: 0.7rem; + border: none; + border-radius: 4px; + background: #2d6a4f; + color: #eee; + cursor: pointer; + transition: background 0.15s ease; + flex-shrink: 0; +} + +#todoForm button:hover { + background: #37845f; +} + +.todoForm-row { + display: flex; + gap: 4px; +} + +#todoTitle { + flex: 1; + min-width: 0; +} + +.todo.blur .todo-content, +.todo.blur .todo-content p:hover { + filter: none; +} + +.search-row { + display: flex; + gap: 4px; + margin-bottom: 8px; +} + +.search-row input { + flex: 1; + min-width: 0; + box-sizing: border-box; + background: #2a2a2a; + color: #eee; + border: 1px solid #444; + border-radius: 4px; + padding: 3px 6px; + font-family: inherit; + font-size: 0.75rem; +} + +.search-row input:focus { + outline: none; + border-color: #666; +} + +.search-row input::placeholder { + color: #888; +} + +.status-filters { + display: flex; + flex-direction: column; + /* stack vertically in sidebar */ + gap: 8px; + margin-top: 10px; +} + +.status-filters button { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 10px; + font-size: 14px; + border: none; + border-radius: 4px; + cursor: pointer; + background: #444; + color: #fff; + transition: background 0.2s; +} + +.status-filters button:hover { + background: #666; +} + +.status-filters .icon { + font-size: 16px; +} + +.todo.blur:hover { + filter: none +} + +form#todoForm textarea { + min-height: 50px; + resize: vertical; +} + +.todo-content p { + white-space: pre-wrap; + word-wrap: break-word; +} +.todo-content.blur { + filter: blur(5px); + user-select: auto + /* transition: filter 0.1s ease; */ +} + +.todo-content.blur:hover { + filter: blur(0); +} +.blur { + filter: blur(4px); +} \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 0000000..38b6573 --- /dev/null +++ b/server.js @@ -0,0 +1,93 @@ +const express = require("express"); +const fs = require("fs"); +const path = require("path"); + +const app = express(); +const PORT = 3005; +const DATA_FILE = path.join(__dirname, "todos.json"); + +app.use(express.json()); +app.use(express.static("public")); + +// Load todos +// Load todos +function loadTodos() { + if (!fs.existsSync(DATA_FILE)) { + // ✅ Create file with empty array if missing + fs.writeFileSync(DATA_FILE, "[]", "utf-8"); + return []; + } + + const data = fs.readFileSync(DATA_FILE, "utf-8"); + if (!data.trim()) return []; // ✅ handle empty file + try { + return JSON.parse(data); + } catch (err) { + console.error("Error parsing todos.json:", err); + return []; // fallback + } +} + +// Delete todo +app.delete("/api/todos/:id", (req, res) => { + let todos = loadTodos(); + todos = todos.filter((todo) => todo.id != req.params.id); + saveTodos(todos); + res.json({ success: true }); +}); + +// Save todos +function saveTodos(todos) { + fs.writeFileSync(DATA_FILE, JSON.stringify(todos, null, 2)); +} + +// Get all todos +app.get("/api/todos", (req, res) => { + res.json(loadTodos()); +}); + +function formatDateTime(isoString) { + const d = new Date(isoString); + const date = d.toLocaleDateString(); // e.g. 8/4/2026 + const time = d.toLocaleTimeString("en-US", { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: true, + }); // e.g. 05:30:33 PM + return `${date} ${time}`; +} + +// Add new todo +app.post("/api/todos", (req, res) => { + const todos = loadTodos(); + const now = new Date(); + + const newTodo = { + id: Date.now(), + text: req.body.text || "", + // ✅ use formatted date+time + date: req.body.date ? formatDateTime(req.body.date) : formatDateTime(now), + title: req.body.title || "", + status: "pending", + private: req.body.private || false, + }; + + todos.push(newTodo); + saveTodos(todos); + res.json(newTodo); +}); + +// Update todo (status, privacy) +app.put("/api/todos/:id", (req, res) => { + let todos = loadTodos(); + todos = todos.map((todo) => + todo.id == req.params.id ? { ...todo, ...req.body } : todo, + ); + saveTodos(todos); + res.json({ success: true }); +}); + +app.listen(PORT, () => + console.log(`Server running at http://localhost:${PORT}`), +);