0%
JavaScript Moderne : Projet Application de Notes

Projet Notes

Application de Notes Interactive

10-15 min

JavaScript Moderne : Projet d’application de notes

Dans ce tutoriel final, nous allons mettre en pratique tous les concepts modernes de JavaScript vus précédemment en créant une application de prise de notes complète.

Structure du projet

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

Configuration initiale

HTML de base

<!-- 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 de base

/* 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;
}

Implémentation des modules

Configuration

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

Classe 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);
    }
}

Gestionnaire de stockage

// 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;
        }
    }
}

Gestionnaire de notes

// 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);
    }
}

Interface utilisateur

// 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);
            }
        });
    }
}

Gestionnaire de notifications

// 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);
    }
}

Point d’entrée 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() {
        // Formulaire
        this.form.addEventListener('submit', (e) => {
            e.preventDefault();
            this.handleSubmit();
        });
        
        // Gestionnaire de notes
        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');
        });
        
        // Liste de notes
        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();
    }
}

// Démarrage de l'application
const app = new App();
app.init();

Fonctionnalités avancées

Recherche de notes

// 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);
});

Catégories de notes

// 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);
}

Export/Import de notes

// 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;
    }
}

Tests unitaires

// 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');
    });
});

Conclusion

Ce projet met en pratique les concepts modernes de JavaScript :

  • Modules ES pour l’organisation du code
  • Classes et héritage pour la POO
  • Map et Set pour la gestion des données
  • Promises et async/await pour les opérations asynchrones
  • Event Emitter pour la communication entre composants
  • Gestion du stockage local
  • Tests unitaires

Points d’amélioration possibles :

  • Ajout de la validation des données
  • Implémentation du drag & drop pour réorganiser les notes
  • Ajout de la synchronisation avec une API backend
  • Support du mode hors ligne avec Service Workers
  • Ajout de raccourcis clavier

Besoin de réviser les structures de données modernes ? Consultez notre tutoriel précédent sur Map et Set.

Félicitations ! Vous avez terminé la série de tutoriels sur le JavaScript moderne. N’hésitez pas à revenir sur les concepts précédents en consultant les autres tutoriels de la série.

Commentaires

Les commentaires sont alimentés par GitHub Discussions

Connectez-vous avec GitHub pour participer à la discussion

Lien copié !