mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-17 04:08:41 +02:00
Merge pull request #7 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
+321
-7
No files matched your search
@@ -508,3 +508,84 @@
|
||||
.users-table tbody tr:hover {
|
||||
background: var(--color-background);
|
||||
}
|
||||
|
||||
.btn-delete-user {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
background: var(--color-danger);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
transition: all var(--transition-speed);
|
||||
}
|
||||
|
||||
.btn-delete-user:hover {
|
||||
background: #c0392b;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Modal de confirmation */
|
||||
.confirm-modal {
|
||||
max-width: 450px;
|
||||
text-align: center;
|
||||
padding: var(--spacing-xl) var(--spacing-lg);
|
||||
}
|
||||
|
||||
.confirm-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.confirm-title {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--spacing-md);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.confirm-message {
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.confirm-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-cancel,
|
||||
.btn-confirm {
|
||||
padding: var(--spacing-md) var(--spacing-xl);
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-speed);
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background: var(--color-background);
|
||||
color: var(--color-text);
|
||||
border: 2px solid var(--color-border);
|
||||
}
|
||||
|
||||
.btn-cancel:hover {
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
background: var(--color-danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-confirm:hover {
|
||||
background: #c0392b;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(231, 76, 60, 0.3);
|
||||
}
|
||||
@@ -146,6 +146,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal de Confirmation -->
|
||||
<div class="modal-backdrop" id="confirmModal" style="display: none;">
|
||||
<div class="modal-content confirm-modal">
|
||||
<div class="confirm-icon" id="confirmIcon">⚠️</div>
|
||||
<h3 class="confirm-title" id="confirmTitle">Confirmer l'action</h3>
|
||||
<p class="confirm-message" id="confirmMessage">Êtes-vous sûr de vouloir continuer ?</p>
|
||||
<div class="confirm-actions">
|
||||
<button class="btn-cancel" id="confirmCancel">Annuler</button>
|
||||
<button class="btn-confirm" id="confirmOk">Confirmer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script src="/js/complete-app.js"></script>
|
||||
</body>
|
||||
|
||||
+227
-7
@@ -38,6 +38,63 @@ const utils = {
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== MODAL DE CONFIRMATION ====================
|
||||
const confirmDialog = {
|
||||
show(options = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const modal = document.getElementById('confirmModal');
|
||||
const icon = document.getElementById('confirmIcon');
|
||||
const title = document.getElementById('confirmTitle');
|
||||
const message = document.getElementById('confirmMessage');
|
||||
const cancelBtn = document.getElementById('confirmCancel');
|
||||
const okBtn = document.getElementById('confirmOk');
|
||||
|
||||
// Configurer le contenu
|
||||
icon.textContent = options.icon || '⚠️';
|
||||
title.textContent = options.title || 'Confirmer l\'action';
|
||||
message.textContent = options.message || 'Êtes-vous sûr de vouloir continuer ?';
|
||||
cancelBtn.textContent = options.cancelText || 'Annuler';
|
||||
okBtn.textContent = options.okText || 'Confirmer';
|
||||
|
||||
// Gérer les clics
|
||||
const handleCancel = () => {
|
||||
cleanup();
|
||||
resolve(false);
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
cleanup();
|
||||
resolve(true);
|
||||
};
|
||||
|
||||
const handleEscape = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
handleCancel();
|
||||
}
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
modal.classList.remove('active');
|
||||
setTimeout(() => {
|
||||
modal.style.display = 'none';
|
||||
}, 300);
|
||||
cancelBtn.removeEventListener('click', handleCancel);
|
||||
okBtn.removeEventListener('click', handleConfirm);
|
||||
document.removeEventListener('keydown', handleEscape);
|
||||
};
|
||||
|
||||
// Ajouter les écouteurs
|
||||
cancelBtn.addEventListener('click', handleCancel);
|
||||
okBtn.addEventListener('click', handleConfirm);
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
|
||||
// Afficher le modal
|
||||
modal.style.display = 'flex';
|
||||
setTimeout(() => modal.classList.add('active'), 10);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== API ====================
|
||||
const api = {
|
||||
async request(url, options = {}) {
|
||||
@@ -401,7 +458,14 @@ async function toggleNoteTodo(todoId, completed) {
|
||||
}
|
||||
|
||||
async function deleteNoteTodo(todoId) {
|
||||
if (!confirm('Supprimer ce todo ?')) return;
|
||||
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}`);
|
||||
@@ -421,7 +485,15 @@ function closeNoteModal() {
|
||||
|
||||
async function deleteCurrentNote() {
|
||||
if (!currentNoteId) return;
|
||||
if (!confirm('Supprimer cette note ?')) return;
|
||||
|
||||
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/${currentNoteId}`);
|
||||
@@ -456,7 +528,15 @@ async function handleImageUpload(event) {
|
||||
|
||||
async function removeNoteImage() {
|
||||
if (!currentNoteId) return;
|
||||
if (!confirm('Supprimer l\'image ?')) return;
|
||||
|
||||
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/${currentNoteId}/image`);
|
||||
@@ -519,23 +599,33 @@ async function addTodo() {
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleTodo(id, completed) {
|
||||
// Exposer globalement pour les event handlers inline
|
||||
window.toggleTodo = async function(id, completed) {
|
||||
try {
|
||||
await api.put(`/api/todos/${id}`, { completed });
|
||||
await loadTodos();
|
||||
} catch (error) {
|
||||
console.error('Erreur toggle todo:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.deleteTodo = async function(id) {
|
||||
const confirmed = await confirmDialog.show({
|
||||
icon: '🗑️',
|
||||
title: 'Supprimer cette tâche',
|
||||
message: 'Êtes-vous sûr de vouloir supprimer cette tâche ?',
|
||||
okText: 'Supprimer'
|
||||
});
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
async function deleteTodo(id) {
|
||||
try {
|
||||
await api.delete(`/api/todos/${id}`);
|
||||
await loadTodos();
|
||||
} catch (error) {
|
||||
console.error('Erreur suppression todo:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function setTodoFilter(filter) {
|
||||
state.filter = filter;
|
||||
@@ -583,6 +673,113 @@ function setupSearch() {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== ADMIN ====================
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const users = await api.get('/api/users');
|
||||
renderUsersTable(users);
|
||||
} catch (error) {
|
||||
console.error('Erreur chargement utilisateurs:', error);
|
||||
alert('Erreur lors du chargement des utilisateurs');
|
||||
}
|
||||
}
|
||||
|
||||
function renderUsersTable(users) {
|
||||
const tbody = document.getElementById('usersTableBody');
|
||||
if (!tbody) return;
|
||||
|
||||
tbody.innerHTML = users.map(user => `
|
||||
<tr>
|
||||
<td>${user.id}</td>
|
||||
<td>${user.username}</td>
|
||||
<td>${user.is_admin ? '✓ Oui' : '✗ Non'}</td>
|
||||
<td>${new Date(user.created_at).toLocaleDateString('fr-FR')}</td>
|
||||
<td>
|
||||
${user.id !== state.user.id ? `
|
||||
<button class="btn-delete-user" onclick="deleteUser(${user.id}, '${user.username}')">
|
||||
🗑️ Supprimer
|
||||
</button>
|
||||
` : '<span style="color: var(--text-secondary);">Vous-même</span>'}
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function openAdminModal() {
|
||||
const modal = document.getElementById('adminModal');
|
||||
if (!modal) return;
|
||||
|
||||
modal.style.display = 'flex';
|
||||
setTimeout(() => modal.classList.add('active'), 10);
|
||||
await loadUsers();
|
||||
}
|
||||
|
||||
function closeAdminModal() {
|
||||
const modal = document.getElementById('adminModal');
|
||||
if (!modal) return;
|
||||
|
||||
modal.classList.remove('active');
|
||||
setTimeout(() => {
|
||||
modal.style.display = 'none';
|
||||
}, 300);
|
||||
}
|
||||
|
||||
async function createUser() {
|
||||
const username = prompt('Nom d\'utilisateur:');
|
||||
if (!username || username.trim().length < 3) {
|
||||
alert('Le nom d\'utilisateur doit contenir au moins 3 caractères');
|
||||
return;
|
||||
}
|
||||
|
||||
const password = prompt('Mot de passe:');
|
||||
if (!password || password.length < 4) {
|
||||
alert('Le mot de passe doit contenir au moins 4 caractères');
|
||||
return;
|
||||
}
|
||||
|
||||
const isAdmin = await confirmDialog.show({
|
||||
icon: '👤',
|
||||
title: 'Droits administrateur',
|
||||
message: `Créer "${username}" en tant qu'administrateur ?`,
|
||||
okText: 'Oui, admin',
|
||||
cancelText: 'Non, utilisateur'
|
||||
});
|
||||
|
||||
try {
|
||||
await api.post('/api/users', {
|
||||
username: username.trim(),
|
||||
password,
|
||||
is_admin: isAdmin
|
||||
});
|
||||
alert('Utilisateur créé avec succès');
|
||||
await loadUsers();
|
||||
} catch (error) {
|
||||
console.error('Erreur création utilisateur:', error);
|
||||
alert('Erreur lors de la création de l\'utilisateur. Il existe peut-être déjà.');
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction globale pour la suppression d'utilisateur (appelée depuis onclick)
|
||||
window.deleteUser = async function(id, username) {
|
||||
const confirmed = await confirmDialog.show({
|
||||
icon: '⚠️',
|
||||
title: 'Supprimer cet utilisateur',
|
||||
message: `Êtes-vous sûr de vouloir supprimer l'utilisateur "${username}" ?\n\nToutes ses notes et todos seront également supprimés définitivement.`,
|
||||
okText: 'Supprimer'
|
||||
});
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
await api.delete(`/api/users/${id}`);
|
||||
alert('Utilisateur supprimé avec succès');
|
||||
await loadUsers();
|
||||
} catch (error) {
|
||||
console.error('Erreur suppression utilisateur:', error);
|
||||
alert('Erreur lors de la suppression de l\'utilisateur');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== INIT ====================
|
||||
async function init() {
|
||||
// Vérifier l'authentification
|
||||
@@ -600,6 +797,29 @@ async function init() {
|
||||
newNoteBtn.addEventListener('click', () => openNoteModal());
|
||||
}
|
||||
|
||||
// Admin button
|
||||
const adminBtn = document.getElementById('adminBtn');
|
||||
if (adminBtn) {
|
||||
adminBtn.addEventListener('click', openAdminModal);
|
||||
}
|
||||
|
||||
const closeAdminModalBtn = document.getElementById('closeAdminModal');
|
||||
if (closeAdminModalBtn) {
|
||||
closeAdminModalBtn.addEventListener('click', closeAdminModal);
|
||||
}
|
||||
|
||||
const createUserBtn = document.getElementById('createUserBtn');
|
||||
if (createUserBtn) {
|
||||
createUserBtn.addEventListener('click', createUser);
|
||||
}
|
||||
|
||||
const adminModalBackdrop = document.getElementById('adminModal');
|
||||
if (adminModalBackdrop) {
|
||||
adminModalBackdrop.addEventListener('click', (e) => {
|
||||
if (e.target === adminModalBackdrop) closeAdminModal();
|
||||
});
|
||||
}
|
||||
|
||||
const closeModal = document.getElementById('closeModal');
|
||||
if (closeModal) {
|
||||
closeModal.addEventListener('click', closeNoteModal);
|
||||
|
||||
Reference in new issue
Block a user