mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-11 17:29:37 +02:00
UX: Refonte complète - Notes en expansion inline (Google Keep style)
Implémentation d'un système d'expansion inline des notes au lieu de modals: - Les notes s'agrandissent directement dans la grille au clic - Mode compact: prévisualisation avec titre, contenu, image, premiers 3 todos - Mode étendu: édition complète avec titre, contenu, todos, image, actions - Auto-save avec debounce (1s) sur les modifications - Filtres Actives/Archivées pour organiser les notes - Système d'archivage avec endpoint API dédié Fonctionnalités inline: - expandNote(noteId): agrandir une note - collapseNote(): réduire la note étendue - saveNoteInline(noteId): sauvegarde auto avec debounce - archiveNote(noteId, archived): archiver/désarchiver - deleteNoteInline(noteId): supprimer depuis l'inline - Gestion complète des todos inline (add, toggle, delete) - Upload/suppression d'images inline - Fermeture avec Escape Event delegation pour performances: - Tous les événements gérés via delegation sur #notesGrid - Suppression de tous les attributs onclick/onchange (CSP compliant) - Click handlers: expand, collapse, archive, delete, image, todos - Change handlers: checkbox todos inline CSS moderne avec shadows: - .note-card.expanded: carte agrandie sur toute la largeur - Styles inline pour titre, contenu, todos, image, footer - Transitions fluides et hover effects - View filters avec background pills - Responsive grid layout Backend prêt: - GET /api/notes?archived=true/false - PUT /api/notes/:id/archive - Todos chargés avec chaque note - Migration auto colonne 'archived'
This commit is contained in:
3 files changed
+797
-26
No files matched your search
+307
-3
@@ -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
|
||||
============================================ */
|
||||
|
||||
@@ -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;
|
||||
|
||||
+443
-23
@@ -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 = `<div class="note-card-title">${escapeHtml(note.title)}</div>`;
|
||||
if (isExpanded) {
|
||||
// Mode édition inline
|
||||
card.innerHTML = `
|
||||
<div class="note-card-header">
|
||||
<input type="text" class="note-title-input-inline" value="${escapeHtml(note.title)}" placeholder="Titre" data-note-id="${note.id}">
|
||||
<div class="note-actions">
|
||||
<button class="btn-icon-small btn-archive-note" data-note-id="${note.id}" data-archived="${note.archived}" title="${note.archived ? 'Désarchiver' : 'Archiver'}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="21 8 21 21 3 21 3 8"></polyline>
|
||||
<rect x="1" y="3" width="22" height="5"></rect>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-icon-small btn-delete-note-inline" data-note-id="${note.id}" title="Supprimer">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="3 6 5 6 21 6"></polyline>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-icon-small btn-collapse-note" title="Fermer">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<textarea class="note-content-textarea-inline" placeholder="Contenu..." data-note-id="${note.id}">${escapeHtml(note.content || '')}</textarea>
|
||||
${note.image_filename ? `
|
||||
<div class="note-image-container-inline">
|
||||
<img src="/uploads/${note.image_filename}" class="note-card-image" alt="">
|
||||
<button class="btn-remove-image-inline" data-note-id="${note.id}">✕</button>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="note-todos-inline">
|
||||
<h4>Todos</h4>
|
||||
<div class="todos-list-inline" id="todos-${note.id}">
|
||||
${renderInlineTodos(note.todos || [])}
|
||||
</div>
|
||||
<button class="btn-add-todo-inline" data-note-id="${note.id}">+ Ajouter une tâche</button>
|
||||
</div>
|
||||
<div class="note-footer-inline">
|
||||
<button class="btn-add-image-inline" data-note-id="${note.id}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
||||
<polyline points="21 15 16 10 5 21"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="note-meta-inline">${utils.formatDate(note.updated_at)}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (note.content) {
|
||||
html += `<div class="note-card-content">${escapeHtml(utils.truncate(note.content))}</div>`;
|
||||
// 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 = `
|
||||
<div class="note-card-compact" data-note-id="${note.id}">
|
||||
<div class="note-card-title">${escapeHtml(note.title)}</div>
|
||||
`;
|
||||
|
||||
if (note.content) {
|
||||
html += `<div class="note-card-preview">${escapeHtml(utils.truncate(note.content, 100))}</div>`;
|
||||
}
|
||||
|
||||
if (note.image_filename) {
|
||||
html += `<img src="/uploads/${note.image_filename}" class="note-card-image" alt="">`;
|
||||
}
|
||||
|
||||
// Afficher les todos
|
||||
if (note.todos && note.todos.length > 0) {
|
||||
const displayTodos = note.todos.slice(0, 3);
|
||||
html += `<div class="note-todos-preview">`;
|
||||
displayTodos.forEach(todo => {
|
||||
html += `<div class="todo-preview ${todo.completed ? 'completed' : ''}">
|
||||
<span class="todo-checkbox-preview">${todo.completed ? '☑' : '☐'}</span>
|
||||
<span class="todo-text-preview">${escapeHtml(todo.text)}</span>
|
||||
</div>`;
|
||||
});
|
||||
if (note.todos.length > 3) {
|
||||
html += `<div class="todo-more">+${note.todos.length - 3} autre(s)</div>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="note-card-meta">
|
||||
<span class="note-card-date">${utils.formatDate(note.updated_at)}</span>
|
||||
${note.todos_count > 0 ? `<span class="note-card-todos-count">${note.todos_completed}/${note.todos_count}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
card.innerHTML = html;
|
||||
}
|
||||
|
||||
if (note.image_filename) {
|
||||
html += `<img src="/uploads/${note.image_filename}" class="note-card-image" alt="">`;
|
||||
}
|
||||
|
||||
html += `<div class="note-card-meta">`;
|
||||
html += `<span>${utils.formatDate(note.updated_at)}</span>`;
|
||||
if (note.todos_count > 0) {
|
||||
html += `<span class="note-card-badge">${note.todos_count} tasks</span>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
|
||||
card.innerHTML = html;
|
||||
return card;
|
||||
}
|
||||
|
||||
function renderInlineTodos(todos) {
|
||||
if (!todos || todos.length === 0) return '<p class="no-todos">Aucune tâche</p>';
|
||||
return todos.map(todo => `
|
||||
<div class="todo-item-inline" data-todo-inline-id="${todo.id}">
|
||||
<input type="checkbox" class="todo-checkbox-inline" ${todo.completed ? 'checked' : ''} data-todo-inline-id="${todo.id}">
|
||||
<span class="todo-text-inline ${todo.completed ? 'completed' : ''}">${escapeHtml(todo.text)}</span>
|
||||
<button class="todo-delete-inline" data-todo-inline-id="${todo.id}">✕</button>
|
||||
</div>
|
||||
`).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';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user