diff --git a/public/css/index.css b/public/css/index.css index 1d82d7159c154749dc311c4d00b434e977d4095d..88bad6b4d2c8855a41462601252ea1f816793982 100644 --- a/public/css/index.css +++ b/public/css/index.css @@ -26,16 +26,29 @@ h1 { margin: 0; } #logout-btn { - background-color: transparent; + background-color: rgb(170, 17, 17); border: 1px solid #333; - color: #333; + color: white; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; transition: all 0.3s ease; } #logout-btn:hover { - background-color: #333; + background-color: #852828; + color: #fff; +} +#toggle-completed-btn{ + background-color: rgb(42, 50, 170); + border: 1px solid #333; + color: white; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + transition: all 0.3s ease; +} +#toggle-completed-btn:hover{ + background-color: #1c277e; color: #fff; } #new-task-form { @@ -71,8 +84,8 @@ button[type="submit"]:hover { margin-bottom: 1rem; padding: 1rem; display: flex; - justify-content: space-between; - align-items: center; + flex-direction: column; + align-items: flex-start; transition: all 0.3s ease; } .task-list li:hover { @@ -85,7 +98,13 @@ button[type="submit"]:hover { .task-list li.completed span { text-decoration: line-through; } +.task-text { + white-space: normal; + word-wrap: break-word; + width: 100%; +} .task-actions { + margin-left: auto; display: flex; gap: 0.5rem; } diff --git a/public/index.html b/public/index.html index 97a91fcdae16c5915876c18c9f3a4e362e7e6cf3..b0d935f2fe8543a54f49729a3e2a4652d2d3b808 100644 --- a/public/index.html +++ b/public/index.html @@ -13,6 +13,7 @@ <header> <h1>ToDo List</h1> <button id="logout-btn">Logout</button> + <button id="toggle-completed-btn">Show Completed Tasks</button> </header> <main> diff --git a/public/js/app.js b/public/js/app.js index c5cdc88350aa239f4a0dcc8421a8993e2168fc8c..7b7c05dc9bab2c2057150e89de8dbf9521fdcda1 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -14,4 +14,163 @@ document.getElementById('logout-btn').addEventListener('click', async () => { } catch (err) { console.error('Error during logout:', err); } -}); \ No newline at end of file +}); + + +// Function to check if the currently processed task belongs to user account +async function isValidTask(taskId) { + try { + const res = await fetch("/api/todos"); + const todos = await res.json(); + let valid = false; + + todos.forEach(todo => { + if (todo._id == taskId) + valid = true; + }); + return valid; + } catch (err) { + console.error("Error loading tasks:", err); + } +} + +// Load every task when the page is initialzed +document.addEventListener('DOMContentLoaded', async () => { + try { + const res = await fetch('/api/todos'); + const todos = await res.json(); + renderTasks(todos); + } catch (err) { + console.error('Error loading tasks:', err); + } +}); + +// Function to render tasks in the DOM +function renderTasks(todos) { + taskList.innerHTML = ''; + todos.forEach(todo => { + const taskItem = createTaskElement(todo); + taskList.appendChild(taskItem); + }); +} + +// Create an element in the DOM +function createTaskElement(todo) { + const li = document.createElement('li'); + li.className = 'task'; + li.dataset.id = todo._id; + if (todo.isDone) { + li.classList.add('completed'); + } + li.innerHTML = ` + <p>${todo.description}</p> + <div class="task-actions"> + <button class="hide-btn" data-id="${todo._id}">Done</button> + <button class="edit-btn" data-id="${todo._id}">Edit</button> + <button class="delete-btn" data-id="${todo._id}">Delete</button> + </div> + `; + + // Button Button assigns + li.querySelector('.hide-btn').addEventListener('click', () => markAsDone(todo._id, todo.isDone)); + li.querySelector('.delete-btn').addEventListener('click', () => deleteTask(todo._id)); + li.querySelector('.edit-btn').addEventListener('click', () => editTask(todo)); + + return li; +} + +// Add new task +newTaskForm.addEventListener('submit', async (e) => { + e.preventDefault(); + const input = newTaskForm.querySelector('input[name="task"]'); + const description = input.value.trim(); + + if (description === '') return; + + try { + const res = await fetch('/api/todos', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ description }) + }); + + const newTodo = await res.json(); + taskList.appendChild(createTaskElement(newTodo)); + input.value = ''; // Clear input field + } catch (err) { + console.error('Error adding task:', err); + } +}); + +// Mark task as completed +async function markAsDone(taskId, isDone) { + // Input sanitation + if (isNaN(parseInt(taskId, 10)) && typeof(isDone) === 'boolean' && isValidTask(taskId)) { + console.error('Invalid task ID, task status is invalid or task does not belong to account'); + return; + } + + try { + const res = await fetch(`/api/todos/${taskId}/done`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ isDone: !isDone }) + }); + + if (res.ok) { + const taskElement = document.querySelector(`li[data-id="${taskId}"]`); + taskElement.classList.toggle('completed'); + } + } catch (err) { + console.error('Error marking task as done:', err); + } +} + +// Erase task +async function deleteTask(taskId) { + // Input sanitation + if (isNaN(parseInt(taskId, 10))) { + console.error('Invalid task ID'); + return; + } + + try { + const res = await fetch(`/api/todos/${taskId}`, { + method: 'DELETE' + }); + + if (res.ok) { + const taskElement = document.querySelector(`li[data-id="${taskId}"]`); + taskElement.remove(); + } else { + console.error('Failed to delete task from server'); + } + } catch (err) { + console.error('Error deleting task:', err); + } +} + +// Edit task +async function updateTask(taskId, newDescription) { + // Input sanitation + if (isNaN(parseInt(taskId, 10)) && typeof(newDescription) === 'string' && isValidTask(taskId)) { + console.error('Invalid task ID, task status is invalid or task does not belong to account'); + return; + } + + try { + const res = await fetch(`/api/todos/${taskId}/description`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ description: newDescription }) + }); + + if (res.ok) { + const updatedTask = await res.json(); + const taskElement = document.querySelector(`li[data-id="${taskId}"]`); + taskElement.querySelector('p').textContent = updatedTask.todo.description; + } + } catch (err) { + console.error('Error updating task:', err); + } +} diff --git a/public/js/index.js b/public/js/index.js index 5d8064574ffe7b926ddbcceae836114e138a021f..fd999b880251debdf6368381abdeee28c89d30ee 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -1,17 +1,245 @@ document.addEventListener('DOMContentLoaded', async () => { - const logoutBtn = document.getElementById('logout-btn'); + const form = document.getElementById("new-task-form"); + const input = form.querySelector('input[name="task"]'); + const taskList = document.getElementById("task-list"); + const logoutBtn = document.getElementById('logout-btn'); + const toggleCompletedBtn = document.getElementById('toggle-completed-btn'); + + let showCompletedTasks = false; + + // Function to check if the currently processed task belongs to user account + async function isValidTask(taskId) { + try { + const res = await fetch("/api/todos"); + const todos = await res.json(); + let valid = false; + + todos.forEach(todo => { + if (todo._id == taskId) + valid = true; + }); + return valid; + } catch (err) { + console.error("Error loading tasks:", err); + } + } + + // Load the tasks when page is initiated + async function loadTasks() { + try { + const res = await fetch("/api/todos"); + const todos = await res.json(); + + // Verify if the response has tasks + if (todos.length > 0) { + renderTasks(todos); + } else { + console.log('No tasks to show.'); + } + } catch (err) { + console.error("Error loading tasks:", err); + } + } + + // Function to render tasks in the list + function renderTasks(todos) { + taskList.innerHTML = ''; + todos.forEach(todo => { + const taskItem = createTaskElement(todo); + taskList.appendChild(taskItem); + }); + + toggleCompletedVisibility(); + } + + // Function to create an element in DOM + function createTaskElement(todo) { + const li = document.createElement('li'); + li.dataset.id = todo._id; + if (todo.isDone) { + li.classList.add('completed'); + li.style.display = 'none'; + } + + li.innerHTML = ` + <span class="task-text">${todo.description}</span> + <input type="text" class="edit-input" value="${todo.description}" style="display:none;"> + <div class="task-actions"> + <button class="complete-btn" title="Mark as complete">✓</button> + <button class="edit-btn" title="Edit task">✎</button> + <button class="save-btn" title="Save changes" style="display:none;">💾</button> + <button class="delete-btn" title="Delete task">×</button> + </div> + `; + + // Button assigns + const completeBtn = li.querySelector(".complete-btn"); + completeBtn.addEventListener("click", async () => { + await toggleComplete(todo._id, !todo.isDone); + li.classList.toggle("completed"); + toggleCompletedVisibility(); + }); + + const deleteBtn = li.querySelector(".delete-btn"); + deleteBtn.addEventListener("click", async () => { + await deleteTask(todo._id); + li.remove(); + }); + + const editBtn = li.querySelector(".edit-btn"); + const saveBtn = li.querySelector(".save-btn"); + const taskText = li.querySelector(".task-text"); + const editInput = li.querySelector(".edit-input"); + + editBtn.addEventListener("click", () => { + taskText.style.display = "none"; + editInput.style.display = "block"; + editInput.focus(); + saveBtn.style.display = "inline" + editBtn.style.display = "none"; + }); + + saveBtn.addEventListener("click", async () => { + const newDescription = editInput.value.trim(); + if (newDescription && newDescription !== taskText.textContent) { + await updateTask(todo._id, newDescription); + taskText.textContent = newDescription; + } + taskText.style.display = "block"; + editInput.style.display = "none"; + saveBtn.style.display = "none"; + editBtn.style.display = "inline"; + }); + + return li; + } + + // Function to toggle completed task visibility + function toggleCompletedVisibility() { + const completedTasks = document.querySelectorAll('.completed'); + completedTasks.forEach(task => { + if (showCompletedTasks) { + task.style.display = 'block'; + } else { + task.style.display = 'none'; + } + }); + } + + // Toggle button event listener + toggleCompletedBtn.addEventListener('click', () => { + showCompletedTasks = !showCompletedTasks; + toggleCompletedVisibility(); + toggleCompletedBtn.textContent = showCompletedTasks ? 'Hide Completed Tasks' : 'Show Completed Tasks'; + }); + + // Function to alternate to the completed state + async function toggleComplete(taskId, isDone) { + // Input sanitation + if (isNaN(parseInt(taskId, 10)) && typeof(isDone) === 'boolean' && isValidTask(taskId)) { + console.error('Invalid task ID, task status is invalid or task does not belong to account'); + return; + } + + try { + const res = await fetch(`/api/todos/${taskId}/done`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ isDone }) + }); + if (!res.ok) { + console.error('Error marking task as complete.'); + } + } catch (err) { + console.error('Error updating task:', err); + } + } + + // Function to eliminate a task + async function deleteTask(taskId) { + // Input sanitation + if (isNaN(parseInt(taskId, 10))) { + console.error('Invalid task ID'); + return; + } + + try { + const res = await fetch(`/api/todos/${taskId}`, { + method: 'DELETE' + }); + if (!res.ok) { + console.error('Error deleting task.'); + } + } catch (err) { + console.error('Error deleting task:', err); + } + } + + // Function to update the description of a task + async function updateTask(taskId, newDescription) { + // Input sanitation + if (isNaN(parseInt(taskId, 10)) && typeof(newDescription) === 'string' && isValidTask(taskId)) { + console.error('Invalid task ID, task status is invalid or task does not belong to account'); + return; + } - // Event for the logout button - logoutBtn.addEventListener('click', async () => { - try { - const res = await fetch('/api/users/logout', { method: 'POST' }); - if (res.ok) { - window.location.href = 'login.html'; - } else { - console.error('Logout failed'); - } - } catch (err) { - console.error('Error during logout:', err); + try { + const res = await fetch(`/api/todos/${taskId}/description`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ description: newDescription }) + }); + if (!res.ok) { + console.error('Error updating task.'); + } + } catch (err) { + console.error('Error updating task:', err); + } + } + + // Load the tasks in the beginning + loadTasks(); + + // Add new task + form.addEventListener("submit", async (e) => { + e.preventDefault(); + const description = input.value.trim(); + if (description) { + await addTask(description); + input.value = ""; + } + }); + + // Function to add new task + async function addTask(description) { + try { + const res = await fetch("/api/todos", { + method: "POST", + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ description }) + }); + if (res.ok) { + const newTodo = await res.json(); + taskList.appendChild(createTaskElement(newTodo)); + } else { + console.error('Error adding task.'); + } + } catch (err) { + console.error('Error adding task:', err); + } + } + + // Logout button event + logoutBtn.addEventListener('click', async () => { + try { + const res = await fetch('/api/users/logout', { method: 'POST' }); + if (res.ok) { + window.location.href = 'login.html'; + } else { + console.error('Logout failed'); } - }); -}); \ No newline at end of file + } catch (err) { + console.error('Error during logout:', err); + } + }); +}); diff --git a/routes/todos.js b/routes/todos.js index 8154cf9113a0a9ffa824114ea20067481d3df3eb..f853d478a09a9afa8ae8f76464bc82c367f6989f 100644 --- a/routes/todos.js +++ b/routes/todos.js @@ -11,7 +11,7 @@ function isAuthenticated(req, res, next) { } } - +// Route to logout router.post('/logout', (req, res) => { req.session.destroy((err) => { if (err) { @@ -22,4 +22,89 @@ router.post('/logout', (req, res) => { }); }); +// Route to obtain all tasks of authenticated user +router.get('/', isAuthenticated, async (req, res) => { + try { + const todos = await Todo.find({ userId: req.session.user.id }); + res.json(todos); + } catch (error) { + console.error(error); + res.status(500).json({ error: 'Server error' }); + } +}); + +// Route to add a task +router.post('/', isAuthenticated, async (req, res) => { + const { description } = req.body; + + try { + const todo = new Todo({ description, userId: req.session.user.id }); + await todo.save(); + res.json(todo); + } catch (error) { + console.error(error); + res.status(500).json({ error: 'Server error' }); + } +}); + + +// Route to eliminate tasks +router.delete('/:id', isAuthenticated, async (req, res) => { + try { + const todo = await Todo.findById(req.params.id); + + if (!todo || todo.userId.toString() !== req.session.user.id) { + return res.status(404).json({ error: 'Todo not found or unauthorized' }); + } + + await todo.deleteOne(); + res.json({ success: true }); + } catch (error) { + console.error(error); + res.status(500).json({ error: 'Server error' }); + } +}); + +// Route to update the description of a task +router.put('/:id/description', isAuthenticated, async (req, res) => { + const { description } = req.body; + + try { + const todo = await Todo.findById(req.params.id); + + if (!todo || todo.userId.toString() !== req.session.user.id) { + return res.status(404).json({ error: 'Todo not found or unauthorized' }); + } + + todo.description = description; + await todo.save(); + + res.json({ todo }); + } catch (error) { + console.error(error); + res.status(500).json({ error: 'Server error' }); + } +}); + +// Route to mark a task as completed +router.put('/:id/done', isAuthenticated, async (req, res) => { + const { isDone } = req.body; + + try { + const todo = await Todo.findById(req.params.id); + + if (!todo || todo.userId.toString() !== req.session.user.id) { + return res.status(404).json({ error: 'Todo not found or unauthorized' }); + } + + todo.isDone = isDone; + await todo.save(); + + res.json({ todo }); + } catch (error) { + console.error(error); + res.status(500).json({ error: 'Server error' }); + } +}); + module.exports = router; \ No newline at end of file diff --git a/routes/users.js b/routes/users.js index 2a5feb7bc706c6995e34fdba2847b99c503fdf68..21dc65368419a46f63353058e99f126ce20e6ead 100644 --- a/routes/users.js +++ b/routes/users.js @@ -31,25 +31,21 @@ router.post('/register', async (req, res) => { // LogIn User router.post('/login', async (req, res) => { const { username, password } = req.body; - console.log('Attempting to log in:', username); try { const user = await User.findOne({ username }); - console.log('User found:', user); if (!user) { return res.status(400).json({ error: 'Invalid username or password' }); } const isMatch = await bcrypt.compare(password, user.password); - console.log('Password match:', isMatch); if (!isMatch) { return res.status(400).json({ error: 'Invalid username or password' }); } req.session.user = { id: user._id, username: user.username }; - console.log('User logged in:', req.session.user); res.json({ success: true, message: 'Login successful' }); } catch (error) {