From 8aeb45387d37d0ded99a823131b9fd1f32ed80c2 Mon Sep 17 00:00:00 2001 From: Gilles Soulier Date: Sun, 24 May 2026 12:13:56 +0200 Subject: [PATCH] =?UTF-8?q?feat(todos):=20page=20TodosPage=20=E2=80=94=20v?= =?UTF-8?q?ue=20mobile=20swipeable=20+=20vue=20laptop=20tableau=20filtrabl?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/App.tsx | 2 + frontend/src/pages/TodosPage.tsx | 366 +++++++++++++++++++++++++++++++ 2 files changed, 368 insertions(+) create mode 100644 frontend/src/pages/TodosPage.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e245ccf..3325284 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,7 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom' import Layout from './components/layout/Layout' import HomePage from './pages/HomePage' +import TodosPage from './pages/TodosPage' export default function App() { return ( @@ -8,6 +9,7 @@ export default function App() { }> } /> + } /> diff --git a/frontend/src/pages/TodosPage.tsx b/frontend/src/pages/TodosPage.tsx new file mode 100644 index 0000000..f4a5d84 --- /dev/null +++ b/frontend/src/pages/TodosPage.tsx @@ -0,0 +1,366 @@ +// frontend/src/pages/TodosPage.tsx +import { useState, useEffect, useCallback } from 'react' +import type { Todo, TodoCreate, TodoFilters } from '../api/todos' +import { fetchTodos, createTodo, updateTodo, deleteTodo, postponeTodo } from '../api/todos' +import SwipeableRow from '../components/todos/SwipeableRow' +import TodoForm from '../components/todos/TodoForm' + +const DOMAINS = [ + 'informatique', 'diy', 'electronique', 'domotique', + 'bricolage', 'jardin', 'cuisine', 'voyage', 'animaux', +] +const STATUS_LABELS: Record = { + pending: 'En cours', done: 'Terminé', cancelled: 'Annulé', +} +const PRIORITY_COLORS: Record = { + high: 'var(--err)', medium: 'var(--warn)', low: 'var(--ink-3)', +} + +const selectStyle: React.CSSProperties = { + background: 'var(--bg-3)', + border: '1px solid var(--bg-5)', + borderRadius: 8, + padding: '6px 10px', + color: 'var(--ink-1)', + fontFamily: 'var(--font-ui)', + fontSize: 13, +} + +export default function TodosPage() { + const [todos, setTodos] = useState([]) + const [loading, setLoading] = useState(true) + const [showForm, setShowForm] = useState(false) + const [filters, setFilters] = useState({ status: 'pending' }) + + const load = useCallback(async () => { + setLoading(true) + try { + setTodos(await fetchTodos(filters)) + } finally { + setLoading(false) + } + }, [filters]) + + useEffect(() => { void load() }, [load]) + + async function handleCreate(data: TodoCreate) { + await createTodo(data) + setShowForm(false) + void load() + } + + async function handleDone(id: string) { + await updateTodo(id, { status: 'done' }) + void load() + } + + async function handleDelete(id: string) { + await deleteTodo(id) + void load() + } + + async function handlePostpone(id: string, days: 1 | 7) { + await postponeTodo(id, days) + void load() + } + + // Grouper par domaine pour la vue mobile + const grouped = DOMAINS.reduce>((acc, d) => { + const items = todos.filter(t => t.domain === d) + if (items.length > 0) acc[d] = items + return acc + }, {}) + const sansDomaine = todos.filter(t => !t.domain) + if (sansDomaine.length > 0) grouped['—'] = sansDomaine + + return ( +
+ {/* En-tête + filtres */} +
+

+ Tâches +

+ + + +
+ + {/* Formulaire de création */} + {showForm && ( +
+
+ setShowForm(false)} extended /> +
+
+ setShowForm(false)} /> +
+
+ )} + + {loading && ( +

Chargement…

+ )} + + {/* Vue mobile — liste groupée par domaine */} +
+ {!loading && Object.entries(grouped).map(([domain, items]) => ( +
+ {/* Entête de groupe avec badge compteur */} +
+ + {domain} + + + {items.length} + +
+ +
+ {items.map((todo, idx) => ( + void handleDone(todo.id)} + rightContent={ +
+ + + +
+ } + > +
+
+
+
+ {todo.title} +
+ {todo.due_date && ( +
+ {new Date(todo.due_date).toLocaleDateString('fr-FR')} + {todo.postponed_count > 0 && ` · reporté ${todo.postponed_count}×`} +
+ )} +
+
+ + ))} +
+
+ ))} + + {!loading && todos.length === 0 && ( +

Aucune tâche

+ )} +
+ + {/* Vue laptop — tableau filtrable */} +
+ {!loading && ( + <> + {/* Filtre période (laptop uniquement) */} +
+ Période : + setFilters(f => ({ ...f, due_after: e.target.value || undefined }))} + /> + setFilters(f => ({ ...f, due_before: e.target.value || undefined }))} + /> +
+ +
+ + + + {['Titre', 'Domaine', 'Priorité', 'Statut', 'Date objectif', 'Reports', 'Actions'].map(h => ( + + ))} + + + + {todos.map((todo, idx) => ( + + + + + + + + + + ))} + +
{h}
+
+ {todo.title} +
+ {todo.tags.length > 0 && ( +
+ {todo.tags.map(tag => ( + + {tag} + + ))} +
+ )} +
{todo.domain ?? '—'} + + {todo.priority} + + + {STATUS_LABELS[todo.status] ?? todo.status} + + {todo.due_date ? new Date(todo.due_date).toLocaleDateString('fr-FR') : '—'} + + {todo.postponed_count > 0 ? `${todo.postponed_count}×` : '—'} + +
+ {todo.status === 'pending' && ( + <> + + + + + )} + +
+
+ {todos.length === 0 && ( +

Aucune tâche

+ )} +
+ + )} +
+ + {/* FAB mobile (au-dessus de la barre de navigation) */} + {!showForm && ( + + )} + + {/* Bouton création laptop */} + {!showForm && ( + + )} +
+ ) +}