Add TodoList component
This commit is contained in:
105
frontend/src/todos/TodoList.tsx
Normal file
105
frontend/src/todos/TodoList.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { useEffect, useState, type FormEvent } from 'react';
|
||||
import { api, type Todo } from '../lib/api';
|
||||
import { supabase } from '../lib/supabase';
|
||||
|
||||
export function TodoList({ userEmail }: { userEmail: string }) {
|
||||
const [todos, setTodos] = useState<Todo[]>([]);
|
||||
const [newTitle, setNewTitle] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
async function refresh() {
|
||||
try {
|
||||
setTodos(await api.list());
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refresh().finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
async function onAdd(e: FormEvent) {
|
||||
e.preventDefault();
|
||||
const title = newTitle.trim();
|
||||
if (!title) return;
|
||||
setError(null);
|
||||
try {
|
||||
const created = await api.create(title);
|
||||
setTodos((prev) => [created, ...prev]);
|
||||
setNewTitle('');
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
async function onToggle(todo: Todo) {
|
||||
setError(null);
|
||||
try {
|
||||
const updated = await api.setCompleted(todo.id, !todo.completed);
|
||||
setTodos((prev) => prev.map((t) => (t.id === updated.id ? updated : t)));
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
async function onDelete(todo: Todo) {
|
||||
setError(null);
|
||||
try {
|
||||
await api.remove(todo.id);
|
||||
setTodos((prev) => prev.filter((t) => t.id !== todo.id));
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
async function onSignOut() {
|
||||
await supabase.auth.signOut();
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 520, margin: '2rem auto', fontFamily: 'sans-serif' }}>
|
||||
<header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
|
||||
<h1>Todos</h1>
|
||||
<span>
|
||||
{userEmail}{' '}
|
||||
<button type="button" onClick={onSignOut}>Sign out</button>
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<form onSubmit={onAdd} style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
|
||||
<input
|
||||
value={newTitle}
|
||||
onChange={(e) => setNewTitle(e.target.value)}
|
||||
placeholder="What needs doing?"
|
||||
maxLength={500}
|
||||
style={{ flex: 1, padding: 6 }}
|
||||
/>
|
||||
<button type="submit">Add</button>
|
||||
</form>
|
||||
|
||||
{error && <p style={{ color: 'crimson' }}>{error}</p>}
|
||||
{loading ? (
|
||||
<p>Loading…</p>
|
||||
) : todos.length === 0 ? (
|
||||
<p>No todos yet.</p>
|
||||
) : (
|
||||
<ul style={{ listStyle: 'none', padding: 0 }}>
|
||||
{todos.map((t) => (
|
||||
<li
|
||||
key={t.id}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '4px 0' }}
|
||||
>
|
||||
<input type="checkbox" checked={t.completed} onChange={() => onToggle(t)} />
|
||||
<span style={{ flex: 1, textDecoration: t.completed ? 'line-through' : 'none' }}>
|
||||
{t.title}
|
||||
</span>
|
||||
<button type="button" onClick={() => onDelete(t)}>Delete</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user