Projeto Notas
Aplicativo de Notas Interativo
10-15 min
JavaScript Moderno: Projeto de aplicativo de notas
Neste tutorial final, vamos colocar em prática todos os conceitos modernos de JavaScript vistos anteriormente criando um aplicativo completo de anotações.
Estrutura do projeto
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
Configuração 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;
}
Implementação dos módulos
Configuração
// 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);
}
}
Gerenciador de armazenamento
// 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;
}
}
}
Gerenciador 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);
}
}
Interface do usuário
// 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);
}
});
}
}
Gerenciador de notificações
// 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);
}
}
Ponto 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() {
// Formulário
this.form.addEventListener('submit', (e) => {
e.preventDefault();
this.handleSubmit();
});
// Gerenciador 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();
}
}
// Inicialização do aplicativo
const app = new App();
app.init();
Funcionalidades avançadas
Busca 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);
});
Categorias 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);
}
Exportação/Importação 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;
}
}
Testes unitários
// 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');
});
});
Conclusão
Este projeto coloca em prática os conceitos modernos de JavaScript:
- Módulos ES para a organização do código
- Classes e herança para a POO
- Map e Set para o gerenciamento de dados
- Promises e async/await para operações assíncronas
- Event Emitter para a comunicação entre componentes
- Gerenciamento do armazenamento local
- Testes unitários
Possíveis pontos de melhoria:
- Adição de validação de dados
- Implementação de drag & drop para reorganizar as notas
- Adição de sincronização com uma API de backend
- Suporte ao modo offline com Service Workers
- Adição de atalhos de teclado
Precisa revisar as estruturas de dados modernas? Confira nosso tutorial anterior sobre Map e Set. Parabéns! Você concluiu a série de tutoriais sobre JavaScript moderno. Sinta-se à vontade para revisitar os conceitos anteriores consultando os outros tutoriais da série.
Comentários
Os comentários são alimentados pelo GitHub Discussions
Conecte-se com o GitHub para participar da discussão