mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-24 07:38:17 +02:00
Merge pull request #10 from R0m1k3/claude/dockerized-notes-todo-app-011CV1roGRbnm21wgtB2LMxP
Claude/dockerized notes todo app 011 cv1ro g rbnm21wgt b2 l mx p
This commit is contained in:
3 files changed
+90
-83
No files matched your search
+13
-1
@@ -81,7 +81,7 @@ function initDatabase() {
|
|||||||
|
|
||||||
logger.info('✓ Tables de base de données créées avec succès');
|
logger.info('✓ Tables de base de données créées avec succès');
|
||||||
|
|
||||||
// Migration: Ajouter image_filename si elle n'existe pas
|
// Migrations automatiques
|
||||||
db.all("PRAGMA table_info(notes)", (err, columns) => {
|
db.all("PRAGMA table_info(notes)", (err, columns) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
logger.error('Erreur lors de la vérification de la structure de la table notes:', err);
|
logger.error('Erreur lors de la vérification de la structure de la table notes:', err);
|
||||||
@@ -89,6 +89,7 @@ function initDatabase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hasImageFilename = columns.some(col => col.name === 'image_filename');
|
const hasImageFilename = columns.some(col => col.name === 'image_filename');
|
||||||
|
const hasArchived = columns.some(col => col.name === 'archived');
|
||||||
|
|
||||||
if (!hasImageFilename) {
|
if (!hasImageFilename) {
|
||||||
logger.info('Migration: Ajout de la colonne image_filename...');
|
logger.info('Migration: Ajout de la colonne image_filename...');
|
||||||
@@ -100,6 +101,17 @@ function initDatabase() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!hasArchived) {
|
||||||
|
logger.info('Migration: Ajout de la colonne archived...');
|
||||||
|
db.run('ALTER TABLE notes ADD COLUMN archived BOOLEAN DEFAULT 0', (err) => {
|
||||||
|
if (err) {
|
||||||
|
logger.error('Erreur lors de l\'ajout de la colonne archived:', err);
|
||||||
|
} else {
|
||||||
|
logger.info('✓ Colonne archived ajoutée avec succès');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Créer l'utilisateur admin par défaut si la table est vide
|
// Créer l'utilisateur admin par défaut si la table est vide
|
||||||
|
|||||||
+30
-77
@@ -84,20 +84,38 @@
|
|||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
<div class="content-container">
|
<div class="content-container">
|
||||||
<!-- Notes Grid -->
|
<!-- Notes Section -->
|
||||||
<div class="notes-section">
|
<div class="notes-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2 class="section-title">Notes</h2>
|
<h2 class="section-title">Notes</h2>
|
||||||
<button class="btn-new-note" id="newNoteBtn">
|
<div class="section-actions">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<div class="view-filters">
|
||||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
<button class="view-filter active" data-view="active">
|
||||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
</svg>
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/>
|
||||||
Nouvelle note
|
</svg>
|
||||||
</button>
|
Actives
|
||||||
|
</button>
|
||||||
|
<button class="view-filter" data-view="archived">
|
||||||
|
<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>
|
||||||
|
<line x1="10" y1="12" x2="14" y2="12"></line>
|
||||||
|
</svg>
|
||||||
|
Archivées
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button class="btn-new-note" id="newNoteBtn">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||||
|
</svg>
|
||||||
|
Nouvelle note
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="notes-grid" id="notesGrid">
|
<div class="notes-grid" id="notesGrid">
|
||||||
<!-- Notes will be inserted here -->
|
<!-- Notes will be inserted here dynamically -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -131,74 +149,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- Note Editor (Expanded in page) -->
|
|
||||||
<div class="note-editor-backdrop" id="noteEditorBackdrop" style="display: none;">
|
|
||||||
<div class="note-editor" id="noteEditor">
|
|
||||||
<div class="note-editor-header">
|
|
||||||
<button class="btn-close-editor" id="closeEditor">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
||||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<div class="note-editor-actions">
|
|
||||||
<button class="btn-icon" id="deleteNote" 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-primary" id="saveNote">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<polyline points="20 6 9 17 4 12"></polyline>
|
|
||||||
</svg>
|
|
||||||
Enregistrer
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input type="text" id="noteTitle" class="note-title-input" placeholder="Titre de la note">
|
|
||||||
|
|
||||||
<textarea id="noteContent" class="note-content-textarea" placeholder="Commencez à écrire..."></textarea>
|
|
||||||
|
|
||||||
<div id="noteImageContainer" class="note-image-container" style="display: none;">
|
|
||||||
<img id="noteImage" class="note-image" alt="Image de la note">
|
|
||||||
<button class="btn-remove-image" id="removeImage">
|
|
||||||
<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 class="note-todos-section">
|
|
||||||
<h3 class="note-section-title">Todos dans cette note</h3>
|
|
||||||
<div id="noteTodosList" class="note-todos-list">
|
|
||||||
<!-- Note todos will be inserted here -->
|
|
||||||
</div>
|
|
||||||
<button class="btn-add-note-todo" id="addNoteTodo">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
||||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
||||||
</svg>
|
|
||||||
Ajouter un todo
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="note-editor-footer">
|
|
||||||
<button class="btn-icon" id="addImageBtn" title="Ajouter une image">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<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>
|
|
||||||
<input type="file" id="imageInput" accept="image/*" style="display: none;">
|
|
||||||
<span id="noteMetadata" class="note-metadata"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modal Admin -->
|
<!-- Modal Admin -->
|
||||||
<div class="modal-backdrop" id="adminModal" style="display: none;">
|
<div class="modal-backdrop" id="adminModal" style="display: none;">
|
||||||
<div class="modal-content admin-modal">
|
<div class="modal-content admin-modal">
|
||||||
@@ -243,6 +193,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Hidden file input -->
|
||||||
|
<input type="file" id="imageInput" accept="image/*" style="display: none;">
|
||||||
|
|
||||||
<!-- JavaScript -->
|
<!-- JavaScript -->
|
||||||
<script src="/js/complete-app.js"></script>
|
<script src="/js/complete-app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+47
-5
@@ -39,20 +39,35 @@ const upload = multer({
|
|||||||
router.use(authenticateToken);
|
router.use(authenticateToken);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/notes
|
* GET /api/notes?archived=true/false
|
||||||
* Liste toutes les notes de l'utilisateur
|
* Liste toutes les notes de l'utilisateur
|
||||||
*/
|
*/
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const showArchived = req.query.archived === 'true';
|
||||||
|
const archivedFilter = showArchived ? 1 : 0;
|
||||||
|
|
||||||
const notes = await getAll(`
|
const notes = await getAll(`
|
||||||
SELECT
|
SELECT
|
||||||
n.id, n.title, n.content, n.image_filename,
|
n.id, n.title, n.content, n.image_filename, n.archived,
|
||||||
n.created_at, n.updated_at,
|
n.created_at, n.updated_at,
|
||||||
(SELECT COUNT(*) FROM note_todos WHERE note_id = n.id) as todos_count
|
(SELECT COUNT(*) FROM note_todos WHERE note_id = n.id) as todos_count,
|
||||||
|
(SELECT COUNT(*) FROM note_todos WHERE note_id = n.id AND completed = 1) as todos_completed
|
||||||
FROM notes n
|
FROM notes n
|
||||||
WHERE n.user_id = ?
|
WHERE n.user_id = ? AND n.archived = ?
|
||||||
ORDER BY n.updated_at DESC
|
ORDER BY n.updated_at DESC
|
||||||
`, [req.user.id]);
|
`, [req.user.id, archivedFilter]);
|
||||||
|
|
||||||
|
// Charger les todos pour chaque note
|
||||||
|
for (const note of notes) {
|
||||||
|
const todos = await getAll(`
|
||||||
|
SELECT id, text, completed, position
|
||||||
|
FROM note_todos
|
||||||
|
WHERE note_id = ?
|
||||||
|
ORDER BY position ASC, id ASC
|
||||||
|
`, [note.id]);
|
||||||
|
note.todos = todos || [];
|
||||||
|
}
|
||||||
|
|
||||||
res.json(notes || []);
|
res.json(notes || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -179,6 +194,33 @@ router.put('/:id',
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /api/notes/:id/archive
|
||||||
|
* Archiver ou désarchiver une note
|
||||||
|
*/
|
||||||
|
router.put('/:id/archive', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { archived } = req.body;
|
||||||
|
|
||||||
|
// Vérifier que la note appartient à l'utilisateur
|
||||||
|
const note = await getOne('SELECT id FROM notes WHERE id = ? AND user_id = ?', [req.params.id, req.user.id]);
|
||||||
|
if (!note) {
|
||||||
|
return res.status(404).json({ error: 'Note non trouvée' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await runQuery(
|
||||||
|
'UPDATE notes SET archived = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
||||||
|
[archived ? 1 : 0, req.params.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`Note ${archived ? 'archivée' : 'désarchivée'}: ${req.params.id}`);
|
||||||
|
res.json({ message: `Note ${archived ? 'archivée' : 'désarchivée'} avec succès`, archived });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Erreur lors de l\'archivage de la note:', error);
|
||||||
|
res.status(500).json({ error: 'Erreur serveur' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE /api/notes/:id
|
* DELETE /api/notes/:id
|
||||||
* Supprimer une note
|
* Supprimer une note
|
||||||
|
|||||||
Reference in new issue
Block a user