import { useState, useEffect, useCallback, useRef } from 'react' import type { Note, NoteFilters } from '../api/notes' import { fetchNotes, createNote, updateNote, deleteNote, addAttachment, deleteAttachment } from '../api/notes' import Modal from '../components/Modal' import NoteForm from '../components/notes/NoteForm' const noSelect: React.CSSProperties = { userSelect: 'none' } const inputStyle: 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, } function formatDate(iso: string) { return new Date(iso).toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', year: 'numeric' }) } function NoteCard({ note, onEdit, onDelete, onAddPhoto, onAddAudio, onDeleteAtt }: { note: Note onEdit: () => void onDelete: () => void onAddPhoto: (file: File) => void onAddAudio: (file: File) => void onDeleteAtt: (attId: string) => void }) { const photoRef = useRef(null) const audioRef = useRef(null) const [recording, setRecording] = useState(false) const recorderRef = useRef(null) const chunksRef = useRef([]) const images = note.attachments.filter(a => a.file_type === 'image') const audios = note.attachments.filter(a => a.file_type === 'audio') async function startRecord() { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) const recorder = new MediaRecorder(stream) chunksRef.current = [] recorder.ondataavailable = e => { if (e.data.size > 0) chunksRef.current.push(e.data) } recorder.onstop = () => { stream.getTracks().forEach(t => t.stop()) const blob = new Blob(chunksRef.current, { type: 'audio/webm' }) onAddAudio(new File([blob], 'enregistrement.webm', { type: 'audio/webm' })) } recorder.start() recorderRef.current = recorder setRecording(true) } catch { // micro non disponible } } function stopRecord() { recorderRef.current?.stop() setRecording(false) } return (
{/* En-tête */}
{note.title && (
{note.title}
)}
{note.content.length > 200 ? note.content.slice(0, 200) + '…' : note.content}
{/* Photos */} {images.length > 0 && (
{images.map(img => (
))}
)} {/* Audios */} {audios.length > 0 && (
{audios.map(aud => (
))}
)} {/* Méta */}
{formatDate(note.created_at)} {note.category && ( {note.category} )} {note.tags.map(t => ( {t} ))} {note.gps_lat && 📍}
{/* Actions */}
{ const f = e.target.files?.[0]; if (f) onAddPhoto(f); e.target.value = '' }} /> { const f = e.target.files?.[0]; if (f) onAddAudio(f); e.target.value = '' }} />
) } export default function NotesPage() { const [notes, setNotes] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [showForm, setShowForm] = useState(false) const [editingNote, setEditingNote] = useState(null) const [filters, setFilters] = useState({}) const [searchInput, setSearchInput] = useState('') const searchTimer = useRef | null>(null) const load = useCallback(async () => { setLoading(true) setError(null) try { setNotes(await fetchNotes(filters)) } catch { setError('Erreur de chargement') } finally { setLoading(false) } }, [filters]) useEffect(() => { void load() }, [load]) function handleSearchChange(val: string) { setSearchInput(val) if (searchTimer.current) clearTimeout(searchTimer.current) searchTimer.current = setTimeout(() => { setFilters(f => ({ ...f, q: val || undefined })) }, 300) } async function handleCreate(data: Parameters[0]) { await createNote(data) setShowForm(false) void load() } async function handleUpdate(id: string, data: Parameters[1]) { await updateNote(id, data) setEditingNote(null) void load() } async function handleDelete(id: string) { if (!confirm('Supprimer cette note ?')) return try { await deleteNote(id) void load() } catch { setError('Erreur lors de la suppression') } } async function handleAddPhoto(noteId: string, file: File) { try { await addAttachment(noteId, file) void load() } catch { setError('Erreur upload photo') } } async function handleAddAudio(noteId: string, file: File) { try { await addAttachment(noteId, file) void load() } catch { setError('Erreur upload audio') } } async function handleDeleteAtt(noteId: string, attId: string) { try { await deleteAttachment(noteId, attId) void load() } catch { setError('Erreur suppression pièce jointe') } } const hasActiveFilters = filters.has_photo || filters.has_audio || filters.has_gps return (
{/* En-tête */}

Notes

{/* Barre de recherche + filtres */}
handleSearchChange(e.target.value)} /> {/* Filtres rapides */} {(['📷 Photo', '🎤 Audio', '📍 GPS'] as const).map((label, i) => { const key = ['has_photo', 'has_audio', 'has_gps'][i] as keyof NoteFilters const active = filters[key] === true return ( ) })} {hasActiveFilters && ( )}
{error && (

{error}

)} {/* Modal création */} {showForm && ( setShowForm(false)}> setShowForm(false)} /> )} {/* Modal édition */} {editingNote && ( setEditingNote(null)}> handleUpdate(editingNote.id, data)} onCancel={() => setEditingNote(null)} submitLabel="Enregistrer" /> )} {loading && (

Chargement…

)} {/* Mobile — liste chronologique */}
{!loading && notes.length === 0 && (

Aucune note

)}
{notes.map(note => ( setEditingNote(note)} onDelete={() => void handleDelete(note.id)} onAddPhoto={f => void handleAddPhoto(note.id, f)} onAddAudio={f => void handleAddAudio(note.id, f)} onDeleteAtt={attId => void handleDeleteAtt(note.id, attId)} /> ))}
{/* Laptop — grille */}
{!loading && notes.length === 0 && (

Aucune note

)}
{notes.map(note => ( setEditingNote(note)} onDelete={() => void handleDelete(note.id)} onAddPhoto={f => void handleAddPhoto(note.id, f)} onAddAudio={f => void handleAddAudio(note.id, f)} onDeleteAtt={attId => void handleDeleteAtt(note.id, attId)} /> ))}
{/* FAB */}
) }