Projet Notes
Application de Notes Interactive
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, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
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