Inicio Electron - Recordatorios App | Parte 5 - Búsqueda y Filtros
Post
Cancel

Electron - Recordatorios App | Parte 5 - Búsqueda y Filtros

Parte 5: Búsqueda y Filtros

Objetivo

Implementar sistema de búsqueda en tiempo real y filtros por categoría, estado y ordenamiento.

Paso 1: Actualizar core.js con funciones de filtrado

Agrega estas funciones en renderer/js/core.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// ===========================================
// BÚSQUEDA Y FILTROS
// ===========================================

// Manejar búsqueda
function handleSearch() {
    const searchTerm = searchInput.value.trim();
    
    if (searchTerm) {
        clearSearchBtn.style.display = 'block';
    } else {
        clearSearchBtn.style.display = 'none';
    }
    
    renderReminders();
}

// Limpiar búsqueda
function clearSearch() {
    searchInput.value = '';
    clearSearchBtn.style.display = 'none';
    renderReminders();
}

// Limpiar todos los filtros
function clearAllFilters() {
    // Limpiar búsqueda
    if (searchInput) {
        searchInput.value = '';
        if (clearSearchBtn) {
            clearSearchBtn.style.display = 'none';
        }
    }
    
    // Restablecer filtros
    const filterCategory = document.getElementById('filterCategory');
    const filterStatus = document.getElementById('filterStatus');
    const sortBy = document.getElementById('sortBy');
    
    if (filterCategory) filterCategory.value = '';
    if (filterStatus) filterStatus.value = '';
    if (sortBy) sortBy.value = 'newest';
    
    renderReminders();
    showNotification('Filtros limpiados', 'info');
}

// Obtener recordatorios filtrados (actualizar esta función)
function getFilteredReminders() {
    const categoryFilter = document.getElementById('filterCategory')?.value || '';
    const statusFilter = document.getElementById('filterStatus')?.value || '';
    const sortBy = document.getElementById('sortBy')?.value || 'newest';
    const searchTerm = searchInput?.value.toLowerCase().trim() || '';
    
    // Aplicar filtros
    let filtered = reminders.filter(reminder => {
        // Filtro por categoría
        const categoryMatch = !categoryFilter || reminder.category === categoryFilter;
        
        // Filtro por estado
        let statusMatch = true;
        if (statusFilter === 'pending') {
            statusMatch = !reminder.completed;
        } else if (statusFilter === 'completed') {
            statusMatch = reminder.completed;
        }
        
        // Filtro por búsqueda
        const searchMatch = !searchTerm || 
            reminder.title.toLowerCase().includes(searchTerm) ||
            (reminder.description && reminder.description.toLowerCase().includes(searchTerm)) ||
            reminder.category.toLowerCase().includes(searchTerm);
        
        return categoryMatch && statusMatch && searchMatch;
    });
    
    // Aplicar ordenamiento
    filtered.sort((a, b) => {
        switch(sortBy) {
            case 'oldest':
                return new Date(a.createdAt) - new Date(b.createdAt);
            case 'priority':
                const priorityOrder = { 'alta': 3, 'media': 2, 'baja': 1 };
                return priorityOrder[b.priority] - priorityOrder[a.priority];
            case 'dueDate':
                if (!a.dueDate && !b.dueDate) return 0;
                if (!a.dueDate) return 1;
                if (!b.dueDate) return -1;
                return new Date(a.dueDate) - new Date(b.dueDate);
            case 'newest':
            default:
                return new Date(b.createdAt) - new Date(a.createdAt);
        }
    });
    
    return filtered;
}

Paso 2: Crear eventListeners.js

Crea renderer/js/eventListeners.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// Configurar event listeners
function setupEventListeners() {
    // Formulario
    if (reminderForm) {
        reminderForm.addEventListener('submit', handleFormSubmit);
    }
    
    // Búsqueda
    if (searchInput) {
        searchInput.addEventListener('input', handleSearch);
    }
    if (clearSearchBtn) {
        clearSearchBtn.addEventListener('click', clearSearch);
    }
    
    // Tema
    if (darkModeToggle) {
        darkModeToggle.addEventListener('click', toggleTheme);
    }
    
    // Filtros
    const filterCategory = document.getElementById('filterCategory');
    const filterStatus = document.getElementById('filterStatus');
    const sortBy = document.getElementById('sortBy');
    const clearFiltersBtn = document.getElementById('clearFilters');
    
    if (filterCategory) {
        filterCategory.addEventListener('change', renderReminders);
    }
    if (filterStatus) {
        filterStatus.addEventListener('change', renderReminders);
    }
    if (sortBy) {
        sortBy.addEventListener('change', renderReminders);
    }
    if (clearFiltersBtn) {
        clearFiltersBtn.addEventListener('click', clearAllFilters);
    }
    
    // Modal
    if (confirmDeleteBtn) {
        confirmDeleteBtn.addEventListener('click', confirmDelete);
    }
    if (cancelDeleteBtn) {
        cancelDeleteBtn.addEventListener('click', closeDeleteModal);
    }
    if (modalClose) {
        modalClose.addEventListener('click', closeDeleteModal);
    }
    if (deleteModal) {
        deleteModal.addEventListener('click', (e) => {
            if (e.target === deleteModal) closeDeleteModal();
        });
    }
    
    // Cancelar edición
    if (cancelBtn) {
        cancelBtn.addEventListener('click', resetForm);
    }
    
    // Atajos de teclado
    document.addEventListener('keydown', handleKeyboardShortcuts);
}

// Manejar atajos de teclado
function handleKeyboardShortcuts(e) {
    // Escape para cancelar
    if (e.key === 'Escape') {
        if (deleteModal.classList.contains('show')) {
            closeDeleteModal();
        } else if (editingId) {
            resetForm();
        }
    }
    
    // Ctrl/Cmd + K para enfocar búsqueda
    if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
        e.preventDefault();
        searchInput.focus();
    }
}

Paso 3: Actualizar init.js

Modifica el final de renderer/js/init.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Inicializar aplicación
async function initApp() {
    console.log('Iniciando aplicación...');
    
    initDOMElements();
    await loadPreferences();
    applyTheme();
    await loadReminders();
    setupEventListeners();
    renderReminders();
    
    console.log('Aplicación iniciada correctamente');
}

Paso 4: Actualizar index.html con los scripts

Reemplaza la sección de scripts al final del <body>:

1
2
3
4
5
6
7
8
    <!-- Scripts -->
    <script src="../js/variables.js"></script>
    <script src="../js/storage.js"></script>
    <script src="../js/core.js"></script>
    <script src="../js/formHandlers.js"></script>
    <script src="../js/eventListeners.js"></script>
    <script src="../js/init.js"></script>
</body>

Paso 5: Actualizar estilos de filtros

Agrega estos estilos en styles.css:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/* Mejoras para filtros */
.filter-select {
    padding: 8px 15px;
    border: 2px solid var(--border-color);
    border-radius: 8px;
    font-size: 14px;
    background: white;
    cursor: pointer;
    transition: border-color 0.3s;
}

.filter-select:hover {
    border-color: var(--primary-color);
}

.filter-select:focus {
    outline: none;
    border-color: var(--primary-color);
}

.btn-sm {
    padding: 8px 15px;
    font-size: 14px;
}

/* Indicador de búsqueda activa */
.search-bar input:not(:placeholder-shown) {
    border-color: var(--primary-color);
}

/* Estado de filtros activos */
.filters .filter-select:not([value=""]) {
    border-color: var(--primary-color);
    background: rgba(102, 126, 234, 0.1);
}

Paso 6: Actualizar estilos para estado vacío

Mejora el diseño del estado vacío en styles.css:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* Estado vacío mejorado */
.empty-state {
    text-align: center;
    padding: 60px 20px;
    color: var(--text-secondary);
}

.empty-state i {
    font-size: 4rem;
    margin-bottom: 20px;
    opacity: 0.3;
    color: var(--primary-color);
}

.empty-state h3 {
    font-size: 1.5rem;
    margin-bottom: 10px;
    color: var(--text-primary);
}

.empty-state p {
    font-size: 1rem;
    color: var(--text-secondary);
}

Paso 7: Agregar contador de resultados

Actualiza la función renderReminders en core.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Renderizar lista de recordatorios (actualizar)
function renderReminders() {
    const filtered = getFilteredReminders();
    
    if (filtered.length === 0) {
        remindersList.style.display = 'none';
        emptyState.style.display = 'block';
        
        // Actualizar mensaje según contexto
        const hasSearch = searchInput && searchInput.value.trim();
        const hasFilters = document.getElementById('filterCategory')?.value || 
                          document.getElementById('filterStatus')?.value;
        
        if (hasSearch || hasFilters) {
            emptyState.querySelector('h3').textContent = 'No se encontraron resultados';
            emptyState.querySelector('p').textContent = 'Intenta con otros filtros o búsqueda';
        } else {
            emptyState.querySelector('h3').textContent = 'No hay recordatorios';
            emptyState.querySelector('p').textContent = 'Comienza creando tu primer recordatorio';
        }
        
        return;
    }
    
    remindersList.style.display = 'grid';
    emptyState.style.display = 'none';
    
    remindersList.innerHTML = filtered.map(reminder => createReminderCard(reminder)).join('');
    updateStats();
}

Paso 8: Probar funcionalidad

1
npm start

Prueba:

  • ✅ Búsqueda en tiempo real
  • ✅ Filtro por categoría
  • ✅ Filtro por estado (pendiente/completado)
  • ✅ Ordenar por fecha, prioridad
  • ✅ Limpiar filtros
  • ✅ Mensaje contextual cuando no hay resultados
  • ✅ Atajo Ctrl/Cmd+K para buscar
  • ✅ Escape para cancelar

Resultado esperado

  • ✅ Búsqueda funciona instantáneamente
  • ✅ Filtros se aplican correctamente
  • ✅ Ordenamiento funciona
  • ✅ Botón limpiar filtros funcional
  • ✅ Mensajes contextuales apropiados
  • ✅ Atajos de teclado funcionando

Siguiente paso

En la Parte 6, implementaremos el selector de fechas personalizado (calendario).

This post is licensed under CC BY 4.0 by the author.