0%
Modern JavaScript: Notes App Project

Notes Project

Interactive Notes App

10-15 min

Modern JavaScript: Notes App Project

In this final tutorial, we’ll put into practice all the modern JavaScript concepts covered so far by building a complete note-taking application.

Project Structure

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

Initial Setup

Basic HTML

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

Basic CSS

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

Module Implementation

Configuration

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

The Note Class

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

Storage Manager

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

Notes Manager

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

User Interface

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

Notification Manager

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

Main Entry Point

// 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() {
        // Form
        this.form.addEventListener('submit', (e) => {
            e.preventDefault();
            this.handleSubmit();
        });
        
        // Notes manager
        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');
        });
        
        // Notes list
        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();
    }
}

// Start the application
const app = new App();
app.init();

Advanced Features

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

Note Categories

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

Exporting and Importing 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;
    }
}

Unit Tests

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

This project puts the following modern JavaScript concepts into practice:

  • ES modules for code organization
  • Classes and inheritance for OOP
  • Map and Set for data management
  • Promises and async/await for asynchronous operations
  • Event Emitter for communication between components
  • Local storage management
  • Unit tests

Possible improvements:

  • Adding data validation
  • Implementing drag and drop to reorder notes
  • Adding synchronization with a backend API
  • Supporting offline mode with Service Workers
  • Adding keyboard shortcuts

Need to review modern data structures? Check out our previous tutorial on Map and Set. Congratulations! You’ve completed the modern JavaScript tutorial series. Feel free to revisit earlier concepts by checking out the other tutorials in the series.

Comments

Comments are powered by GitHub Discussions

Sign in with GitHub to join the discussion

Link copied!