Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 88 additions & 1 deletion frontend/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ async function fetchTasks() {
function addTaskToDOM(task) {
const li = document.createElement('li');
li.innerHTML = `
<span class="${task.completed ? 'completed' : ''}">${task.title}</span>
<span class="${task.completed ? 'completed' : ''}" data-task-id="${task.id}">${task.title}</span>
<div>
<button onclick="editTask('${task.id}')" title="Edit task">✎</button>
<button onclick="toggleComplete('${task.id}', ${!task.completed})">✓</button>
<button onclick="deleteTask('${task.id}')">✕</button>
Comment on lines +42 to 44

Copilot AI Apr 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new edit button is icon-only (✎) and relies on title. For accessibility/screen readers, add an explicit accessible name (e.g., aria-label="Edit task") and consider doing the same for the other icon-only action buttons for consistency.

Suggested change
<button onclick="editTask('${task.id}')" title="Edit task"></button>
<button onclick="toggleComplete('${task.id}', ${!task.completed})"></button>
<button onclick="deleteTask('${task.id}')"></button>
<button onclick="editTask('${task.id}')" title="Edit task" aria-label="Edit task"></button>
<button onclick="toggleComplete('${task.id}', ${!task.completed})" aria-label="${task.completed ? 'Mark task as incomplete' : 'Mark task as complete'}"></button>
<button onclick="deleteTask('${task.id}')" aria-label="Delete task"></button>

Copilot uses AI. Check for mistakes.
</div>
Comment on lines 37 to 45

Copilot AI Apr 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addTaskToDOM builds the task row via innerHTML and interpolates task.title directly. Since titles come from user input and are served back by the API, this allows HTML/script injection (XSS). Prefer constructing DOM nodes and setting the title via textContent (or escaping) instead of injecting untrusted content into innerHTML.

Copilot uses AI. Check for mistakes.
Expand Down Expand Up @@ -73,4 +74,90 @@ async function deleteTask(id) {
fetchTasks();
}

function editTask(id) {
const span = document.querySelector(`span[data-task-id="${id}"]`);
if (!span) return;

const currentTitle = span.textContent;
const wasCompleted = span.classList.contains('completed');

// Create input field
const input = document.createElement('input');
input.type = 'text';
input.value = currentTitle;
input.className = 'edit-input';
input.setAttribute('data-task-id', id);

// Create save button
const saveBtn = document.createElement('button');
saveBtn.textContent = '✓';
saveBtn.className = 'save-btn';
saveBtn.title = 'Save changes';
saveBtn.onclick = () => saveEdit(id);

// Create cancel button
const cancelBtn = document.createElement('button');
cancelBtn.textContent = '✕';
cancelBtn.className = 'cancel-btn';
cancelBtn.title = 'Cancel editing';
cancelBtn.onclick = () => cancelEdit(id, currentTitle, wasCompleted);

// Replace span with input
span.replaceWith(input);

// Replace action buttons with save/cancel buttons
const buttonDiv = input.parentElement.querySelector('div');
buttonDiv.innerHTML = '';
buttonDiv.appendChild(saveBtn);
buttonDiv.appendChild(cancelBtn);

// Focus input and select text
input.focus();
input.select();

// Allow Enter key to save
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
Comment on lines +119 to +120

Copilot AI Apr 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Enter key handler uses the keypress event, which is deprecated/inconsistent across browsers for non-printable keys. Use a keydown handler for Enter (and consider calling e.preventDefault() if needed) to make keyboard saving reliable.

Suggested change
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();

Copilot uses AI. Check for mistakes.
saveEdit(id);
}
});

// Allow Escape key to cancel
input.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
cancelEdit(id, currentTitle, wasCompleted);
}
});
}

async function saveEdit(id) {
const input = document.querySelector(`input[data-task-id="${id}"]`);
if (!input) return;

const newTitle = input.value.trim();

// Validate that title is not empty
if (!newTitle) {
alert('El título de la tarea no puede estar vacío');
input.focus();
return;
}

try {
await fetch(`${API_URL}/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: newTitle })
});
Comment on lines +147 to +151

Copilot AI Apr 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

saveEdit does not check the HTTP response status. fetch only rejects on network errors, so non-2xx responses will still call fetchTasks() and make failures hard to notice. Capture the response, check res.ok (and optionally parse/display server error details) before refreshing the list.

Suggested change
await fetch(`${API_URL}/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: newTitle })
});
const res = await fetch(`${API_URL}/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: newTitle })
});
if (!res.ok) {
let errorMessage = 'Error al actualizar la tarea';
try {
const data = await res.json();
if (data && (data.message || data.error)) {
errorMessage = data.message || data.error;
}
} catch (parseError) {
// Ignore JSON parse errors and use the default message
}
alert(errorMessage);
return;
}

Copilot uses AI. Check for mistakes.
fetchTasks();
} catch (error) {
alert('Error al actualizar la tarea');
console.error(error);
}
}

function cancelEdit(id, originalTitle, wasCompleted) {
fetchTasks();

Copilot AI Apr 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cancelEdit accepts originalTitle and wasCompleted but does not use them, and always re-fetches the full task list. Either remove the unused parameters or restore the original DOM state locally to avoid an unnecessary network request on cancel.

Suggested change
fetchTasks();
// Try to restore the original DOM state locally to avoid a full refetch
const editedElement = document.querySelector(`[data-task-id="${id}"]`);
// If we cannot find the element for some reason, fall back to refetching
if (!editedElement) {
fetchTasks();
return;
}
const li = editedElement.closest('li');
if (!li) {
fetchTasks();
return;
}
// Restore the original markup, mirroring addTaskToDOM
li.innerHTML = `
<span class="${wasCompleted ? 'completed' : ''}" data-task-id="${id}">${originalTitle}</span>
<div>
<button onclick="editTask('${id}')" title="Edit task"></button>
<button onclick="toggleComplete('${id}', ${!wasCompleted})"></button>
<button onclick="deleteTask('${id}')"></button>
</div>
`;

Copilot uses AI. Check for mistakes.
}

fetchTasks();
36 changes: 36 additions & 0 deletions frontend/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,42 @@ li button:hover {
li button:active {
background: var(--button-active-bg);
}
.edit-input {
flex: 1;
padding: 0.5em 0.75em;
border: 2px solid var(--primary-color);
border-radius: 4px;
background: var(--input-bg);
color: var(--text-color);
font-size: 1.05em;
outline: none;
transition: box-shadow 0.2s;
}
.edit-input:focus {
box-shadow: 0 0 0 3px var(--input-focus-shadow);
}
.save-btn {
background: #4caf50 !important;
color: white !important;
border-radius: 4px !important;
width: auto !important;
padding: 0 0.75em !important;
font-weight: 500;
}
.save-btn:hover {
background: #45a049 !important;
}
.cancel-btn {
background: #f44336 !important;
color: white !important;
border-radius: 4px !important;
width: auto !important;
padding: 0 0.75em !important;
font-weight: 500;
}
.cancel-btn:hover {
background: #da190b !important;
Comment on lines +190 to +210

Copilot AI Apr 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The save/cancel button styles rely heavily on !important to override the base li button rules. This makes future styling harder and can cause specificity battles. Prefer increasing selector specificity (e.g., li button.save-btn, li button.cancel-btn) or scoping styles so !important isn't needed.

Suggested change
.save-btn {
background: #4caf50 !important;
color: white !important;
border-radius: 4px !important;
width: auto !important;
padding: 0 0.75em !important;
font-weight: 500;
}
.save-btn:hover {
background: #45a049 !important;
}
.cancel-btn {
background: #f44336 !important;
color: white !important;
border-radius: 4px !important;
width: auto !important;
padding: 0 0.75em !important;
font-weight: 500;
}
.cancel-btn:hover {
background: #da190b !important;
li button.save-btn {
background: #4caf50;
color: white;
border-radius: 4px;
width: auto;
padding: 0 0.75em;
font-weight: 500;
}
li button.save-btn:hover {
background: #45a049;
}
li button.cancel-btn {
background: #f44336;
color: white;
border-radius: 4px;
width: auto;
padding: 0 0.75em;
font-weight: 500;
}
li button.cancel-btn:hover {
background: #da190b;

Copilot uses AI. Check for mistakes.
}
@media (max-width: 600px) {
body {
max-width: 100vw;
Expand Down
Loading