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';
+ }
}
});
}