⚡ Capítulo 20: Projeto Capstone JS Vanilla — Aplicação SPA Interativa com DOM, LocalStorage e Fetch API

🎯 Objetivos da Aula

Ao final desta aula, você será capaz de:

  1. Integrar todos os conceitos da Fase 04 (JS ES6+, Seletores DOM, Manipulação de Nós, Eventos, async/await, Fetch API e LocalStorage).
  2. Construir um Gerenciador Interativo de Cadastro de Alunos 100% em cliente (Single Page Application — SPA).
  3. Persistir e recuperar o estado da aplicação no navegador via localStorage.getItem e localStorage.setItem.
  4. Consumir dados assíncronos de uma API simulada usando fetch() com tratamento de erros try/catch.

🧠 1. Arquitetura da Aplicação Interativa em JS

A aplicação conecta a interface HTML com o estado dinâmico mantido pelo JavaScript na memória do navegador:

flowchart TD
    subgraph UI ["Interface HTML (DOM)"]
        Form["<form id='form-aluno'>"]
        Table["<table id='tabela-alunos'>"]
        BtnAPI["<button id='btn-carregar-api'>"]
    end
    
    subgraph AppLogic ["Lógica JavaScript (app.js)"]
        State["Vetor de Estado: let alunos = []"]
        EventHandlers["Tratadores de Evento (addEventListener)"]
        StorageHandler["Sincronização LocalStorage (JSON.stringify)"]
        APIHandler["Consumo Assíncrono (fetch + async/await)"]
    end

    Form -->|"submit"| EventHandlers
    EventHandlers --> State
    State --> StorageHandler & Table
    BtnAPI -->|"click"| APIHandler --> State

💻 2. Código Completo do Projeto Integrado (app.js)

Let’s write the complete, clean JavaScript code for the Capstone project:

// =================================================================
// PROJETO CAPSTONE FASE 04: SISTEMA INTERATIVO DE GESTÃO DE ALUNOS
// =================================================================
 
// 1. ESTADO DA APLICAÇÃO (Carregado do LocalStorage)
let listaAlunos = JSON.parse(localStorage.getItem("@portal:alunos")) || [];
 
// 2. SELEÇÃO DOS ELEMENTOS DO DOM
const formAluno = document.getElementById("form-aluno");
const inputMatricula = document.getElementById("input-matricula");
const inputNome = document.getElementById("input-nome");
const inputCurso = document.getElementById("input-curso");
const tabelaCorpo = document.getElementById("tabela-corpo-alunos");
const btnCarregarDadosAPI = document.getElementById("btn-carregar-api");
const mensagemFeedback = document.getElementById("mensagem-feedback");
 
// 3. INICIALIZAÇÃO DA APLICAÇÃO
document.addEventListener("DOMContentLoaded", () => {
    console.log(">> Aplicação SPA carregada. Registros salvos:", listaAlunos.length);
    renderizarTabela();
});
 
// 4. CADASTRO DE NOVO ALUNO (Evento Submit)
formAluno.addEventListener("submit", (evento) => {
    evento.preventDefault(); // Impede o recarregamento da página
 
    const matricula = inputMatricula.value.trim();
    const nome = inputNome.value.trim();
    const curso = inputCurso.value.trim();
 
    if (!matricula || !nome || !curso) {
        exibirFeedback("Preencha todos os campos obrigatórios!", "erro");
        return;
    }
 
    // Criando novo objeto de aluno
    const novoAluno = {
        id: Date.now(),
        matricula: matricula,
        nome: nome,
        curso: curso,
        dataCadastro: new Date().toLocaleDateString("pt-BR")
    };
 
    listaAlunos.push(novoAluno);
    salvarEstado();
    renderizarTabela();
    formAluno.reset();
    exibirFeedback("Aluno cadastrado com sucesso!", "sucesso");
});
 
// 5. RENDERIZAÇÃO DA TABELA NO DOM
function renderizarTabela() {
    tabelaCorpo.innerHTML = ""; // Limpa a tabela antes de renderizar
 
    if (listaAlunos.length === 0) {
        tabelaCorpo.innerHTML = `
            <tr>
                <td colspan="5" style="text-align: center; color: #777;">
                    Nenhum aluno cadastrado no momento.
                </td>
            </tr>
        `;
        return;
    }
 
    listaAlunos.forEach((aluno) => {
        const tr = document.createElement("tr");
 
        tr.innerHTML = `
            <td>${aluno.matricula}</td>
            <td><strong>${aluno.nome}</strong></td>
            <td>${aluno.curso}</td>
            <td>${aluno.dataCadastro}</td>
            <td>
                <button class="btn-excluir" onclick="removerAluno(${aluno.id})">❌ Excluir</button>
            </td>
        `;
 
        tabelaCorpo.appendChild(tr);
    });
}
 
// 6. REMOÇÃO DE ALUNO DO ESTADO
window.removerAluno = function(id) {
    listaAlunos = listaAlunos.filter(aluno => aluno.id !== id);
    salvarEstado();
    renderizarTabela();
    exibirFeedback("Registro de aluno removido com sucesso.", "alerta");
};
 
// 7. PERSISTÊNCIA NO LOCALSTORAGE
function salvarEstado() {
    localStorage.setItem("@portal:alunos", JSON.stringify(listaAlunos));
}
 
// 8. CONSUMO ASSÍNCRONO DE API COM FETCH & ASYNC/AWAIT
btnCarregarDadosAPI.addEventListener("click", async () => {
    exibirFeedback("Carregando dados da API externa...", "info");
    
    try {
        // Simulando requisicao a API REST externa (JSONPlaceholder / API de Testes)
        const resposta = await fetch("https://jsonplaceholder.typicode.com/users?_limit=3");
        
        if (!resposta.ok) {
            throw new Error(`Erro HTTP: status ${resposta.status}`);
        }
 
        const usuariosAPI = await resposta.json();
 
        // Mapeando dados da API para o formato do nosso sistema
        usuariosAPI.forEach(usuario => {
            listaAlunos.push({
                id: Date.now() + Math.random(),
                matricula: `2026${usuario.id}`,
                nome: usuario.name,
                curso: "Engenharia de Software (Importado)",
                dataCadastro: new Date().toLocaleDateString("pt-BR")
            });
        });
 
        salvarEstado();
        renderizarTabela();
        exibirFeedback("3 alunos importados da API com sucesso!", "sucesso");
 
    } catch (erro) {
        console.error(">> Falha na requisição da API:", erro);
        exibirFeedback("Falha ao conectar com a API externa.", "erro");
    }
});
 
// 9. FUNÇÃO DE FEEDBACK VISUAL NA TELA
function exibirFeedback(mensagem, tipo) {
    mensagemFeedback.innerText = mensagem;
    mensagemFeedback.className = `feedback feedback-${tipo}`;
    mensagemFeedback.style.display = "block";
 
    setTimeout(() => {
        mensagemFeedback.style.display = "none";
    }, 4000);
}

⚔️ 3. Desafios Práticos (Exercícios 30/50/20)

🥉 Nível Bronze (Fixação)

  1. Para que serve o método JSON.stringify() ao salvar dados no localStorage? E por que utilizamos JSON.parse() ao carregar os dados salvos?
  2. O que faz o método evento.preventDefault() dentro de um escutador de evento de envio de formulário (submit)?

🥈 Nível Prata (Aplicação)

  1. Adicione ao projeto a funcionalidade de busca/filtro por nome em tempo real (evento input no campo de busca), renderizando apenas os alunos cujo nome contenha o termo digitado pelo usuário.

🥇 Nível Ouro (Desafio)

  1. Implemente um botão “Limpar Todos os Registros” no sistema, solicitando confirmação via confirm() do JavaScript antes de executar localStorage.clear(), zerar o vetor de estado e atualizar a tabela.

💡 Gabaritos Sanfonados de Resposta

💡 Ver Gabarito do Nível Bronze
  1. O localStorage aceita apenas textos (Strings). O JSON.stringify() converte objetos e arrays JavaScript em String formatada. O JSON.parse() reconverte essa string de volta para objetos e vetores operáveis no JS.
  2. Evita o comportamento padrão dos navegadores de recarregar a página e tentar enviar uma requisição HTTP síncrona ao submeter um <form>, permitindo o processamento 100% em cliente (SPA).
💡 Ver Gabarito do Nível Prata & Ouro (Código JS)
// Gabarito Nível Ouro: Botão Limpar Tudo
const btnLimparTudo = document.getElementById("btn-limpar-tudo");
 
btnLimparTudo.addEventListener("click", () => {
    const confirmacao = confirm("Tem certeza que deseja apagar TODOS os registros de alunos?");
    if (confirmacao) {
        listaAlunos = [];
        localStorage.removeItem("@portal:alunos");
        renderizarTabela();
        exibirFeedback("Todos os registros foram apagados.", "alerta");
    }
});

📚 Referências Teóricas Oficiais

  • ECMA INTERNATIONAL. ECMAScript 2023 Specification — Async/Await & Promises.
  • WHATWG HTML SPECIFICATION. Web Storage API — LocalStorage & SessionStorage.

⬅️ Capítulo Anterior (Boas Práticas de JS) | Voltar ao Sumário da Fase 04 | Avançar para a Fase 05 (TypeScript) ➡️