diff --git a/frontend/index.html b/frontend/index.html index 81125fe..18c6146 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,7 +2,7 @@ - + diff --git a/frontend/src/api/todos.ts b/frontend/src/api/todos.ts index 9f1e84e..39fcbbc 100644 --- a/frontend/src/api/todos.ts +++ b/frontend/src/api/todos.ts @@ -4,13 +4,16 @@ export interface Todo { title: string body: string | null url: string | null - domain: string | null + domains: string[] category: string | null tags: string[] status: 'pending' | 'done' | 'cancelled' priority: 'low' | 'medium' | 'high' due_date: string | null postponed_count: number + photo_path: string | null + gps_lat: number | null + gps_lng: number | null created_at: string updated_at: string | null owner_id: string | null @@ -21,11 +24,15 @@ export interface TodoCreate { body?: string url?: string domain?: string + domains?: string[] category?: string tags?: string[] status?: 'pending' | 'done' | 'cancelled' priority?: 'low' | 'medium' | 'high' due_date?: string + photo_path?: string + gps_lat?: number + gps_lng?: number } export interface TodoUpdate { @@ -33,11 +40,15 @@ export interface TodoUpdate { body?: string url?: string domain?: string + domains?: string[] category?: string tags?: string[] status?: 'pending' | 'done' | 'cancelled' priority?: 'low' | 'medium' | 'high' due_date?: string + photo_path?: string + gps_lat?: number + gps_lng?: number } export interface TodoFilters { diff --git a/frontend/src/components/todos/TodoForm.tsx b/frontend/src/components/todos/TodoForm.tsx index 993c08a..05a0b3c 100644 --- a/frontend/src/components/todos/TodoForm.tsx +++ b/frontend/src/components/todos/TodoForm.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useState, useRef } from 'react' import type { TodoCreate } from '../../api/todos' const DOMAINS = [ @@ -6,10 +6,27 @@ const DOMAINS = [ 'bricolage', 'jardin', 'cuisine', 'voyage', 'animaux', ] +export const DOMAIN_COLORS: Record = { + informatique: 'var(--info)', + diy: 'var(--warn)', + electronique: 'var(--accent)', + domotique: 'var(--ok)', + bricolage: 'var(--warn)', + jardin: 'var(--ok)', + cuisine: 'var(--err)', + voyage: 'var(--info)', + animaux: 'var(--ok)', +} + +const PRIORITY_CONFIG = [ + { value: 'high' as const, label: 'Haute', color: 'var(--err)', textColor: '#fff' }, + { value: 'medium' as const, label: 'Moyenne', color: 'var(--warn)', textColor: '#1d2021' }, + { value: 'low' as const, label: 'Basse', color: 'var(--bg-5)', textColor: 'var(--ink-2)' }, +] + interface TodoFormProps { onSubmit: (data: TodoCreate) => Promise onCancel: () => void - extended?: boolean initialValues?: Partial submitLabel?: string } @@ -26,17 +43,67 @@ const inputStyle: React.CSSProperties = { boxSizing: 'border-box', } -export default function TodoForm({ onSubmit, onCancel, extended = false, initialValues, submitLabel = 'Créer' }: TodoFormProps) { +const labelStyle: React.CSSProperties = { + color: 'var(--ink-3)', + fontSize: 11, + fontFamily: 'var(--font-ui)', + marginBottom: 6, + textTransform: 'uppercase', + letterSpacing: 0.5, +} + +export default function TodoForm({ onSubmit, onCancel, initialValues, submitLabel = 'Créer' }: TodoFormProps) { + const today = new Date().toISOString().slice(0, 10) const [title, setTitle] = useState(initialValues?.title ?? '') - const [domain, setDomain] = useState(initialValues?.domain ?? '') + const [selectedDomains, setSelectedDomains] = useState(initialValues?.domains ?? []) const [priority, setPriority] = useState<'low' | 'medium' | 'high'>(initialValues?.priority ?? 'medium') const [dueDate, setDueDate] = useState( - initialValues?.due_date ? initialValues.due_date.slice(0, 10) : '' + initialValues?.due_date ? initialValues.due_date.slice(0, 10) : today ) const [body, setBody] = useState(initialValues?.body ?? '') const [url, setUrl] = useState(initialValues?.url ?? '') const [tags, setTags] = useState((initialValues?.tags ?? []).join(', ')) + const [gpsLat, setGpsLat] = useState(initialValues?.gps_lat ?? undefined) + const [gpsLng, setGpsLng] = useState(initialValues?.gps_lng ?? undefined) + const [photoPath, setPhotoPath] = useState(initialValues?.photo_path ?? undefined) + const [photoLoading, setPhotoLoading] = useState(false) + const [gpsLoading, setGpsLoading] = useState(false) const [loading, setLoading] = useState(false) + const fileRef = useRef(null) + + function toggleDomain(d: string) { + setSelectedDomains(prev => prev.includes(d) ? prev.filter(x => x !== d) : [...prev, d]) + } + + async function handlePhotoCapture(e: React.ChangeEvent) { + const file = e.target.files?.[0] + if (!file) return + setPhotoLoading(true) + try { + const formData = new FormData() + formData.append('file', file) + const res = await fetch('/api/media/upload', { method: 'POST', body: formData }) + if (res.ok) { + const data = await res.json() as { file_path: string } + setPhotoPath(data.file_path) + } + } finally { + setPhotoLoading(false) + } + } + + function handleGps() { + if (!navigator.geolocation) return + setGpsLoading(true) + navigator.geolocation.getCurrentPosition( + pos => { + setGpsLat(pos.coords.latitude) + setGpsLng(pos.coords.longitude) + setGpsLoading(false) + }, + () => setGpsLoading(false) + ) + } async function handleSubmit(e: React.FormEvent) { e.preventDefault() @@ -45,12 +112,15 @@ export default function TodoForm({ onSubmit, onCancel, extended = false, initial try { await onSubmit({ title: title.trim(), - domain: domain || undefined, + domains: selectedDomains, priority, due_date: dueDate ? new Date(dueDate).toISOString() : undefined, body: body.trim() || undefined, url: url.trim() || undefined, tags: tags ? tags.split(',').map(t => t.trim()).filter(Boolean) : [], + photo_path: photoPath, + gps_lat: gpsLat, + gps_lng: gpsLng, }) } finally { setLoading(false) @@ -58,7 +128,7 @@ export default function TodoForm({ onSubmit, onCancel, extended = false, initial } return ( -
+ -
- - + {/* Domaines — chips multi-select */} +
+
Domaines
+
+ {DOMAINS.map(d => { + const selected = selectedDomains.includes(d) + const color = DOMAIN_COLORS[d] + return ( + + ) + })} +
- setDueDate(e.target.value)} + {/* Priorité — boutons radio colorés */} +
+
Priorité
+
+ {PRIORITY_CONFIG.map(p => { + const selected = priority === p.value + return ( + + ) + })} +
+
+ + {/* Date objectif */} +
+
Date objectif
+ setDueDate(e.target.value)} + /> +
+ + {/* Description */} +