From 134678a6f17802b520b8818f58fafcb035654825 Mon Sep 17 00:00:00 2001 From: Gilles Soulier Date: Sun, 24 May 2026 14:54:47 +0200 Subject: [PATCH] =?UTF-8?q?feat(todos):=20favicon=20maison=20+=20mode=20?= =?UTF-8?q?=C3=A9dition=20double-tap/double-clic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Favicon SVG maison Gruvbox orange sur fond sombre - TodoForm accepte initialValues et submitLabel pour l'édition - SwipeableRow détecte le double-tap (< 300ms, sans déplacement) - TodosPage : double-tap mobile / double-clic laptop ouvre l'édition Co-Authored-By: Claude Sonnet 4.6 --- frontend/index.html | 1 + frontend/public/favicon.svg | 5 +++ .../src/components/todos/SwipeableRow.tsx | 28 ++++++++---- frontend/src/components/todos/TodoForm.tsx | 25 ++++++----- frontend/src/pages/TodosPage.tsx | 44 ++++++++++++++++++- 5 files changed, 83 insertions(+), 20 deletions(-) create mode 100644 frontend/public/favicon.svg diff --git a/frontend/index.html b/frontend/index.html index 68500d3..81125fe 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -9,6 +9,7 @@ + HomeHub diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..0fc7cbb --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/components/todos/SwipeableRow.tsx b/frontend/src/components/todos/SwipeableRow.tsx index e387293..b150252 100644 --- a/frontend/src/components/todos/SwipeableRow.tsx +++ b/frontend/src/components/todos/SwipeableRow.tsx @@ -2,16 +2,19 @@ import { useRef, useState } from 'react' interface SwipeableRowProps { children: React.ReactNode - rightContent: React.ReactNode // actions révélées par swipe gauche - onSwipeRight?: () => void // callback swipe droit (marquer done) + rightContent: React.ReactNode + onSwipeRight?: () => void + onDoubleTap?: () => void } -const THRESHOLD = 80 // pixels pour déclencher une action +const THRESHOLD = 80 +const DOUBLE_TAP_DELAY = 300 -export default function SwipeableRow({ children, rightContent, onSwipeRight }: SwipeableRowProps) { +export default function SwipeableRow({ children, rightContent, onSwipeRight, onDoubleTap }: SwipeableRowProps) { const [offsetX, setOffsetX] = useState(0) const [isDragging, setIsDragging] = useState(false) const startX = useRef(null) + const lastTap = useRef(0) function onTouchStart(e: React.TouchEvent) { startX.current = e.touches[0].clientX @@ -21,14 +24,25 @@ export default function SwipeableRow({ children, rightContent, onSwipeRight }: S function onTouchMove(e: React.TouchEvent) { if (startX.current === null) return const dx = e.touches[0].clientX - startX.current - // Clamp : +120px à droite, -160px à gauche (largeur des boutons) setOffsetX(Math.max(Math.min(dx, 120), -160)) } function onTouchEnd() { - if (offsetX > THRESHOLD && onSwipeRight) { + const finalOffset = offsetX + + if (finalOffset > THRESHOLD && onSwipeRight) { onSwipeRight() } + + // Double-tap : seulement si le doigt n'a pas bougé (tap, pas swipe) + if (Math.abs(finalOffset) < 10 && onDoubleTap) { + const now = Date.now() + if (now - lastTap.current < DOUBLE_TAP_DELAY) { + onDoubleTap() + } + lastTap.current = now + } + setOffsetX(0) setIsDragging(false) startX.current = null @@ -38,7 +52,6 @@ export default function SwipeableRow({ children, rightContent, onSwipeRight }: S return (
- {/* Boutons d'action révélés à droite (swipe gauche) */}
- {/* Rangée principale déplaçable */}
Promise onCancel: () => void - extended?: boolean // true = tous les champs (vue laptop) + extended?: boolean + initialValues?: Partial + submitLabel?: string } const inputStyle: React.CSSProperties = { @@ -24,14 +26,16 @@ const inputStyle: React.CSSProperties = { boxSizing: 'border-box', } -export default function TodoForm({ onSubmit, onCancel, extended = false }: TodoFormProps) { - const [title, setTitle] = useState('') - const [domain, setDomain] = useState('') - const [priority, setPriority] = useState<'low' | 'medium' | 'high'>('medium') - const [dueDate, setDueDate] = useState('') - const [body, setBody] = useState('') - const [url, setUrl] = useState('') - const [tags, setTags] = useState('') +export default function TodoForm({ onSubmit, onCancel, extended = false, initialValues, submitLabel = 'Créer' }: TodoFormProps) { + const [title, setTitle] = useState(initialValues?.title ?? '') + const [domain, setDomain] = useState(initialValues?.domain ?? '') + const [priority, setPriority] = useState<'low' | 'medium' | 'high'>(initialValues?.priority ?? 'medium') + const [dueDate, setDueDate] = useState( + initialValues?.due_date ? initialValues.due_date.slice(0, 10) : '' + ) + const [body, setBody] = useState(initialValues?.body ?? '') + const [url, setUrl] = useState(initialValues?.url ?? '') + const [tags, setTags] = useState((initialValues?.tags ?? []).join(', ')) const [loading, setLoading] = useState(false) async function handleSubmit(e: React.FormEvent) { @@ -81,7 +85,6 @@ export default function TodoForm({ onSubmit, onCancel, extended = false }: TodoF type="date" value={dueDate} onChange={e => setDueDate(e.target.value)} - placeholder="Date objectif" /> {extended && ( @@ -139,7 +142,7 @@ export default function TodoForm({ onSubmit, onCancel, extended = false }: TodoF minHeight: 48, }} > - {loading ? '…' : 'Créer'} + {loading ? '…' : submitLabel}
diff --git a/frontend/src/pages/TodosPage.tsx b/frontend/src/pages/TodosPage.tsx index c0c9edf..c087bf2 100644 --- a/frontend/src/pages/TodosPage.tsx +++ b/frontend/src/pages/TodosPage.tsx @@ -5,6 +5,8 @@ import { fetchTodos, createTodo, updateTodo, deleteTodo, postponeTodo } from '.. import SwipeableRow from '../components/todos/SwipeableRow' import TodoForm from '../components/todos/TodoForm' +type EditingTodo = Todo | null + const DOMAINS = [ 'informatique', 'diy', 'electronique', 'domotique', 'bricolage', 'jardin', 'cuisine', 'voyage', 'animaux', @@ -31,6 +33,7 @@ export default function TodosPage() { const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [showForm, setShowForm] = useState(false) + const [editingTodo, setEditingTodo] = useState(null) const [filters, setFilters] = useState({ status: 'pending' }) const load = useCallback(async () => { @@ -57,6 +60,16 @@ export default function TodosPage() { } } + async function handleUpdate(id: string, data: TodoCreate) { + try { + await updateTodo(id, data) + setEditingTodo(null) + void load() + } catch { + setError('Erreur lors de la mise à jour') + } + } + async function handleDone(id: string) { try { await updateTodo(id, { status: 'done' }) @@ -148,6 +161,30 @@ export default function TodosPage() {
)} + {/* Formulaire d'édition */} + {editingTodo && ( +
+

+ Modifier la tâche +

+ handleUpdate(editingTodo.id, data)} + onCancel={() => setEditingTodo(null)} + extended + submitLabel="Enregistrer" + initialValues={{ + title: editingTodo.title, + domain: editingTodo.domain ?? undefined, + priority: editingTodo.priority, + due_date: editingTodo.due_date ?? undefined, + body: editingTodo.body ?? undefined, + url: editingTodo.url ?? undefined, + tags: editingTodo.tags, + }} + /> +
+ )} + {loading && (

Chargement…

)} @@ -177,6 +214,7 @@ export default function TodosPage() { void handleDone(todo.id)} + onDoubleTap={() => setEditingTodo(todo)} rightContent={