All checks were successful
Release and Build Docker Images / release-and-build (push) Successful in 1m29s
130 lines
4.3 KiB
JavaScript
130 lines
4.3 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import { getTodos, createTodo, updateTodo, deleteTodo } from '../services/api';
|
|
|
|
const TodoList = () => {
|
|
const [todos, setTodos] = useState([]);
|
|
const [newTitle, setNewTitle] = useState('');
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState(null);
|
|
|
|
useEffect(() => {
|
|
fetchTodos();
|
|
}, []);
|
|
|
|
const fetchTodos = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const data = await getTodos();
|
|
// Sort by active first, then completed. Secondary sort by id descending
|
|
setTodos(data.sort((a, b) => {
|
|
if (a.completed === b.completed) {
|
|
return b.id - a.id;
|
|
}
|
|
return a.completed ? 1 : -1;
|
|
}));
|
|
} catch (err) {
|
|
setError(err.message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleCreate = async (e) => {
|
|
e.preventDefault();
|
|
if (!newTitle.trim()) return;
|
|
|
|
try {
|
|
const created = await createTodo({
|
|
title: newTitle.trim(),
|
|
description: '',
|
|
completed: false
|
|
});
|
|
setTodos([created, ...todos]);
|
|
setNewTitle('');
|
|
} catch (err) {
|
|
setError(err.message);
|
|
}
|
|
};
|
|
|
|
const handleToggle = async (todo) => {
|
|
try {
|
|
const updated = await updateTodo(todo.id, {
|
|
...todo,
|
|
completed: !todo.completed
|
|
});
|
|
setTodos(todos.map(t => t.id === todo.id ? updated : t).sort((a, b) => {
|
|
if (a.id === updated.id) a = updated;
|
|
if (b.id === updated.id) b = updated;
|
|
if (a.completed === b.completed) return b.id - a.id;
|
|
return a.completed ? 1 : -1;
|
|
}));
|
|
} catch (err) {
|
|
setError(err.message);
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (id) => {
|
|
try {
|
|
await deleteTodo(id);
|
|
setTodos(todos.filter(t => t.id !== id));
|
|
} catch (err) {
|
|
setError(err.message);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="todo-wrapper">
|
|
<h1 className="title">Tasks</h1>
|
|
|
|
<form className="todo-form" onSubmit={handleCreate}>
|
|
<input
|
|
type="text"
|
|
className="todo-input"
|
|
placeholder="What needs to be done?"
|
|
value={newTitle}
|
|
onChange={(e) => setNewTitle(e.target.value)}
|
|
autoFocus
|
|
/>
|
|
<button type="submit" className="add-btn" disabled={!newTitle.trim()}>
|
|
Add Task
|
|
</button>
|
|
</form>
|
|
|
|
{error ? (
|
|
<div className="error">{error}</div>
|
|
) : loading ? (
|
|
<div className="loading">Loading tasks...</div>
|
|
) : todos.length === 0 ? (
|
|
<div className="empty">All caught up! No active tasks.</div>
|
|
) : (
|
|
<ul className="todo-list">
|
|
{todos.map(todo => (
|
|
<li key={todo.id} className="todo-item">
|
|
<label className="todo-content">
|
|
<input
|
|
type="checkbox"
|
|
className="checkbox"
|
|
checked={todo.completed}
|
|
onChange={() => handleToggle(todo)}
|
|
/>
|
|
<span className={`todo-text ${todo.completed ? 'completed' : ''}`}>
|
|
{todo.title}
|
|
</span>
|
|
</label>
|
|
<button
|
|
className="delete-btn"
|
|
onClick={() => handleDelete(todo.id)}
|
|
aria-label="Delete"
|
|
>
|
|
✕
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default TodoList;
|