diff --git a/frontend/package.json b/frontend/package.json index e4ad600..c506c0d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "homehub-frontend", "private": true, - "version": "0.5.4", + "version": "0.5.5", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/pages/NotesPage.tsx b/frontend/src/pages/NotesPage.tsx index 5bce26b..9a58d51 100644 --- a/frontend/src/pages/NotesPage.tsx +++ b/frontend/src/pages/NotesPage.tsx @@ -17,10 +17,102 @@ const inputStyle: React.CSSProperties = { fontSize: 13, } +const actionBtnStyle: React.CSSProperties = { + padding: '5px 10px', + borderRadius: 6, + border: '1px solid var(--bg-5)', + background: 'var(--bg-4)', + color: 'var(--ink-3)', + cursor: 'pointer', + fontSize: 13, + minHeight: 32, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +} + function formatDate(iso: string) { return new Date(iso).toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', year: 'numeric' }) } +// Formate le texte inline : **gras**, *italique*, `code` +function inlineFmt(text: string): React.ReactNode { + const parts = text.split(/(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/) + return ( + <> + {parts.map((p, i) => { + if (p.startsWith('**') && p.endsWith('**')) return {p.slice(2, -2)} + if (p.startsWith('*') && p.endsWith('*')) return {p.slice(1, -1)} + if (p.startsWith('`') && p.endsWith('`')) return {p.slice(1, -1)} + return p || null + })} + + ) +} + +// Renderer pseudo-markdown ligne par ligne +function renderMarkdown(text: string): React.ReactNode { + const lines = text.split('\n') + const nodes: React.ReactNode[] = [] + let i = 0 + while (i < lines.length) { + const line = lines[i] + if (line.startsWith('```')) { + const lang = line.slice(3).trim() + const code: string[] = [] + i++ + while (i < lines.length && !lines[i].startsWith('```')) { code.push(lines[i]); i++ } + nodes.push( +
+          {lang && 
{lang}
} + {code.join('\n')} +
+ ) + } else if (line.startsWith('# ')) { + nodes.push(
{inlineFmt(line.slice(2))}
) + } else if (line.startsWith('## ')) { + nodes.push(
{inlineFmt(line.slice(3))}
) + } else if (line.startsWith('### ')) { + nodes.push(
{inlineFmt(line.slice(4))}
) + } else if (line.startsWith('- ') || line.startsWith('* ')) { + nodes.push( +
+ + {inlineFmt(line.slice(2))} +
+ ) + } else if (/^\d+\.\s/.test(line)) { + const m = line.match(/^(\d+)\.\s(.*)/) + nodes.push( +
+ {m?.[1]}. + {inlineFmt(m?.[2] ?? '')} +
+ ) + } else if (line.startsWith('> ')) { + nodes.push( +
+ {inlineFmt(line.slice(2))} +
+ ) + } else if (line === '---' || line === '***') { + nodes.push(
) + } else if (line.trim() === '') { + nodes.push(
) + } else { + nodes.push( +
+ {inlineFmt(line)} +
+ ) + } + i++ + } + return <>{nodes} +} + +type NoteState = 'semi' | 'expanded' | 'collapsed' + function NoteCard({ note, onEdit, onDelete, onAddPhoto, onAddAudio, onAddVideo, onDeleteAtt }: { note: Note onEdit: () => void @@ -30,6 +122,7 @@ function NoteCard({ note, onEdit, onDelete, onAddPhoto, onAddAudio, onAddVideo, onAddVideo: (file: File) => void onDeleteAtt: (attId: string) => void }) { + const [state, setState] = useState('semi') const photoRef = useRef(null) const audioRef = useRef(null) const videoRef = useRef(null) @@ -41,10 +134,16 @@ function NoteCard({ note, onEdit, onDelete, onAddPhoto, onAddAudio, onAddVideo, const audios = note.attachments.filter(a => a.file_type === 'audio') const videos = note.attachments.filter(a => a.file_type === 'video') + function cycleState() { + setState(s => s === 'semi' ? 'expanded' : s === 'expanded' ? 'collapsed' : 'semi') + } + + const stateIcon = state === 'semi' ? 'fa-chevron-down' : state === 'expanded' ? 'fa-minus' : 'fa-chevron-right' + const stateTitle = state === 'semi' ? 'Tout afficher' : state === 'expanded' ? 'Réduire' : 'Développer' + async function startRecord() { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) - // Choisir le format supporté par le navigateur (Safari → mp4, Chrome/Firefox → webm) const mimeType = ['audio/webm', 'audio/mp4', 'audio/ogg'].find(t => MediaRecorder.isTypeSupported(t)) ?? '' const recorder = mimeType ? new MediaRecorder(stream, { mimeType }) : new MediaRecorder(stream) chunksRef.current = [] @@ -59,9 +158,7 @@ function NoteCard({ note, onEdit, onDelete, onAddPhoto, onAddAudio, onAddVideo, recorder.start() recorderRef.current = recorder setRecording(true) - } catch { - // micro non disponible ou permission refusée - } + } catch { /* micro non disponible */ } } function stopRecord() { @@ -69,130 +166,154 @@ function NoteCard({ note, onEdit, onDelete, onAddPhoto, onAddAudio, onAddVideo, setRecording(false) } - return ( -
- {/* En-tête */} -
-
- {note.title && ( -
- {note.title} -
- )} -
- {note.content.length > 200 ? note.content.slice(0, 200) + '…' : note.content} -
-
-
+ const toggleBtn = ( + + ) - {/* Photos */} + const metaLine = ( +
+ {formatDate(note.created_at)} + {note.category && {note.category}} + {note.tags.map(t => {t})} + {images.length > 0 && } + {audios.length > 0 && } + {videos.length > 0 && } + {note.gps_lat != null && ( + + )} +
+ ) + + const mediaSection = ( + <> {images.length > 0 && (
{images.map(img => (
- - + +
))}
)} - - {/* Audios */} {audios.length > 0 && (
{audios.map(aud => (
))}
)} - - {/* Vidéos */} {videos.length > 0 && ( -
+
{videos.map(vid => (
-
))}
)} + + ) - {/* Méta */} -
- {formatDate(note.created_at)} - {note.category && ( - {note.category} - )} - {note.tags.map(t => ( - {t} - ))} - {images.length > 0 && } - {audios.length > 0 && } - {videos.length > 0 && } - {note.gps_lat != null && ( - - )} + const actionButtons = ( +
+ { 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 = '' }} /> + + + { const f = e.target.files?.[0]; if (f) onAddVideo(f); e.target.value = '' }} /> + + +
+ + +
+ ) + + // ─── COLLAPSED ─────────────────────────────────────────────────────────────── + if (state === 'collapsed') { + return ( +
+
+
+ + {note.title || note.content.slice(0, 60).replace(/\n/g, ' ')} + + + {formatDate(note.created_at)} + +
+ {toggleBtn} +
+ ) + } - {/* 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 = '' }} /> - - - { const f = e.target.files?.[0]; if (f) onAddVideo(f); e.target.value = '' }} /> - - -
- - + // ─── SEMI (défaut) ─────────────────────────────────────────────────────────── + if (state === 'semi') { + return ( +
+
+
+ {note.title && ( +
+ {note.title} +
+ )} +
+ {note.content} +
+
+ {toggleBtn} +
+ {metaLine} + {actionButtons}
+ ) + } + + // ─── EXPANDED ──────────────────────────────────────────────────────────────── + return ( +
+
+
+ {note.title && ( +
+ {note.title} +
+ )} +
+ {toggleBtn} +
+
{renderMarkdown(note.content)}
+ {mediaSection} + {metaLine} + {actionButtons}
) } +// ─── PAGE ───────────────────────────────────────────────────────────────────── + export default function NotesPage() { const [notes, setNotes] = useState([]) const [loading, setLoading] = useState(true) @@ -209,13 +330,7 @@ export default function NotesPage() { ) return () => setActionButton(null) @@ -257,62 +372,56 @@ export default function NotesPage() { async function handleDelete(id: string) { if (!confirm('Supprimer cette note ?')) return - try { - await deleteNote(id) - void load() - } catch { - setError('Erreur lors de la suppression') - } + 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') - } + 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') - } + try { await addAttachment(noteId, file); void load() } + catch { setError('Erreur upload audio') } } async function handleAddVideo(noteId: string, file: File) { - try { - await addAttachment(noteId, file) - void load() - } catch { - setError('Erreur upload vidéo') - } + try { await addAttachment(noteId, file); void load() } + catch { setError('Erreur upload vidéo') } } async function handleDeleteAtt(noteId: string, attId: string) { - try { - await deleteAttachment(noteId, attId) - void load() - } catch { - setError('Erreur suppression pièce jointe') - } + try { await deleteAttachment(noteId, attId); void load() } + catch { setError('Erreur suppression pièce jointe') } } const hasActiveFilters = filters.has_photo || filters.has_audio || filters.has_video || filters.has_gps + const noteGrid = (cols: string) => ( +
+ {notes.map(note => ( + setEditingNote(note)} + onDelete={() => void handleDelete(note.id)} + onAddPhoto={f => void handleAddPhoto(note.id, f)} + onAddAudio={f => void handleAddAudio(note.id, f)} + onAddVideo={f => void handleAddVideo(note.id, f)} + onDeleteAtt={attId => void handleDeleteAtt(note.id, attId)} + /> + ))} +
+ ) + return (
- {/* En-tête */} -
-

- Notes -

+
+

Notes

- {/* Barre de recherche + filtres */} + {/* Barre recherche + filtres */}
handleSearchChange(e.target.value)} /> - {/* Filtres rapides */} {([ { key: 'has_photo', icon: 'fa-image', label: 'Photo' }, { key: 'has_audio', icon: 'fa-microphone', label: 'Audio' }, @@ -332,17 +440,9 @@ export default function NotesPage() { ) })} @@ -355,76 +455,32 @@ export default function NotesPage() {
{error && ( -

- {error} -

+

{error}

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

Chargement…

+ {loading &&

Chargement…

} + + {!loading && notes.length === 0 && ( +

Aucune note

)} - {/* 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)} - onAddVideo={f => void handleAddVideo(note.id, f)} - onDeleteAtt={attId => void handleDeleteAtt(note.id, attId)} - /> - ))} -
+ {noteGrid('1fr')}
- - {/* 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)} - onAddVideo={f => void handleAddVideo(note.id, f)} - onDeleteAtt={attId => void handleDeleteAtt(note.id, attId)} - /> - ))} -
+ {noteGrid('repeat(auto-fill, minmax(320px, 1fr))')}
-
) }