feat(todos): refonte formulaire + chips domaines + non-zoomable
- TodoForm : domaines en chips multi-select colorés, priorité en 3 boutons colorés (haute/moyenne/basse), date initialisée à aujourd'hui, description et URL toujours visibles, boutons photo et GPS - TodosPage : suppression filtres domaine/priorité, tags colorés par domaine dans les lignes, userSelect:none, groupage multi-domaines - todos.ts : ajout domains[], photo_path, gps_lat/lng dans les interfaces TS - index.html : viewport maximum-scale=1.0, user-scalable=no Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@ 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'
|
||||
import TodoForm, { DOMAIN_COLORS } from '../components/todos/TodoForm'
|
||||
import Modal from '../components/Modal'
|
||||
|
||||
type EditingTodo = Todo | null
|
||||
@@ -12,9 +12,11 @@ const DOMAINS = [
|
||||
'informatique', 'diy', 'electronique', 'domotique',
|
||||
'bricolage', 'jardin', 'cuisine', 'voyage', 'animaux',
|
||||
]
|
||||
|
||||
const STATUS_LABELS: Record<string, string> = {
|
||||
pending: 'En cours', done: 'Terminé', cancelled: 'Annulé',
|
||||
}
|
||||
|
||||
const PRIORITY_COLORS: Record<string, string> = {
|
||||
high: 'var(--err)', medium: 'var(--warn)', low: 'var(--ink-3)',
|
||||
}
|
||||
@@ -29,6 +31,8 @@ const selectStyle: React.CSSProperties = {
|
||||
fontSize: 13,
|
||||
}
|
||||
|
||||
const noSelect: React.CSSProperties = { userSelect: 'none' }
|
||||
|
||||
export default function TodosPage() {
|
||||
const [todos, setTodos] = useState<Todo[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
@@ -98,20 +102,20 @@ export default function TodosPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// Grouper par domaine pour la vue mobile
|
||||
// Grouper par domaine (un todo peut apparaître dans plusieurs groupes)
|
||||
const grouped = DOMAINS.reduce<Record<string, Todo[]>>((acc, d) => {
|
||||
const items = todos.filter(t => t.domain === d)
|
||||
const items = todos.filter(t => t.domains.includes(d))
|
||||
if (items.length > 0) acc[d] = items
|
||||
return acc
|
||||
}, {})
|
||||
const sansDomaine = todos.filter(t => !t.domain)
|
||||
const sansDomaine = todos.filter(t => t.domains.length === 0)
|
||||
if (sansDomaine.length > 0) grouped['—'] = sansDomaine
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
{/* En-tête + filtres */}
|
||||
{/* En-tête + filtre statut uniquement */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16, flexWrap: 'wrap' }}>
|
||||
<h1 style={{ color: 'var(--accent)', fontFamily: 'var(--font-mono)', margin: 0, flex: 1, minWidth: 100 }}>
|
||||
<h1 style={{ color: 'var(--accent)', fontFamily: 'var(--font-mono)', margin: 0, flex: 1, minWidth: 100, ...noSelect }}>
|
||||
Tâches
|
||||
</h1>
|
||||
<select
|
||||
@@ -124,24 +128,6 @@ export default function TodosPage() {
|
||||
<option value="cancelled">Annulé</option>
|
||||
<option value="">Tous</option>
|
||||
</select>
|
||||
<select
|
||||
style={selectStyle}
|
||||
value={filters.domain ?? ''}
|
||||
onChange={e => setFilters(f => ({ ...f, domain: e.target.value || undefined }))}
|
||||
>
|
||||
<option value="">Tous domaines</option>
|
||||
{DOMAINS.map(d => <option key={d} value={d}>{d}</option>)}
|
||||
</select>
|
||||
<select
|
||||
style={selectStyle}
|
||||
value={filters.priority ?? ''}
|
||||
onChange={e => setFilters(f => ({ ...f, priority: e.target.value || undefined }))}
|
||||
>
|
||||
<option value="">Toutes priorités</option>
|
||||
<option value="high">Haute</option>
|
||||
<option value="medium">Moyenne</option>
|
||||
<option value="low">Basse</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
@@ -150,29 +136,31 @@ export default function TodosPage() {
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Formulaire de création */}
|
||||
{/* Modal création */}
|
||||
{showForm && (
|
||||
<Modal title="Nouvelle tâche" onClose={() => setShowForm(false)}>
|
||||
<TodoForm onSubmit={handleCreate} onCancel={() => setShowForm(false)} extended />
|
||||
<TodoForm onSubmit={handleCreate} onCancel={() => setShowForm(false)} />
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* Formulaire d'édition */}
|
||||
{/* Modal édition */}
|
||||
{editingTodo && (
|
||||
<Modal title="Modifier la tâche" onClose={() => setEditingTodo(null)}>
|
||||
<TodoForm
|
||||
onSubmit={data => handleUpdate(editingTodo.id, data)}
|
||||
onCancel={() => setEditingTodo(null)}
|
||||
extended
|
||||
submitLabel="Enregistrer"
|
||||
initialValues={{
|
||||
title: editingTodo.title,
|
||||
domain: editingTodo.domain ?? undefined,
|
||||
domains: editingTodo.domains,
|
||||
priority: editingTodo.priority,
|
||||
due_date: editingTodo.due_date ?? undefined,
|
||||
body: editingTodo.body ?? undefined,
|
||||
url: editingTodo.url ?? undefined,
|
||||
tags: editingTodo.tags,
|
||||
photo_path: editingTodo.photo_path ?? undefined,
|
||||
gps_lat: editingTodo.gps_lat ?? undefined,
|
||||
gps_lng: editingTodo.gps_lng ?? undefined,
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
@@ -186,11 +174,13 @@ export default function TodosPage() {
|
||||
<div className="block lg:hidden">
|
||||
{!loading && Object.entries(grouped).map(([domain, items]) => (
|
||||
<div key={domain} style={{ marginBottom: 20 }}>
|
||||
{/* Entête de groupe avec badge compteur */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8, ...noSelect }}>
|
||||
<span style={{
|
||||
color: 'var(--accent)', fontFamily: 'var(--font-mono)',
|
||||
fontSize: 12, textTransform: 'uppercase', letterSpacing: 1,
|
||||
color: DOMAIN_COLORS[domain] ?? 'var(--accent)',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: 12,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 1,
|
||||
}}>
|
||||
{domain}
|
||||
</span>
|
||||
@@ -232,18 +222,35 @@ export default function TodosPage() {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
...noSelect,
|
||||
}}>
|
||||
<div style={{ width: 8, height: 8, borderRadius: '50%', background: PRIORITY_COLORS[todo.priority], flexShrink: 0 }} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ color: 'var(--ink-1)', fontSize: 14, fontFamily: 'var(--font-ui)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{todo.title}
|
||||
</div>
|
||||
{todo.due_date && (
|
||||
<div style={{ color: 'var(--ink-3)', fontSize: 11, marginTop: 2 }}>
|
||||
{new Date(todo.due_date).toLocaleDateString('fr-FR')}
|
||||
{todo.postponed_count > 0 && ` · reporté ${todo.postponed_count}×`}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', gap: 4, marginTop: 3, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
{todo.domains.map(d => (
|
||||
<span
|
||||
key={d}
|
||||
style={{
|
||||
background: DOMAIN_COLORS[d] ?? 'var(--bg-5)',
|
||||
color: '#1d2021',
|
||||
fontSize: 10,
|
||||
fontFamily: 'var(--font-ui)',
|
||||
fontWeight: 600,
|
||||
borderRadius: 999,
|
||||
padding: '1px 7px',
|
||||
}}
|
||||
>{d}</span>
|
||||
))}
|
||||
{todo.due_date && (
|
||||
<span style={{ color: 'var(--ink-3)', fontSize: 11 }}>
|
||||
{new Date(todo.due_date).toLocaleDateString('fr-FR')}
|
||||
{todo.postponed_count > 0 && ` · reporté ${todo.postponed_count}×`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SwipeableRow>
|
||||
@@ -253,7 +260,7 @@ export default function TodosPage() {
|
||||
))}
|
||||
|
||||
{!loading && todos.length === 0 && (
|
||||
<p style={{ color: 'var(--ink-3)', textAlign: 'center', marginTop: 40 }}>Aucune tâche</p>
|
||||
<p style={{ color: 'var(--ink-3)', textAlign: 'center', marginTop: 40, ...noSelect }}>Aucune tâche</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -261,9 +268,9 @@ export default function TodosPage() {
|
||||
<div className="hidden lg:block">
|
||||
{!loading && (
|
||||
<>
|
||||
{/* Filtre période (laptop uniquement) */}
|
||||
{/* Filtre période */}
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 12, alignItems: 'center' }}>
|
||||
<span style={{ color: 'var(--ink-3)', fontSize: 12, fontFamily: 'var(--font-ui)' }}>Période :</span>
|
||||
<span style={{ color: 'var(--ink-3)', fontSize: 12, fontFamily: 'var(--font-ui)', ...noSelect }}>Période :</span>
|
||||
<input
|
||||
type="date"
|
||||
style={selectStyle}
|
||||
@@ -282,8 +289,8 @@ export default function TodosPage() {
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontFamily: 'var(--font-ui)', fontSize: 13 }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '1px solid var(--bg-4)' }}>
|
||||
{['Titre', 'Domaine', 'Priorité', 'Statut', 'Date objectif', 'Reports', 'Actions'].map(h => (
|
||||
<th key={h} style={{ padding: '10px 14px', textAlign: 'left', color: 'var(--ink-3)', fontWeight: 500 }}>{h}</th>
|
||||
{['Titre', 'Domaines', 'Priorité', 'Statut', 'Date objectif', 'Reports', 'Actions'].map(h => (
|
||||
<th key={h} style={{ padding: '10px 14px', textAlign: 'left', color: 'var(--ink-3)', fontWeight: 500, ...noSelect }}>{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -297,7 +304,7 @@ export default function TodosPage() {
|
||||
}}
|
||||
>
|
||||
<td
|
||||
style={{ padding: '10px 14px', color: 'var(--ink-1)', maxWidth: 280, cursor: 'pointer' }}
|
||||
style={{ padding: '10px 14px', color: 'var(--ink-1)', maxWidth: 280, cursor: 'pointer', ...noSelect }}
|
||||
onDoubleClick={() => setEditingTodo(todo)}
|
||||
title="Double-clic pour modifier"
|
||||
>
|
||||
@@ -314,19 +321,38 @@ export default function TodosPage() {
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ padding: '10px 14px', color: 'var(--ink-2)' }}>{todo.domain ?? '—'}</td>
|
||||
<td style={{ padding: '10px 14px' }}>
|
||||
<td style={{ padding: '10px 14px', ...noSelect }}>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{todo.domains.length === 0 ? (
|
||||
<span style={{ color: 'var(--ink-4)' }}>—</span>
|
||||
) : todo.domains.map(d => (
|
||||
<span
|
||||
key={d}
|
||||
style={{
|
||||
background: DOMAIN_COLORS[d] ?? 'var(--bg-5)',
|
||||
color: '#1d2021',
|
||||
fontSize: 11,
|
||||
fontFamily: 'var(--font-ui)',
|
||||
fontWeight: 600,
|
||||
borderRadius: 999,
|
||||
padding: '2px 8px',
|
||||
}}
|
||||
>{d}</span>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
<td style={{ padding: '10px 14px', ...noSelect }}>
|
||||
<span style={{ color: PRIORITY_COLORS[todo.priority], fontFamily: 'var(--font-mono)', fontSize: 12 }}>
|
||||
{todo.priority}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '10px 14px', color: 'var(--ink-2)' }}>
|
||||
<td style={{ padding: '10px 14px', color: 'var(--ink-2)', ...noSelect }}>
|
||||
{STATUS_LABELS[todo.status] ?? todo.status}
|
||||
</td>
|
||||
<td style={{ padding: '10px 14px', color: 'var(--ink-2)', fontFamily: 'var(--font-mono)', fontSize: 12 }}>
|
||||
<td style={{ padding: '10px 14px', color: 'var(--ink-2)', fontFamily: 'var(--font-mono)', fontSize: 12, ...noSelect }}>
|
||||
{todo.due_date ? new Date(todo.due_date).toLocaleDateString('fr-FR') : '—'}
|
||||
</td>
|
||||
<td style={{ padding: '10px 14px', color: 'var(--ink-3)', fontFamily: 'var(--font-mono)' }}>
|
||||
<td style={{ padding: '10px 14px', color: 'var(--ink-3)', fontFamily: 'var(--font-mono)', ...noSelect }}>
|
||||
{todo.postponed_count > 0 ? `${todo.postponed_count}×` : '—'}
|
||||
</td>
|
||||
<td style={{ padding: '10px 14px' }}>
|
||||
@@ -362,14 +388,14 @@ export default function TodosPage() {
|
||||
</tbody>
|
||||
</table>
|
||||
{todos.length === 0 && (
|
||||
<p style={{ padding: 24, color: 'var(--ink-3)', textAlign: 'center' }}>Aucune tâche</p>
|
||||
<p style={{ padding: 24, color: 'var(--ink-3)', textAlign: 'center', ...noSelect }}>Aucune tâche</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* FAB mobile (au-dessus de la barre de navigation) */}
|
||||
{/* FAB mobile */}
|
||||
<button
|
||||
className="lg:hidden"
|
||||
onClick={() => setShowForm(true)}
|
||||
|
||||
Reference in New Issue
Block a user