Spaces:
Running
Running
Download index.html from stephaneibens/task-manager-si: direct link, hf CLI and curl.
- Browser
- Download file 37.4 kB
-
https://huggingface.co/spaces/stephaneibens/task-manager-si/resolve/main/index.html
- Command line
-
hf download hf://spaces/stephaneibens/task-manager-si/index.html
-
curl -L -o index.html https://huggingface.co/spaces/stephaneibens/task-manager-si/resolve/main/index.html
37.4 kB
| <html lang="fr"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Todo List Avancée</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| /* Animation pour les notifications */ | |
| @keyframes slideIn { | |
| from { transform: translateX(100%); opacity: 0; } | |
| to { transform: translateX(0); opacity: 1; } | |
| } | |
| @keyframes fadeOut { | |
| from { opacity: 1; } | |
| to { opacity: 0; } | |
| } | |
| .notification { | |
| animation: slideIn 0.3s ease-out; | |
| } | |
| .notification.hide { | |
| animation: fadeOut 0.5s ease-out forwards; | |
| } | |
| /* Style personnalisé pour le datepicker */ | |
| input[type="datetime-local"]::-webkit-calendar-picker-indicator { | |
| filter: invert(0.5); | |
| cursor: pointer; | |
| } | |
| /* Style pour les tâches complétées */ | |
| .completed { | |
| opacity: 0.7; | |
| position: relative; | |
| } | |
| .completed::after { | |
| content: ""; | |
| position: absolute; | |
| left: 0; | |
| top: 50%; | |
| width: 100%; | |
| height: 1px; | |
| background: currentColor; | |
| transform: translateY(-50%); | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-100 min-h-screen font-sans"> | |
| <div class="container mx-auto px-4 py-8 max-w-6xl"> | |
| <div class="flex flex-col md:flex-row gap-6"> | |
| <!-- Sidebar --> | |
| <div class="w-full md:w-64 bg-white rounded-lg shadow-md p-4 h-fit"> | |
| <h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center gap-2"> | |
| <i class="fas fa-tasks"></i> | |
| <span>Todo List</span> | |
| </h1> | |
| <div class="mb-6"> | |
| <h2 class="text-lg font-semibold text-gray-700 mb-2">Catégories</h2> | |
| <ul id="categoriesList" class="space-y-1"> | |
| <li> | |
| <button onclick="filterTasks('all')" class="w-full text-left px-3 py-2 rounded hover:bg-gray-100 flex items-center gap-2 text-blue-600"> | |
| <i class="fas fa-list"></i> | |
| <span>Toutes les tâches</span> | |
| </button> | |
| </li> | |
| </ul> | |
| <div class="mt-4 flex gap-2"> | |
| <input type="text" id="newCategoryName" placeholder="Nouvelle catégorie" class="flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm"> | |
| <button onclick="addNewCategory()" class="px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm"> | |
| <i class="fas fa-plus"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="mb-6"> | |
| <h2 class="text-lg font-semibold text-gray-700 mb-2">Priorités</h2> | |
| <ul class="space-y-1"> | |
| <li> | |
| <button onclick="filterByPriority('all')" class="w-full text-left px-3 py-2 rounded hover:bg-gray-100 flex items-center gap-2"> | |
| <i class="fas fa-flag text-gray-400"></i> | |
| <span>Toutes</span> | |
| </button> | |
| </li> | |
| <li> | |
| <button onclick="filterByPriority('high')" class="w-full text-left px-3 py-2 rounded hover:bg-gray-100 flex items-center gap-2"> | |
| <i class="fas fa-flag text-red-500"></i> | |
| <span>Haute</span> | |
| </button> | |
| </li> | |
| <li> | |
| <button onclick="filterByPriority('medium')" class="w-full text-left px-3 py-2 rounded hover:bg-gray-100 flex items-center gap-2"> | |
| <i class="fas fa-flag text-yellow-500"></i> | |
| <span>Moyenne</span> | |
| </button> | |
| </li> | |
| <li> | |
| <button onclick="filterByPriority('low')" class="w-full text-left px-3 py-2 rounded hover:bg-gray-100 flex items-center gap-2"> | |
| <i class="fas fa-flag text-green-500"></i> | |
| <span>Basse</span> | |
| </button> | |
| </li> | |
| </ul> | |
| </div> | |
| <div> | |
| <h2 class="text-lg font-semibold text-gray-700 mb-2">Statut</h2> | |
| <ul class="space-y-1"> | |
| <li> | |
| <button onclick="filterByStatus('all')" class="w-full text-left px-3 py-2 rounded hover:bg-gray-100 flex items-center gap-2"> | |
| <i class="fas fa-circle text-gray-400"></i> | |
| <span>Toutes</span> | |
| </button> | |
| </li> | |
| <li> | |
| <button onclick="filterByStatus('completed')" class="w-full text-left px-3 py-2 rounded hover:bg-gray-100 flex items-center gap-2"> | |
| <i class="fas fa-check-circle text-green-500"></i> | |
| <span>Complétées</span> | |
| </button> | |
| </li> | |
| <li> | |
| <button onclick="filterByStatus('pending')" class="w-full text-left px-3 py-2 rounded hover:bg-gray-100 flex items-center gap-2"> | |
| <i class="fas fa-hourglass-half text-blue-500"></i> | |
| <span>En attente</span> | |
| </button> | |
| </li> | |
| <li> | |
| <button onclick="filterByStatus('overdue')" class="w-full text-left px-3 py-2 rounded hover:bg-gray-100 flex items-center gap-2"> | |
| <i class="fas fa-exclamation-circle text-red-500"></i> | |
| <span>En retard</span> | |
| </button> | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| <!-- Main Content --> | |
| <div class="flex-1"> | |
| <!-- Add Task Form --> | |
| <div class="bg-white rounded-lg shadow-md p-4 mb-6"> | |
| <h2 class="text-lg font-bold text-gray-800 mb-3">Ajouter une nouvelle tâche</h2> | |
| <form id="addTaskForm" class="space-y-3"> | |
| <div> | |
| <label for="taskTitle" class="block text-sm font-medium text-gray-700 mb-1">Titre de la tâche</label> | |
| <input type="text" id="taskTitle" required class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-4"> | |
| <div> | |
| <label for="taskCategory" class="block text-sm font-medium text-gray-700 mb-1">Catégorie</label> | |
| <select id="taskCategory" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| <!-- Les catégories seront ajoutées dynamiquement --> | |
| </select> | |
| </div> | |
| <div> | |
| <label for="taskPriority" class="block text-sm font-medium text-gray-700 mb-1">Priorité</label> | |
| <select id="taskPriority" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| <option value="high">Haute</option> | |
| <option value="medium" selected>Moyenne</option> | |
| <option value="low">Basse</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label for="taskDueDate" class="block text-sm font-medium text-gray-700 mb-1">Date d'échéance</label> | |
| <input type="datetime-local" id="taskDueDate" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| </div> | |
| </div> | |
| <div class="flex items-center"> | |
| <input type="checkbox" id="taskReminder" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"> | |
| <label for="taskReminder" class="ml-2 block text-sm text-gray-700">Activer un rappel</label> | |
| </div> | |
| <div id="reminderOptions" class="hidden pl-6 border-l-2 border-gray-200 ml-2"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label for="reminderTime" class="block text-sm font-medium text-gray-700 mb-1">Temps avant rappel</label> | |
| <select id="reminderTime" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| <option value="5">5 minutes avant</option> | |
| <option value="15">15 minutes avant</option> | |
| <option value="30" selected>30 minutes avant</option> | |
| <option value="60">1 heure avant</option> | |
| <option value="1440">1 jour avant</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label for="reminderMethod" class="block text-sm font-medium text-gray-700 mb-1">Méthode de rappel</label> | |
| <select id="reminderMethod" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| <option value="notification">Notification</option> | |
| <option value="email">Email</option> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex justify-end"> | |
| <button type="submit" class="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"> | |
| <i class="fas fa-plus mr-2"></i>Ajouter la tâche | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| <!-- Tasks List --> | |
| <div class="bg-white rounded-lg shadow-md p-6"> | |
| <div class="flex justify-between items-center mb-6"> | |
| <h2 class="text-xl font-bold text-gray-800">Mes Tâches</h2> | |
| <div class="flex items-center gap-2"> | |
| <span id="taskCount" class="text-sm text-gray-500">0 tâches</span> | |
| <button onclick="clearCompletedTasks()" class="text-sm text-red-500 hover:text-red-700"> | |
| <i class="fas fa-trash-alt mr-1"></i>Effacer complétées | |
| </button> | |
| </div> | |
| </div> | |
| <div class="mb-4"> | |
| <input type="text" id="searchTask" placeholder="Rechercher des tâches..." class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| </div> | |
| <div id="tasksList" class="space-y-3"> | |
| <!-- Les tâches seront ajoutées ici dynamiquement --> | |
| <div class="text-center py-10 text-gray-500"> | |
| <i class="fas fa-tasks text-4xl mb-2"></i> | |
| <p>Aucune tâche pour le moment</p> | |
| <p class="text-sm">Ajoutez votre première tâche ci-dessus</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Notification Area --> | |
| <div id="notificationArea" class="fixed bottom-4 right-4 space-y-2 w-80 z-50"></div> | |
| <script> | |
| // Variables globales | |
| let tasks = JSON.parse(localStorage.getItem('tasks')) || []; | |
| let categories = JSON.parse(localStorage.getItem('categories')) || []; | |
| let currentFilter = { category: 'all', priority: 'all', status: 'all' }; | |
| let activeReminders = {}; | |
| // Initialisation | |
| document.addEventListener('DOMContentLoaded', function() { | |
| renderCategories(); | |
| renderTasks(); | |
| updateTaskCount(); | |
| setupReminderToggle(); | |
| setupSearch(); | |
| checkForOverdueTasks(); | |
| // Vérifier les rappels toutes les minutes | |
| setInterval(checkReminders, 60000); | |
| }); | |
| // Gestion du formulaire | |
| document.getElementById('addTaskForm').addEventListener('submit', function(e) { | |
| e.preventDefault(); | |
| const title = document.getElementById('taskTitle').value.trim(); | |
| const category = document.getElementById('taskCategory').value; | |
| const priority = document.getElementById('taskPriority').value; | |
| const dueDate = document.getElementById('taskDueDate').value; | |
| const hasReminder = document.getElementById('taskReminder').checked; | |
| const reminderTime = hasReminder ? parseInt(document.getElementById('reminderTime').value) : null; | |
| const reminderMethod = hasReminder ? document.getElementById('reminderMethod').value : null; | |
| if (!title) return; | |
| const newTask = { | |
| id: Date.now().toString(), | |
| title, | |
| description: '', | |
| category, | |
| priority, | |
| dueDate: dueDate || null, | |
| completed: false, | |
| createdAt: new Date().toISOString(), | |
| reminder: hasReminder ? { | |
| time: reminderTime, | |
| method: reminderMethod, | |
| triggered: false | |
| } : null | |
| }; | |
| tasks.push(newTask); | |
| saveTasks(); | |
| renderTasks(); | |
| updateTaskCount(); | |
| // Réinitialiser le formulaire | |
| this.reset(); | |
| document.getElementById('reminderOptions').classList.add('hidden'); | |
| // Planifier le rappel si nécessaire | |
| if (hasReminder && dueDate) { | |
| scheduleReminder(newTask); | |
| } | |
| showNotification('Tâche ajoutée avec succès!', 'success'); | |
| }); | |
| // Fonction pour afficher les tâches | |
| function renderTasks(filteredTasks = null) { | |
| const tasksList = document.getElementById('tasksList'); | |
| const tasksToRender = filteredTasks || filterTasks(); | |
| if (tasksToRender.length === 0) { | |
| tasksList.innerHTML = ` | |
| <div class="text-center py-10 text-gray-500"> | |
| <i class="fas fa-tasks text-4xl mb-2"></i> | |
| <p>Aucune tâche trouvée</p> | |
| <p class="text-sm">Essayez de modifier vos filtres</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| tasksList.innerHTML = ''; | |
| tasksToRender.forEach(task => { | |
| const taskElement = createTaskElement(task); | |
| tasksList.appendChild(taskElement); | |
| }); | |
| } | |
| // Fonction pour créer un élément de tâche | |
| function createTaskElement(task) { | |
| const taskElement = document.createElement('div'); | |
| taskElement.className = `border rounded-lg p-4 ${task.completed ? 'bg-gray-50 completed' : 'bg-white'} ${isTaskOverdue(task) && !task.completed ? 'border-red-200' : 'border-gray-200'}`; | |
| taskElement.dataset.id = task.id; | |
| // Trouver la catégorie de la tâche | |
| const taskCategory = categories.find(cat => cat.id === task.category); | |
| const categoryIcon = taskCategory ? taskCategory.icon : 'ellipsis-h'; | |
| const categoryColor = taskCategory ? taskCategory.color : 'gray'; | |
| const categoryName = taskCategory ? taskCategory.name : 'Inconnue'; | |
| // Icône de priorité | |
| const priorityIcons = { | |
| high: 'flag', | |
| medium: 'flag', | |
| low: 'flag' | |
| }; | |
| const priorityColors = { | |
| high: 'red', | |
| medium: 'yellow', | |
| low: 'green' | |
| }; | |
| // Date formatée | |
| const dueDateFormatted = task.dueDate ? new Date(task.dueDate).toLocaleString('fr-FR', { | |
| day: '2-digit', | |
| month: '2-digit', | |
| year: 'numeric', | |
| hour: '2-digit', | |
| minute: '2-digit' | |
| }) : 'Pas de date'; | |
| // Statut de la tâche | |
| let statusText = ''; | |
| let statusColor = ''; | |
| if (task.completed) { | |
| statusText = 'Complétée'; | |
| statusColor = 'green'; | |
| } else if (isTaskOverdue(task)) { | |
| statusText = 'En retard'; | |
| statusColor = 'red'; | |
| } else if (task.dueDate) { | |
| statusText = 'En cours'; | |
| statusColor = 'blue'; | |
| } else { | |
| statusText = 'Sans date'; | |
| statusColor = 'gray'; | |
| } | |
| taskElement.innerHTML = ` | |
| <div class="flex justify-between items-start mb-2"> | |
| <div class="flex items-center gap-2"> | |
| <input type="checkbox" ${task.completed ? 'checked' : ''} onchange="toggleTaskCompletion('${task.id}')" class="h-5 w-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"> | |
| <h3 class="font-medium ${task.completed ? 'line-through text-gray-500' : 'text-gray-800'}">${task.title}</h3> | |
| </div> | |
| <div class="flex items-center gap-2"> | |
| <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-${priorityColors[task.priority]}-100 text-${priorityColors[task.priority]}-800"> | |
| <i class="fas fa-${priorityIcons[task.priority]} text-${priorityColors[task.priority]}-500 mr-1"></i> | |
| ${task.priority === 'high' ? 'Haute' : task.priority === 'medium' ? 'Moyenne' : 'Basse'} | |
| </span> | |
| <button onclick="deleteTask('${task.id}')" class="text-gray-400 hover:text-red-500"> | |
| <i class="fas fa-trash-alt"></i> | |
| </button> | |
| </div> | |
| </div> | |
| ${task.description ? `<p class="text-sm text-gray-600 mb-3">${task.description}</p>` : ''} | |
| <div class="flex flex-wrap items-center justify-between gap-2 text-sm"> | |
| <div class="flex items-center gap-2"> | |
| <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-${categoryColor}-100 text-${categoryColor}-800"> | |
| <i class="fas fa-${categoryIcon} text-${categoryColor}-500 mr-1"></i> | |
| ${categoryName} | |
| </span> | |
| <span class="text-gray-500"> | |
| <i class="far fa-calendar-alt mr-1"></i> | |
| ${dueDateFormatted} | |
| </span> | |
| </div> | |
| <div class="flex items-center gap-2"> | |
| <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-${statusColor}-100 text-${statusColor}-800"> | |
| ${statusText} | |
| </span> | |
| ${task.reminder ? ` | |
| <span class="text-gray-500"> | |
| <i class="fas fa-bell ${task.reminder.triggered ? 'text-yellow-500' : ''} mr-1"></i> | |
| Rappel ${task.reminder.time} min avant | |
| </span> | |
| ` : ''} | |
| </div> | |
| </div> | |
| `; | |
| return taskElement; | |
| } | |
| // Fonction pour basculer l'état de complétion d'une tâche | |
| function toggleTaskCompletion(taskId) { | |
| const taskIndex = tasks.findIndex(task => task.id === taskId); | |
| if (taskIndex !== -1) { | |
| tasks[taskIndex].completed = !tasks[taskIndex].completed; | |
| saveTasks(); | |
| renderTasks(); | |
| updateTaskCount(); | |
| if (tasks[taskIndex].completed) { | |
| showNotification('Tâche marquée comme complétée!', 'success'); | |
| } | |
| } | |
| } | |
| // Fonction pour supprimer une tâche | |
| function deleteTask(taskId) { | |
| if (confirm('Êtes-vous sûr de vouloir supprimer cette tâche?')) { | |
| tasks = tasks.filter(task => task.id !== taskId); | |
| saveTasks(); | |
| renderTasks(); | |
| updateTaskCount(); | |
| showNotification('Tâche supprimée avec succès!', 'success'); | |
| // Annuler tout rappel planifié pour cette tâche | |
| if (activeReminders[taskId]) { | |
| clearTimeout(activeReminders[taskId]); | |
| delete activeReminders[taskId]; | |
| } | |
| } | |
| } | |
| // Fonction pour effacer toutes les tâches complétées | |
| function clearCompletedTasks() { | |
| if (confirm('Êtes-vous sûr de vouloir supprimer toutes les tâches complétées?')) { | |
| // Annuler les rappels pour les tâches complétées | |
| tasks.forEach(task => { | |
| if (task.completed && activeReminders[task.id]) { | |
| clearTimeout(activeReminders[task.id]); | |
| delete activeReminders[task.id]; | |
| } | |
| }); | |
| tasks = tasks.filter(task => !task.completed); | |
| saveTasks(); | |
| renderTasks(); | |
| updateTaskCount(); | |
| showNotification('Tâches complétées effacées!', 'success'); | |
| } | |
| } | |
| // Fonction pour filtrer les tâches par catégorie | |
| function filterTasks(category = 'all') { | |
| currentFilter.category = category; | |
| return applyFilters(); | |
| } | |
| // Fonction pour filtrer les tâches par priorité | |
| function filterByPriority(priority = 'all') { | |
| currentFilter.priority = priority; | |
| return applyFilters(); | |
| } | |
| // Fonction pour filtrer les tâches par statut | |
| function filterByStatus(status = 'all') { | |
| currentFilter.status = status; | |
| return applyFilters(); | |
| } | |
| // Fonction pour appliquer tous les filtres | |
| function applyFilters() { | |
| let filteredTasks = [...tasks]; | |
| // Filtrer par catégorie | |
| if (currentFilter.category !== 'all') { | |
| filteredTasks = filteredTasks.filter(task => task.category === currentFilter.category); | |
| } | |
| // Filtrer par priorité | |
| if (currentFilter.priority !== 'all') { | |
| filteredTasks = filteredTasks.filter(task => task.priority === currentFilter.priority); | |
| } | |
| // Filtrer par statut | |
| if (currentFilter.status !== 'all') { | |
| if (currentFilter.status === 'completed') { | |
| filteredTasks = filteredTasks.filter(task => task.completed); | |
| } else if (currentFilter.status === 'pending') { | |
| filteredTasks = filteredTasks.filter(task => !task.completed && !isTaskOverdue(task)); | |
| } else if (currentFilter.status === 'overdue') { | |
| filteredTasks = filteredTasks.filter(task => !task.completed && isTaskOverdue(task)); | |
| } | |
| } | |
| renderTasks(filteredTasks); | |
| return filteredTasks; | |
| } | |
| // Fonction pour vérifier si une tâche est en retard | |
| function isTaskOverdue(task) { | |
| if (!task.dueDate || task.completed) return false; | |
| return new Date(task.dueDate) < new Date(); | |
| } | |
| // Fonction pour vérifier les tâches en retard au chargement | |
| function checkForOverdueTasks() { | |
| const overdueTasks = tasks.filter(task => isTaskOverdue(task)); | |
| if (overdueTasks.length > 0) { | |
| showNotification(`Vous avez ${overdueTasks.length} tâche(s) en retard!`, 'warning'); | |
| } | |
| } | |
| // Fonction pour configurer la recherche | |
| function setupSearch() { | |
| const searchInput = document.getElementById('searchTask'); | |
| searchInput.addEventListener('input', function() { | |
| const searchTerm = this.value.toLowerCase(); | |
| const filteredTasks = tasks.filter(task => | |
| task.title.toLowerCase().includes(searchTerm) || | |
| (task.description && task.description.toLowerCase().includes(searchTerm)) | |
| ); | |
| renderTasks(filteredTasks); | |
| }); | |
| } | |
| // Fonction pour configurer le toggle des rappels | |
| function setupReminderToggle() { | |
| const reminderCheckbox = document.getElementById('taskReminder'); | |
| const reminderOptions = document.getElementById('reminderOptions'); | |
| reminderCheckbox.addEventListener('change', function() { | |
| if (this.checked) { | |
| reminderOptions.classList.remove('hidden'); | |
| } else { | |
| reminderOptions.classList.add('hidden'); | |
| } | |
| }); | |
| } | |
| // Fonction pour planifier un rappel | |
| function scheduleReminder(task) { | |
| if (!task.reminder || !task.dueDate) return; | |
| const dueDate = new Date(task.dueDate); | |
| const reminderTime = task.reminder.time * 60000; // Convertir en millisecondes | |
| const reminderDate = new Date(dueDate.getTime() - reminderTime); | |
| const now = new Date(); | |
| const timeUntilReminder = reminderDate.getTime() - now.getTime(); | |
| // Si le rappel est dans le futur | |
| if (timeUntilReminder > 0) { | |
| activeReminders[task.id] = setTimeout(() => { | |
| triggerReminder(task); | |
| }, timeUntilReminder); | |
| } | |
| } | |
| // Fonction pour déclencher un rappel | |
| function triggerReminder(task) { | |
| // Marquer le rappel comme déclenché | |
| const taskIndex = tasks.findIndex(t => t.id === task.id); | |
| if (taskIndex !== -1 && tasks[taskIndex].reminder) { | |
| tasks[taskIndex].reminder.triggered = true; | |
| saveTasks(); | |
| renderTasks(); | |
| } | |
| // Afficher la notification | |
| const method = task.reminder.method; | |
| const message = `Rappel: "${task.title}" est due à ${new Date(task.dueDate).toLocaleTimeString('fr-FR', {hour: '2-digit', minute:'2-digit'})}`; | |
| if (method === 'notification') { | |
| showNotification(message, 'info', 10000); | |
| } else if (method === 'email') { | |
| // En production, vous enverriez un vrai email ici | |
| console.log(`Email envoyé: ${message}`); | |
| showNotification(`Rappel envoyé par email: ${task.title}`, 'info'); | |
| } | |
| // Supprimer le rappel des rappels actifs | |
| delete activeReminders[task.id]; | |
| } | |
| // Fonction pour vérifier périodiquement les rappels | |
| function checkReminders() { | |
| const now = new Date(); | |
| tasks.forEach(task => { | |
| if (task.reminder && !task.reminder.triggered && task.dueDate) { | |
| const dueDate = new Date(task.dueDate); | |
| const reminderTime = task.reminder.time * 60000; | |
| const reminderDate = new Date(dueDate.getTime() - reminderTime); | |
| // Si le rappel devrait être déclenché maintenant | |
| if (reminderDate <= now && dueDate > now) { | |
| triggerReminder(task); | |
| } | |
| } | |
| }); | |
| } | |
| // Fonction pour afficher une notification | |
| function showNotification(message, type = 'info', duration = 5000) { | |
| const notificationArea = document.getElementById('notificationArea'); | |
| const notificationId = Date.now().toString(); | |
| const colors = { | |
| success: 'bg-green-100 border-green-500 text-green-700', | |
| error: 'bg-red-100 border-red-500 text-red-700', | |
| warning: 'bg-yellow-100 border-yellow-500 text-yellow-700', | |
| info: 'bg-blue-100 border-blue-500 text-blue-700' | |
| }; | |
| const icons = { | |
| success: 'fa-check-circle', | |
| error: 'fa-exclamation-circle', | |
| warning: 'fa-exclamation-triangle', | |
| info: 'fa-info-circle' | |
| }; | |
| const notification = document.createElement('div'); | |
| notification.id = `notification-${notificationId}`; | |
| notification.className = `notification p-4 border-l-4 rounded-lg shadow-md ${colors[type]} flex items-start`; | |
| notification.innerHTML = ` | |
| <i class="fas ${icons[type]} mt-1 mr-3"></i> | |
| <div class="flex-1"> | |
| <p class="text-sm font-medium">${message}</p> | |
| </div> | |
| <button onclick="dismissNotification('${notificationId}')" class="ml-2 text-gray-500 hover:text-gray-700"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| `; | |
| notificationArea.appendChild(notification); | |
| // Fermer automatiquement après la durée spécifiée | |
| setTimeout(() => { | |
| dismissNotification(notificationId); | |
| }, duration); | |
| } | |
| // Fonction pour fermer une notification | |
| function dismissNotification(id) { | |
| const notification = document.getElementById(`notification-${id}`); | |
| if (notification) { | |
| notification.classList.add('hide'); | |
| setTimeout(() => { | |
| notification.remove(); | |
| }, 500); | |
| } | |
| } | |
| // Fonction pour mettre à jour le compteur de tâches | |
| function updateTaskCount() { | |
| const totalTasks = tasks.length; | |
| const completedTasks = tasks.filter(task => task.completed).length; | |
| const pendingTasks = totalTasks - completedTasks; | |
| document.getElementById('taskCount').textContent = | |
| `${pendingTasks} en cours • ${completedTasks} complétées • ${totalTasks} au total`; | |
| } | |
| // Fonction pour sauvegarder les tâches dans le localStorage | |
| function saveTasks() { | |
| localStorage.setItem('tasks', JSON.stringify(tasks)); | |
| } | |
| // Fonction pour sauvegarder les catégories | |
| function saveCategories() { | |
| localStorage.setItem('categories', JSON.stringify(categories)); | |
| } | |
| // Fonction pour afficher les catégories | |
| function renderCategories() { | |
| const categoriesList = document.getElementById('categoriesList'); | |
| const taskCategorySelect = document.getElementById('taskCategory'); | |
| // Garder seulement le bouton "Toutes les tâches" | |
| categoriesList.innerHTML = categoriesList.children[0].outerHTML; | |
| taskCategorySelect.innerHTML = ''; | |
| // Ajouter les catégories personnalisées | |
| categories.forEach(category => { | |
| // Ajouter à la liste des catégories | |
| const categoryItem = document.createElement('li'); | |
| categoryItem.innerHTML = ` | |
| <button onclick="filterTasks('${category.id}')" class="w-full text-left px-3 py-2 rounded hover:bg-gray-100 flex items-center gap-2"> | |
| <i class="fas fa-${category.icon} text-${category.color}-500"></i> | |
| <span>${category.name}</span> | |
| </button> | |
| `; | |
| categoriesList.appendChild(categoryItem); | |
| // Ajouter au select du formulaire | |
| const option = document.createElement('option'); | |
| option.value = category.id; | |
| option.textContent = category.name; | |
| taskCategorySelect.appendChild(option); | |
| }); | |
| } | |
| // Fonction pour ajouter une nouvelle catégorie | |
| function addNewCategory() { | |
| const nameInput = document.getElementById('newCategoryName'); | |
| const name = nameInput.value.trim(); | |
| if (!name) return; | |
| // Générer un ID unique | |
| const id = name.toLowerCase().replace(/\s+/g, '-'); | |
| // Vérifier si la catégorie existe déjà | |
| if (categories.some(cat => cat.id === id)) { | |
| showNotification('Cette catégorie existe déjà!', 'error'); | |
| return; | |
| } | |
| // Icônes et couleurs par défaut | |
| const icons = ['briefcase', 'user', 'shopping-cart', 'home', 'utensils', 'book', 'heart', 'star']; | |
| const colors = ['red', 'blue', 'green', 'purple', 'yellow', 'indigo', 'pink', 'gray']; | |
| // Ajouter la nouvelle catégorie | |
| categories.push({ | |
| id, | |
| name, | |
| icon: icons[Math.floor(Math.random() * icons.length)], | |
| color: colors[Math.floor(Math.random() * colors.length)] | |
| }); | |
| saveCategories(); | |
| renderCategories(); | |
| nameInput.value = ''; | |
| showNotification('Catégorie ajoutée avec succès!', 'success'); | |
| } | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=stephaneibens/task-manager-si" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |