0%
JavaScript Moderno: Proyecto Aplicación de Notas

Proyecto Notas

Aplicación de Notas Interactiva

10-15 min

JavaScript Moderno: Proyecto de aplicación de notas

En este tutorial final, vamos a poner en práctica todos los conceptos modernos de JavaScript vistos anteriormente creando una aplicación completa para tomar notas.

Estructura del proyecto

notes-app/
├── index.html
├── styles/
│   └── main.css
└── src/
    ├── main.js
    ├── modules/
    │   ├── notes/
    │   │   ├── Note.js
    │   │   ├── NotesManager.js
    │   │   └── NotesStorage.js
    │   ├── ui/
    │   │   ├── NoteForm.js
    │   │   ├── NoteList.js
    │   │   └── Toast.js
    │   └── utils/
    │       ├── EventEmitter.js
    │       └── Storage.js
    └── config.js

Configuración inicial

HTML básico

<!-- index.html -->
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Notes App</title>
    <link rel="stylesheet" href="styles/main.css">
</head>
<body>
    <div class="container">
        <header>
            <h1>Notes App</h1>
        </header>
        
        <main>
            <form id="note-form" class="note-form">
                <input type="text" id="note-title" placeholder="Titre" required>
                <textarea id="note-content" placeholder="Contenu" required></textarea>
                <button type="submit">Ajouter</button>
            </form>
            
            <div id="notes-list" class="notes-list"></div>
        </main>
    </div>
    
    <div id="toast" class="toast"></div>
    
    <script type="module" src="src/main.js"></script>
</body>
</html>

CSS básico

/* styles/main.css */
:root {
    --primary-color: #4CAF50;
    --secondary-color: #2196F3;
    --error-color: #f44336;
    --text-color: #333;
    --bg-color: #f5f5f5;
}

body {
    font-family: 'Segoe UI', sans-serif;
    line-height: 1.6;
    margin: 0;
    padding: 0;
    background-color: var(--bg-color);
    color: var(--text-color);
}

.container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 2rem;
}

.note-form {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    margin-bottom: 2rem;
}

.notes-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: 1rem;
}

.note-card {
    background: white;
    padding: 1rem;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

.toast {
    position: fixed;
    bottom: 20px;
    right: 20px;
    padding: 1rem;
    border-radius: 4px;
    color: white;
    opacity: 0;
    transition: opacity 0.3s;
}

.toast.show {
    opacity: 1;
}

Implementación de los módulos

Configuración

// src/config.js
export const CONFIG = {
    STORAGE_KEY: 'notes-app-data',
    MAX_TITLE_LENGTH: 50,
    MAX_CONTENT_LENGTH: 1000,
    TOAST_DURATION: 3000
};

Clase Note

// src/modules/notes/Note.js
export class Note {
    constructor({ id = Date.now(), title, content, createdAt = new Date() }) {
        this.id = id;
        this.title = title;
        this.content = content;
        this.createdAt = new Date(createdAt);
    }
    
    toJSON() {
        return {
            id: this.id,
            title: this.title,
            content: this.content,
            createdAt: this.createdAt.toISOString()
        };
    }
    
    static fromJSON(json) {
        return new Note(json);
    }
}

Gestor de almacenamiento

// src/modules/utils/Storage.js
export class Storage {
    constructor(key) {
        this.key = key;
    }
    
    save(data) {
        try {
            localStorage.setItem(this.key, JSON.stringify(data));
            return true;
        } catch (error) {
            console.error('Erreur de sauvegarde:', error);
            return false;
        }
    }
    
    load() {
        try {
            const data = localStorage.getItem(this.key);
            return data ? JSON.parse(data) : null;
        } catch (error) {
            console.error('Erreur de chargement:', error);
            return null;
        }
    }
}

Gestor de notas

// src/modules/notes/NotesManager.js
import { Note } from './Note.js';
import { Storage } from '../utils/Storage.js';
import { CONFIG } from '../../config.js';
import { EventEmitter } from '../utils/EventEmitter.js';

export class NotesManager extends EventEmitter {
    constructor() {
        super();
        this.storage = new Storage(CONFIG.STORAGE_KEY);
        this.notes = new Map();
        this.loadNotes();
    }
    
    loadNotes() {
        const data = this.storage.load() || [];
        data.forEach(noteData => {
            const note = Note.fromJSON(noteData);
            this.notes.set(note.id, note);
        });
        this.emit('notesLoaded', Array.from(this.notes.values()));
    }
    
    saveNotes() {
        const data = Array.from(this.notes.values()).map(note => note.toJSON());
        this.storage.save(data);
    }
    
    addNote(title, content) {
        const note = new Note({ title, content });
        this.notes.set(note.id, note);
        this.saveNotes();
        this.emit('noteAdded', note);
        return note;
    }
    
    deleteNote(id) {
        const note = this.notes.get(id);
        if (note) {
            this.notes.delete(id);
            this.saveNotes();
            this.emit('noteDeleted', note);
            return true;
        }
        return false;
    }
    
    updateNote(id, { title, content }) {
        const note = this.notes.get(id);
        if (note) {
            Object.assign(note, { title, content });
            this.saveNotes();
            this.emit('noteUpdated', note);
            return note;
        }
        return null;
    }
    
    getNotes() {
        return Array.from(this.notes.values())
            .sort((a, b) => b.createdAt - a.createdAt);
    }
}

Interfaz de usuario

// src/modules/ui/NoteList.js
export class NoteList {
    constructor(container) {
        this.container = container;
    }
    
    render(notes) {
        this.container.innerHTML = notes.map(note => `
            <div class="note-card" data-id="${note.id}">
                <h3>${this.escapeHtml(note.title)}</h3>
                <p>${this.escapeHtml(note.content)}</p>
                <div class="note-actions">
                    <button class="edit-btn">Modifier</button>
                    <button class="delete-btn">Supprimer</button>
                </div>
            </div>
        `).join('');
        
        this.attachEventListeners();
    }
    
    escapeHtml(unsafe) {
        return unsafe
            .replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;")
            .replace(/"/g, "&quot;")
            .replace(/'/g, "&#039;");
    }
    
    attachEventListeners() {
        this.container.addEventListener('click', (e) => {
            const noteCard = e.target.closest('.note-card');
            if (!noteCard) return;
            
            const noteId = noteCard.dataset.id;
            
            if (e.target.classList.contains('delete-btn')) {
                this.onDelete?.(noteId);
            } else if (e.target.classList.contains('edit-btn')) {
                this.onEdit?.(noteId);
            }
        });
    }
}

Gestor de notificaciones

// src/modules/ui/Toast.js
export class Toast {
    constructor(container) {
        this.container = container;
        this.timeoutId = null;
    }
    
    show(message, type = 'info') {
        if (this.timeoutId) {
            clearTimeout(this.timeoutId);
        }
        
        this.container.textContent = message;
        this.container.className = `toast ${type} show`;
        
        this.timeoutId = setTimeout(() => {
            this.container.classList.remove('show');
        }, 3000);
    }
}

Punto de entrada principal

// src/main.js
import { NotesManager } from './modules/notes/NotesManager.js';
import { NoteList } from './modules/ui/NoteList.js';
import { Toast } from './modules/ui/Toast.js';

class App {
    constructor() {
        this.notesManager = new NotesManager();
        this.noteList = new NoteList(document.getElementById('notes-list'));
        this.toast = new Toast(document.getElementById('toast'));
        
        this.form = document.getElementById('note-form');
        this.titleInput = document.getElementById('note-title');
        this.contentInput = document.getElementById('note-content');
        
        this.setupEventListeners();
    }
    
    setupEventListeners() {
        // Formulario
        this.form.addEventListener('submit', (e) => {
            e.preventDefault();
            this.handleSubmit();
        });
        
        // Gestor de notas
        this.notesManager.on('noteAdded', () => {
            this.refreshNotes();
            this.toast.show('Note ajoutée avec succès', 'success');
        });
        
        this.notesManager.on('noteDeleted', () => {
            this.refreshNotes();
            this.toast.show('Note supprimée', 'info');
        });
        
        this.notesManager.on('noteUpdated', () => {
            this.refreshNotes();
            this.toast.show('Note mise à jour', 'success');
        });
        
        // Lista de notas
        this.noteList.onDelete = (id) => {
            if (confirm('Voulez-vous vraiment supprimer cette note ?')) {
                this.notesManager.deleteNote(id);
            }
        };
        
        this.noteList.onEdit = (id) => {
            const note = this.notesManager.notes.get(id);
            if (note) {
                this.titleInput.value = note.title;
                this.contentInput.value = note.content;
                this.form.dataset.editId = id;
                this.form.querySelector('button').textContent = 'Modifier';
            }
        };
    }
    
    handleSubmit() {
        const title = this.titleInput.value.trim();
        const content = this.contentInput.value.trim();
        
        if (!title || !content) {
            this.toast.show('Veuillez remplir tous les champs', 'error');
            return;
        }
        
        const editId = this.form.dataset.editId;
        
        if (editId) {
            this.notesManager.updateNote(editId, { title, content });
            delete this.form.dataset.editId;
            this.form.querySelector('button').textContent = 'Ajouter';
        } else {
            this.notesManager.addNote(title, content);
        }
        
        this.form.reset();
    }
    
    refreshNotes() {
        this.noteList.render(this.notesManager.getNotes());
    }
    
    init() {
        this.refreshNotes();
    }
}

// Inicio de la aplicación
const app = new App();
app.init();

Funcionalidades avanzadas

Búsqueda de notas

// src/modules/notes/NotesManager.js
searchNotes(query) {
    const searchTerm = query.toLowerCase();
    return this.getNotes().filter(note => 
        note.title.toLowerCase().includes(searchTerm) ||
        note.content.toLowerCase().includes(searchTerm)
    );
}

// src/main.js
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', (e) => {
    const query = e.target.value;
    const filteredNotes = this.notesManager.searchNotes(query);
    this.noteList.render(filteredNotes);
});

Categorías de notas

// src/modules/notes/Note.js
constructor({ id = Date.now(), title, content, category = 'default', createdAt = new Date() }) {
    this.id = id;
    this.title = title;
    this.content = content;
    this.category = category;
    this.createdAt = new Date(createdAt);
}

// src/modules/notes/NotesManager.js
getNotesByCategory(category) {
    return this.getNotes().filter(note => note.category === category);
}

Exportación/Importación de notas

// src/modules/notes/NotesManager.js
exportNotes() {
    const data = JSON.stringify(Array.from(this.notes.values())
        .map(note => note.toJSON()), null, 2);
    
    const blob = new Blob([data], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    
    const a = document.createElement('a');
    a.href = url;
    a.download = `notes-${new Date().toISOString()}.json`;
    a.click();
    
    URL.revokeObjectURL(url);
}

async importNotes(file) {
    try {
        const text = await file.text();
        const data = JSON.parse(text);
        
        data.forEach(noteData => {
            const note = Note.fromJSON(noteData);
            this.notes.set(note.id, note);
        });
        
        this.saveNotes();
        this.emit('notesLoaded', this.getNotes());
        return true;
    } catch (error) {
        console.error('Erreur d'importation:', error);
        return false;
    }
}

Pruebas unitarias

// tests/NotesManager.test.js
import { NotesManager } from '../src/modules/notes/NotesManager.js';

describe('NotesManager', () => {
    let notesManager;
    
    beforeEach(() => {
        localStorage.clear();
        notesManager = new NotesManager();
    });
    
    test('should add a new note', () => {
        const note = notesManager.addNote('Test', 'Content');
        expect(notesManager.notes.size).toBe(1);
        expect(note.title).toBe('Test');
    });
    
    test('should delete a note', () => {
        const note = notesManager.addNote('Test', 'Content');
        expect(notesManager.deleteNote(note.id)).toBe(true);
        expect(notesManager.notes.size).toBe(0);
    });
    
    test('should update a note', () => {
        const note = notesManager.addNote('Test', 'Content');
        const updated = notesManager.updateNote(note.id, {
            title: 'Updated',
            content: 'New content'
        });
        
        expect(updated.title).toBe('Updated');
        expect(updated.content).toBe('New content');
    });
});

Conclusión

Este proyecto pone en práctica los conceptos modernos de JavaScript:

  • Módulos ES para la organización del código
  • Clases y herencia para la POO
  • Map y Set para la gestión de datos
  • Promises y async/await para las operaciones asíncronas
  • Event Emitter para la comunicación entre componentes
  • Gestión del almacenamiento local
  • Pruebas unitarias

Posibles puntos de mejora:

  • Añadir validación de datos
  • Implementar drag & drop para reorganizar las notas
  • Añadir sincronización con una API de backend
  • Soporte del modo sin conexión con Service Workers
  • Añadir atajos de teclado

¿Necesitas repasar las estructuras de datos modernas? Consulta nuestro tutorial anterior sobre Map y Set. ¡Enhorabuena! Has terminado la serie de tutoriales sobre JavaScript moderno. No dudes en volver a los conceptos anteriores consultando los demás tutoriales de la serie.

Comentarios

Los comentarios funcionan con GitHub Discussions

Inicia sesión con GitHub para participar en la discusión

¡Enlace copiado!