feat: Add initial frontend dependencies, base CSS, and Gitea Docker build workflow.
Some checks failed
Build and Push Docker Images / build-and-push (push) Failing after 2m23s
Some checks failed
Build and Push Docker Images / build-and-push (push) Failing after 2m23s
This commit is contained in:
129
frontend/src/components/TodoList.jsx
Normal file
129
frontend/src/components/TodoList.jsx
Normal file
@@ -0,0 +1,129 @@
|
||||
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="app-container">
|
||||
<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;
|
||||
Reference in New Issue
Block a user