diff --git a/backend/controllers/tasksController.js b/backend/controllers/tasksController.js index 745738d..2f1e314 100644 --- a/backend/controllers/tasksController.js +++ b/backend/controllers/tasksController.js @@ -3,6 +3,11 @@ const path = require('path'); const { v4: uuidv4 } = require('uuid'); const filePath = path.join(__dirname, '../data/tasks.json'); +const VALID_CATEGORIES = ['High', 'Medium', 'Low']; + +function normalizeCategory(category) { + return VALID_CATEGORIES.includes(category) ? category : 'Medium'; +} function readTasks() { return JSON.parse(fs.readFileSync(filePath, 'utf-8')); @@ -18,8 +23,12 @@ exports.getAllTasks = (req, res) => { exports.createTask = (req, res) => { const tasks = readTasks(); - const { title, completed = false } = req.body; - const newTask = { id: uuidv4(), title, completed }; + const { title, completed = false, category = 'Medium' } = req.body; + if (typeof title !== 'string' || title.trim() === '') { + return res.status(400).json({ message: 'Title is required and cannot be empty' }); + } + const normalizedCategory = normalizeCategory(category); + const newTask = { id: uuidv4(), title: title.trim(), completed, category: normalizedCategory }; tasks.push(newTask); writeTasks(tasks); res.status(201).json(newTask); @@ -30,8 +39,16 @@ exports.updateTask = (req, res) => { const task = tasks.find(t => t.id === req.params.id); if (!task) return res.status(404).json({ message: 'Task not found' }); - task.title = req.body.title ?? task.title; + if (req.body.title !== undefined) { + if (typeof req.body.title !== 'string' || req.body.title.trim() === '') { + return res.status(400).json({ message: 'Title is required and cannot be empty' }); + } + task.title = req.body.title.trim(); + } task.completed = req.body.completed ?? task.completed; + if (req.body.category !== undefined) { + task.category = normalizeCategory(req.body.category); + } writeTasks(tasks); res.json(task); }; diff --git a/frontend/index.html b/frontend/index.html index 915426f..4291634 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -14,6 +14,11 @@

Task List

+
diff --git a/frontend/script.js b/frontend/script.js index a519ccf..e7cc9e2 100644 --- a/frontend/script.js +++ b/frontend/script.js @@ -1,7 +1,9 @@ const API_URL = 'http://localhost:3000/api/tasks'; +const DEFAULT_CATEGORY = 'Medium'; const taskList = document.getElementById('taskList'); const taskForm = document.getElementById('taskForm'); const taskInput = document.getElementById('taskInput'); +const taskCategory = document.getElementById('taskCategory'); const themeToggle = document.getElementById('themeToggle'); // Theme functionality @@ -35,28 +37,54 @@ async function fetchTasks() { } function addTaskToDOM(task) { + const category = task.category || DEFAULT_CATEGORY; const li = document.createElement('li'); - li.innerHTML = ` - ${task.title} -
- - -
- `; + const taskContent = document.createElement('div'); + taskContent.className = 'task-content'; + + const title = document.createElement('span'); + title.className = `task-title ${task.completed ? 'completed' : ''}`; + title.textContent = task.title; + + const badge = document.createElement('span'); + badge.className = `category-badge category-${category.toLowerCase()}`; + badge.textContent = category; + + const actions = document.createElement('div'); + + const completeButton = document.createElement('button'); + completeButton.textContent = '✓'; + completeButton.setAttribute('aria-label', task.completed ? 'Mark task as incomplete' : 'Mark task as complete'); + completeButton.addEventListener('click', () => toggleComplete(task.id, !task.completed)); + + const deleteButton = document.createElement('button'); + deleteButton.textContent = '✕'; + deleteButton.setAttribute('aria-label', 'Delete task'); + deleteButton.addEventListener('click', () => deleteTask(task.id)); + + taskContent.appendChild(title); + taskContent.appendChild(badge); + actions.appendChild(completeButton); + actions.appendChild(deleteButton); + li.appendChild(taskContent); + li.appendChild(actions); taskList.appendChild(li); } taskForm.addEventListener('submit', async e => { e.preventDefault(); - const title = taskInput.value; + const title = taskInput.value.trim(); + if (!title) return; + const category = taskCategory.value || DEFAULT_CATEGORY; const res = await fetch(API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ title }) + body: JSON.stringify({ title, category }) }); const task = await res.json(); addTaskToDOM(task); taskInput.value = ''; + taskCategory.value = DEFAULT_CATEGORY; }); async function toggleComplete(id, completed) { diff --git a/frontend/style.css b/frontend/style.css index d351594..65e6af1 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -88,7 +88,8 @@ form { gap: 12px; margin-bottom: 1.5em; } -#taskInput { +#taskInput, +#taskCategory { flex: 1; padding: 0.75em 1em; border: none; @@ -100,7 +101,8 @@ form { outline: none; transition: box-shadow 0.2s; } -#taskInput:focus { +#taskInput:focus, +#taskCategory:focus { box-shadow: 0 2px 8px var(--input-focus-shadow); } button[type="submit"] { @@ -138,12 +140,39 @@ li { li:hover { box-shadow: 0 4px 16px var(--task-hover-shadow); } -li span { +.task-title { font-size: 1.05em; flex: 1; color: var(--text-color); transition: color 0.2s; } +.task-content { + display: flex; + flex-direction: column; + gap: 4px; + flex: 1; +} +.category-badge { + display: inline-block; + width: fit-content; + padding: 0.15em 0.6em; + border-radius: 999px; + font-size: 0.75em; + font-weight: 500; + line-height: 1.4; +} +.category-high { + background: #ffebee; + color: #8b0000; +} +.category-medium { + background: #fff8e1; + color: #8a3b00; +} +.category-low { + background: #e8f5e9; + color: #1b5e20; +} .completed { text-decoration: line-through; color: var(--completed-color);