diff --git a/public/css/components.css b/public/css/components.css index 6535892..e72bb9c 100644 --- a/public/css/components.css +++ b/public/css/components.css @@ -8,18 +8,32 @@ background: var(--color-card); border: 1px solid var(--color-border); border-radius: var(--radius-lg); - padding: var(--spacing-lg); - cursor: pointer; transition: all var(--transition-base); box-shadow: var(--shadow-sm); } -.note-card:hover { +.note-card-compact { + padding: var(--spacing-lg); + cursor: pointer; +} + +.note-card-compact:hover { box-shadow: var(--shadow-hover); transform: translateY(-2px); border-color: var(--color-accent); } +.note-card.expanded { + grid-column: 1 / -1; + cursor: default; + box-shadow: var(--shadow-lg); + border-color: var(--color-accent); +} + +.note-card.expanded:hover { + transform: none; +} + .note-card-title { font-size: var(--font-size-lg); font-weight: var(--font-weight-semibold); @@ -71,6 +85,296 @@ margin-bottom: var(--spacing-md); } +.note-todos-preview { + margin-top: var(--spacing-md); + margin-bottom: var(--spacing-sm); + display: flex; + flex-direction: column; + gap: var(--spacing-xs); +} + +.todo-preview { + display: flex; + align-items: center; + gap: var(--spacing-sm); + font-size: var(--font-size-xs); + color: var(--color-text-secondary); +} + +.todo-preview.completed .todo-text-preview { + text-decoration: line-through; + opacity: 0.6; +} + +.todo-checkbox-preview { + font-size: var(--font-size-sm); +} + +.todo-more { + font-size: var(--font-size-xs); + color: var(--color-text-secondary); + font-style: italic; + margin-top: var(--spacing-xs); +} + +/* Inline expansion styles */ +.note-card-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--spacing-md); + padding: var(--spacing-lg); + border-bottom: 1px solid var(--color-border); +} + +.note-title-input-inline { + flex: 1; + border: none; + font-size: var(--font-size-xl); + font-weight: var(--font-weight-bold); + color: var(--color-text-primary); + background: transparent; + padding: var(--spacing-sm) 0; +} + +.note-title-input-inline:focus { + outline: none; + background: var(--color-background); + padding: var(--spacing-sm) var(--spacing-md); + border-radius: var(--radius-sm); +} + +.note-actions { + display: flex; + gap: var(--spacing-xs); + flex-shrink: 0; +} + +.btn-icon-small { + width: 32px; + height: 32px; + padding: 6px; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + background: transparent; + color: var(--color-text-secondary); + cursor: pointer; + transition: all var(--transition-fast); + display: flex; + align-items: center; + justify-content: center; +} + +.btn-icon-small:hover { + background: var(--color-hover); + color: var(--color-text-primary); + border-color: var(--color-accent); +} + +.btn-icon-small svg { + width: 100%; + height: 100%; +} + +.note-content-textarea-inline { + width: 100%; + min-height: 200px; + padding: var(--spacing-lg); + border: none; + font-size: var(--font-size-base); + color: var(--color-text-primary); + line-height: 1.8; + resize: vertical; + font-family: var(--font-family); + background: transparent; +} + +.note-content-textarea-inline:focus { + outline: none; + background: var(--color-background); +} + +.note-image-container-inline { + padding: 0 var(--spacing-lg); + position: relative; + margin-bottom: var(--spacing-md); +} + +.btn-remove-image-inline { + position: absolute; + top: var(--spacing-sm); + right: calc(var(--spacing-lg) + var(--spacing-sm)); + width: 28px; + height: 28px; + padding: 4px; + border: none; + border-radius: var(--radius-full); + background: rgba(0, 0, 0, 0.7); + color: white; + cursor: pointer; + font-size: var(--font-size-lg); + line-height: 1; + transition: all var(--transition-fast); +} + +.btn-remove-image-inline:hover { + background: var(--color-danger); +} + +.note-todos-inline { + padding: var(--spacing-lg); + border-top: 1px solid var(--color-border); +} + +.note-todos-inline h4 { + font-size: var(--font-size-base); + font-weight: var(--font-weight-semibold); + color: var(--color-text-primary); + margin-bottom: var(--spacing-md); +} + +.todos-list-inline { + display: flex; + flex-direction: column; + gap: var(--spacing-sm); + margin-bottom: var(--spacing-md); +} + +.todo-item-inline { + display: flex; + align-items: center; + gap: var(--spacing-md); + padding: var(--spacing-sm); + background: var(--color-background); + border-radius: var(--radius-sm); + transition: all var(--transition-fast); +} + +.todo-item-inline:hover { + background: var(--color-hover); +} + +.todo-checkbox-inline { + width: 18px; + height: 18px; + border: 2px solid var(--color-border); + border-radius: 4px; + cursor: pointer; + transition: all var(--transition-fast); + flex-shrink: 0; +} + +.todo-checkbox-inline:checked { + background: var(--color-accent); + border-color: var(--color-accent); +} + +.todo-text-inline { + flex: 1; + font-size: var(--font-size-sm); + color: var(--color-text-primary); + line-height: 1.5; +} + +.todo-text-inline.completed { + text-decoration: line-through; + color: var(--color-text-secondary); +} + +.todo-delete-inline { + width: 24px; + height: 24px; + padding: 4px; + border: none; + border-radius: var(--radius-sm); + background: transparent; + color: var(--color-text-secondary); + cursor: pointer; + opacity: 0; + transition: all var(--transition-fast); + flex-shrink: 0; +} + +.todo-item-inline:hover .todo-delete-inline { + opacity: 1; +} + +.todo-delete-inline:hover { + background: var(--color-danger); + color: white; +} + +.no-todos { + text-align: center; + color: var(--color-text-secondary); + font-size: var(--font-size-sm); + padding: var(--spacing-md); + font-style: italic; +} + +.btn-add-todo-inline { + display: flex; + align-items: center; + gap: var(--spacing-sm); + padding: var(--spacing-sm) var(--spacing-md); + border: 1px dashed var(--color-border); + border-radius: var(--radius-sm); + background: transparent; + color: var(--color-text-secondary); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + cursor: pointer; + transition: all var(--transition-fast); + width: 100%; +} + +.btn-add-todo-inline:hover { + background: var(--color-hover); + border-color: var(--color-accent); + color: var(--color-text-primary); +} + +.note-footer-inline { + display: flex; + align-items: center; + gap: var(--spacing-md); + padding: var(--spacing-lg); + border-top: 1px solid var(--color-border); + background: var(--color-background); +} + +.note-meta-inline { + flex: 1; + font-size: var(--font-size-xs); + color: var(--color-text-secondary); +} + +.btn-add-image-inline { + width: 36px; + height: 36px; + padding: 8px; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + background: transparent; + color: var(--color-text-secondary); + cursor: pointer; + transition: all var(--transition-fast); + display: flex; + align-items: center; + justify-content: center; +} + +.btn-add-image-inline:hover { + background: var(--color-hover); + color: var(--color-text-primary); + border-color: var(--color-accent); +} + +.btn-add-image-inline svg { + width: 100%; + height: 100%; +} + /* ============================================ TODO BOX ============================================ */ diff --git a/public/css/layout.css b/public/css/layout.css index 1602f71..ff11c85 100644 --- a/public/css/layout.css +++ b/public/css/layout.css @@ -244,6 +244,8 @@ body { align-items: center; justify-content: space-between; margin-bottom: var(--spacing-lg); + gap: var(--spacing-lg); + flex-wrap: wrap; } .section-title { @@ -252,6 +254,51 @@ body { color: var(--color-text-primary); } +.section-actions { + display: flex; + align-items: center; + gap: var(--spacing-md); +} + +.view-filters { + display: flex; + gap: var(--spacing-xs); + background: var(--color-background); + border-radius: var(--radius-md); + padding: 4px; +} + +.view-filter { + display: flex; + align-items: center; + gap: var(--spacing-xs); + padding: var(--spacing-sm) var(--spacing-md); + border: none; + border-radius: var(--radius-sm); + background: transparent; + color: var(--color-text-secondary); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + cursor: pointer; + transition: all var(--transition-fast); +} + +.view-filter svg { + width: 16px; + height: 16px; +} + +.view-filter:hover { + background: var(--color-hover); + color: var(--color-text-primary); +} + +.view-filter.active { + background: var(--color-card); + color: var(--color-accent); + box-shadow: var(--shadow-xs); +} + .btn-new-note { display: flex; align-items: center; diff --git a/public/js/complete-app.js b/public/js/complete-app.js index a525100..ad8f051 100644 --- a/public/js/complete-app.js +++ b/public/js/complete-app.js @@ -174,8 +174,10 @@ const state = { notes: [], todos: [], currentNote: null, + expandedNoteId: null, filter: 'all', searchQuery: '', + view: 'active', user: null }; @@ -241,7 +243,8 @@ async function initAuth() { // ==================== NOTES ==================== async function loadNotes() { try { - const response = await api.get('/api/notes'); + const archived = state.view === 'archived'; + const response = await api.get(`/api/notes?archived=${archived}`); // S'assurer que response est un tableau state.notes = Array.isArray(response) ? response : []; renderNotes(); @@ -287,37 +290,369 @@ function renderNotes() { function createNoteCard(note) { const card = document.createElement('div'); - card.className = 'note-card'; - card.onclick = () => openNoteModal(note.id); + const isExpanded = state.expandedNoteId === note.id; + card.className = `note-card ${isExpanded ? 'expanded' : ''}`; + card.dataset.noteId = note.id; - let html = `
${escapeHtml(note.title)}
`; + if (isExpanded) { + // Mode édition inline + card.innerHTML = ` +
+ +
+ + + +
+
+ + ${note.image_filename ? ` +
+ + +
+ ` : ''} +
+

Todos

+
+ ${renderInlineTodos(note.todos || [])} +
+ +
+ + `; - if (note.content) { - html += `
${escapeHtml(utils.truncate(note.content))}
`; + // Auto-save sur changement + setTimeout(() => { + const titleInput = card.querySelector('.note-title-input-inline'); + const contentTextarea = card.querySelector('.note-content-textarea-inline'); + if (titleInput) titleInput.oninput = utils.debounce(() => saveNoteInline(note.id), 1000); + if (contentTextarea) contentTextarea.oninput = utils.debounce(() => saveNoteInline(note.id), 1000); + }, 0); + } else { + // Mode compact + let html = ` +
+
${escapeHtml(note.title)}
+ `; + + if (note.content) { + html += `
${escapeHtml(utils.truncate(note.content, 100))}
`; + } + + if (note.image_filename) { + html += ``; + } + + // Afficher les todos + if (note.todos && note.todos.length > 0) { + const displayTodos = note.todos.slice(0, 3); + html += `
`; + displayTodos.forEach(todo => { + html += `
+ ${todo.completed ? '☑' : '☐'} + ${escapeHtml(todo.text)} +
`; + }); + if (note.todos.length > 3) { + html += `
+${note.todos.length - 3} autre(s)
`; + } + html += `
`; + } + + html += ` +
+ ${utils.formatDate(note.updated_at)} + ${note.todos_count > 0 ? `${note.todos_completed}/${note.todos_count}` : ''} +
+
+ `; + + card.innerHTML = html; } - if (note.image_filename) { - html += ``; - } - - html += `
`; - html += `${utils.formatDate(note.updated_at)}`; - if (note.todos_count > 0) { - html += `${note.todos_count} tasks`; - } - html += `
`; - - card.innerHTML = html; return card; } +function renderInlineTodos(todos) { + if (!todos || todos.length === 0) return '

Aucune tâche

'; + return todos.map(todo => ` +
+ + ${escapeHtml(todo.text)} + +
+ `).join(''); +} + function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } -// ==================== MODAL ==================== +// ==================== INLINE NOTE EXPANSION ==================== +function expandNote(noteId) { + state.expandedNoteId = noteId; + renderNotes(); +} + +function collapseNote() { + state.expandedNoteId = null; + renderNotes(); +} + +async function saveNoteInline(noteId) { + const card = document.querySelector(`.note-card[data-note-id="${noteId}"]`); + if (!card) return; + + const titleInput = card.querySelector('.note-title-input-inline'); + const contentTextarea = card.querySelector('.note-content-textarea-inline'); + + if (!titleInput) return; + + try { + await api.put(`/api/notes/${noteId}`, { + title: titleInput.value, + content: contentTextarea ? contentTextarea.value : '' + }); + + // Update state without full reload + const noteIndex = state.notes.findIndex(n => n.id === noteId); + if (noteIndex !== -1) { + state.notes[noteIndex].title = titleInput.value; + state.notes[noteIndex].content = contentTextarea ? contentTextarea.value : ''; + state.notes[noteIndex].updated_at = new Date().toISOString(); + } + } catch (error) { + console.error('Erreur sauvegarde note:', error); + } +} + +async function archiveNote(noteId, archived) { + try { + await api.put(`/api/notes/${noteId}/archive`, { archived }); + collapseNote(); + await loadNotes(); + } catch (error) { + console.error('Erreur archivage note:', error); + alert('Erreur lors de l\'archivage de la note'); + } +} + +async function deleteNoteInline(noteId) { + const confirmed = await confirmDialog.show({ + icon: '🗑️', + title: 'Supprimer cette note', + message: 'Êtes-vous sûr de vouloir supprimer cette note ?\n\nTous les todos associés seront également supprimés.', + okText: 'Supprimer' + }); + + if (!confirmed) return; + + try { + await api.delete(`/api/notes/${noteId}`); + collapseNote(); + await loadNotes(); + } catch (error) { + console.error('Erreur suppression note:', error); + alert('Erreur lors de la suppression de la note'); + } +} + +async function addNoteTodoInline(noteId) { + const text = prompt('Texte du todo:'); + if (!text || !text.trim()) return; + + try { + await api.post(`/api/notes/${noteId}/todos`, { text: text.trim() }); + + // Recharger seulement cette note + const note = await api.get(`/api/notes/${noteId}`); + const noteIndex = state.notes.findIndex(n => n.id === noteId); + if (noteIndex !== -1) { + state.notes[noteIndex] = note; + } + + // Re-render la liste des todos inline + const todosList = document.getElementById(`todos-${noteId}`); + if (todosList) { + todosList.innerHTML = renderInlineTodos(note.todos || []); + } + } catch (error) { + console.error('Erreur ajout todo:', error); + alert('Erreur lors de l\'ajout du todo'); + } +} + +async function toggleNoteTodoInline(todoId, completed) { + try { + await api.put(`/api/notes/todos/${todoId}`, { completed }); + // Update UI optimistically + const todoItem = document.querySelector(`.todo-item-inline[data-todo-inline-id="${todoId}"]`); + if (todoItem) { + const textSpan = todoItem.querySelector('.todo-text-inline'); + if (textSpan) { + if (completed) { + textSpan.classList.add('completed'); + } else { + textSpan.classList.remove('completed'); + } + } + } + } catch (error) { + console.error('Erreur toggle todo:', error); + } +} + +async function deleteNoteTodoInline(todoId) { + const confirmed = await confirmDialog.show({ + icon: '🗑️', + title: 'Supprimer ce todo', + message: 'Êtes-vous sûr de vouloir supprimer ce todo ?', + okText: 'Supprimer' + }); + + if (!confirmed) return; + + try { + await api.delete(`/api/notes/todos/${todoId}`); + // Remove from DOM + const todoItem = document.querySelector(`.todo-item-inline[data-todo-inline-id="${todoId}"]`); + if (todoItem) { + todoItem.remove(); + } + } catch (error) { + console.error('Erreur suppression todo:', error); + } +} + +function triggerImageUpload(noteId) { + const imageInput = document.getElementById('imageInput'); + if (imageInput) { + imageInput.dataset.noteId = noteId; + imageInput.click(); + } +} + +async function removeImageInline(noteId) { + const confirmed = await confirmDialog.show({ + icon: '🖼️', + title: 'Supprimer l\'image', + message: 'Êtes-vous sûr de vouloir supprimer cette image ?', + okText: 'Supprimer' + }); + + if (!confirmed) return; + + try { + await api.delete(`/api/notes/${noteId}/image`); + + // Update state + const noteIndex = state.notes.findIndex(n => n.id === noteId); + if (noteIndex !== -1) { + state.notes[noteIndex].image_filename = null; + } + + // Re-render expanded card + renderNotes(); + } catch (error) { + console.error('Erreur suppression image:', error); + alert('Erreur lors de la suppression de l\'image'); + } +} + +async function handleImageUploadInline(event) { + const file = event.target.files[0]; + const noteId = event.target.dataset.noteId; + + if (!file || !noteId) return; + + const formData = new FormData(); + formData.append('image', file); + + try { + const result = await api.uploadFile(`/api/notes/${noteId}/image`, formData); + + // Update state + const noteIndex = state.notes.findIndex(n => n.id === parseInt(noteId)); + if (noteIndex !== -1) { + state.notes[noteIndex].image_filename = result.filename; + } + + // Re-render expanded card + renderNotes(); + } catch (error) { + console.error('Erreur upload image:', error); + alert('Erreur lors de l\'upload de l\'image'); + } + + // Reset input + event.target.value = ''; + event.target.dataset.noteId = ''; +} + +async function createNewNote() { + try { + const newNote = await api.post('/api/notes', { + title: 'Nouvelle note', + content: '' + }); + + // Reload notes + await loadNotes(); + + // Expand the new note + state.expandedNoteId = newNote.id; + renderNotes(); + + // Focus title input + setTimeout(() => { + const titleInput = document.querySelector('.note-title-input-inline'); + if (titleInput) { + titleInput.focus(); + titleInput.select(); + } + }, 100); + } catch (error) { + console.error('Erreur création note:', error); + alert('Erreur lors de la création de la note'); + } +} + +function setNoteView(view) { + state.view = view; + document.querySelectorAll('.view-filter').forEach(btn => { + btn.classList.toggle('active', btn.dataset.view === view); + }); + loadNotes(); +} + +// ==================== MODAL (OLD - DEPRECATED) ==================== let saveTimeout; let currentNoteId = null; @@ -838,9 +1173,14 @@ async function init() { // Event listeners const newNoteBtn = document.getElementById('newNoteBtn'); if (newNoteBtn) { - newNoteBtn.addEventListener('click', () => openNoteModal()); + newNoteBtn.addEventListener('click', createNewNote); } + // View filters (Active/Archived) + document.querySelectorAll('.view-filter').forEach(btn => { + btn.addEventListener('click', () => setNoteView(btn.dataset.view)); + }); + // Admin button const adminBtn = document.getElementById('adminBtn'); if (adminBtn) { @@ -891,7 +1231,7 @@ async function init() { const imageInput = document.getElementById('imageInput'); if (imageInput) { - imageInput.addEventListener('change', handleImageUpload); + imageInput.addEventListener('change', handleImageUploadInline); } const removeImage = document.getElementById('removeImage'); @@ -986,11 +1326,91 @@ async function init() { }); } + // Event delegation for notes grid + const notesGrid = document.getElementById('notesGrid'); + if (notesGrid) { + // Click on compact note to expand + notesGrid.addEventListener('click', (e) => { + const compactNote = e.target.closest('.note-card-compact'); + if (compactNote) { + const noteId = parseInt(compactNote.dataset.noteId); + expandNote(noteId); + return; + } + + // Collapse note button + if (e.target.closest('.btn-collapse-note')) { + collapseNote(); + return; + } + + // Archive note button + const archiveBtn = e.target.closest('.btn-archive-note'); + if (archiveBtn) { + const noteId = parseInt(archiveBtn.dataset.noteId); + const archived = archiveBtn.dataset.archived === 'true'; + archiveNote(noteId, !archived); + return; + } + + // Delete note button + const deleteBtn = e.target.closest('.btn-delete-note-inline'); + if (deleteBtn) { + const noteId = parseInt(deleteBtn.dataset.noteId); + deleteNoteInline(noteId); + return; + } + + // Add image button + const addImageBtn = e.target.closest('.btn-add-image-inline'); + if (addImageBtn) { + const noteId = parseInt(addImageBtn.dataset.noteId); + triggerImageUpload(noteId); + return; + } + + // Remove image button + const removeImageBtn = e.target.closest('.btn-remove-image-inline'); + if (removeImageBtn) { + const noteId = parseInt(removeImageBtn.dataset.noteId); + removeImageInline(noteId); + return; + } + + // Add todo button + const addTodoBtn = e.target.closest('.btn-add-todo-inline'); + if (addTodoBtn) { + const noteId = parseInt(addTodoBtn.dataset.noteId); + addNoteTodoInline(noteId); + return; + } + + // Delete todo inline button + if (e.target.classList.contains('todo-delete-inline')) { + const todoId = parseInt(e.target.dataset.todoInlineId); + deleteNoteTodoInline(todoId); + return; + } + }); + + // Checkbox change for inline todos + notesGrid.addEventListener('change', (e) => { + if (e.target.classList.contains('todo-checkbox-inline')) { + const todoId = parseInt(e.target.dataset.todoInlineId); + toggleNoteTodoInline(todoId, e.target.checked); + } + }); + } + // Close modal on Escape document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { - closeNoteModal(); - document.getElementById('adminModal').style.display = 'none'; + if (state.expandedNoteId) { + collapseNote(); + } else { + closeNoteModal(); + document.getElementById('adminModal').style.display = 'none'; + } } }); }