Skip to content

Capítulo 4.6: Criação de uma Interface Simples para API

Tempo de estudo: 1 hora e 15 minutos


1. Montagem Final: Lançamento do "Centro de Controle de Missões"

Estudamos todos os sistemas separadamente: configuramos a "antena" (Fetch), aprendemos a enviar "comandos" (GET, POST, DELETE), desenvolvemos "protocolos de emergência" (tratamento de erros).

Chegou a hora de juntar todos os componentes e lançar o nosso CCM — uma interface completa e interativa para gerenciar a frota espacial.

Nosso objetivo:

  • Criar uma interface unificada, limpa e clara.
  • Implementar o ciclo CRUD completo: criação, exibição, atualização e exclusão de naves.
  • Adicionar feedback visual para o usuário (carregando, sucesso, erro).

💡 Analogia Espacial:

Estamos a passar de consoles de teste individuais para o ecrã principal do CCM. Deve ter todos os botões e indicadores necessários para que um único operador possa gerir toda a frota sem ter de alternar entre dezenas de sistemas diferentes.


2. Design da Interface: "Painel de Controle"

Precisaremos de um HTML mais estruturado. Usaremos "cartões" para exibir as naves e uma janela modal para editá-las.

Atualizando index.html:

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>CCM v2.0 - Gerenciamento de Frota</title>
    <link rel="stylesheet" href="style.css"> <!-- Conectando estilos -->
</head>
<body>
    <header>
        <h1>Painel de controle da frota espacial</h1>
    </header>

    <main>
        <section id="fleet-controls">
            <button id="load-fleet-btn">Atualizar lista da frota</button>
            <button id="show-create-form-btn">Lançar nova nave</button>
        </section>

        <section id="fleet-display">
            <h2>Composição atual da frota</h2>
            <div id="fleet-list" class="cards-container">
                <!-- Os cartões das naves estarão aqui -->
            </div>
        </section>
    </main>

    <!-- Janela modal para criação/edição (inicialmente oculta) -->
    <div id="modal" class="modal-overlay" style="display: none;">
        <div class="modal-content">
            <h2 id="modal-title">Lançar nova nave</h2>
            <form id="ship-form">
                <input type="hidden" id="ship-id">
                <input type="text" id="ship-name" placeholder="Nome" required>
                <input type="text" id="ship-type" placeholder="Tipo" required>
                <input type="number" id="ship-year" placeholder="Ano de lançamento" required>
                <input type="text" id="ship-status" placeholder="Status" required>
                <div class="modal-actions">
                    <button type="submit" id="save-btn">Salvar</button>
                    <button type="button" id="cancel-btn">Cancelar</button>
                </div>
            </form>
            <div id="notification-area"></div>
        </div>
    </div>

    <script src="api.js"></script>
    <script src="app.js"></script>
</body>
</html>


3. Adicionando design "espacial": style.css

Crie o arquivo style.css para que o nosso CCM tenha uma aparência digna.

/* style.css */
body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background-color: #1a1a2e;
    color: #e0e0e0;
    margin: 0;
    padding: 20px;
}
header { text-align: center; margin-bottom: 20px; }
button {
    background-color: #4a4e69;
    color: white;
    border: none;
    padding: 10px 15px;
    border-radius: 5px;
    cursor: pointer;
    transition: background-color 0.3s;
}
button:hover { background-color: #6a6e94; }
.cards-container {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: 20px;
}
.card {
    background-color: #2a2a4e;
    border: 1px solid #4a4e69;
    border-radius: 8px;
    padding: 15px;
}
.card h3 { margin-top: 0; color: #9394a5; }
.card-actions { margin-top: 15px; }

/* Estilos para a janela modal */
.modal-overlay {
    position: fixed;
    top: 0; left: 0;
    width: 100%; height: 100%;
    background-color: rgba(0,0,0,0.7);
    display: flex;
    justify-content: center;
    align-items: center;
}
.modal-content {
    background: #1a1a2e;
    padding: 20px;
    border-radius: 8px;
    border: 1px solid #4a4e69;
    width: 90%;
    max-width: 500px;
}
#ship-form input {
    width: calc(100% - 20px);
    padding: 10px;
    margin-bottom: 10px;
    border-radius: 4px;
    border: 1px solid #4a4e69;
    background-color: #2a2a4e;
    color: white;
}
.modal-actions { text-align: right; }


4. Recompilação completa da lógica: app.js

Agora escreveremos a versão final de app.js, unindo todo o nosso conhecimento.

// app.js

// --- Elementos DOM ---
const loadFleetBtn = document.getElementById('load-fleet-btn');
const fleetListContainer = document.getElementById('fleet-list');
const modal = document.getElementById('modal');
const modalTitle = document.getElementById('modal-title');
const shipForm = document.getElementById('ship-form');
const saveBtn = document.getElementById('save-btn');
const cancelBtn = document.getElementById('cancel-btn');
const showCreateFormBtn = document.getElementById('show-create-form-btn');
const notificationArea = document.getElementById('notification-area');

// --- Funções para UI ---

function showNotification(message, isError = false) {
    notificationArea.textContent = message;
    notificationArea.style.color = isError ? '#ff6b6b' : '#6bff6b';
}

function openModalForCreate() {
    shipForm.reset();
    document.getElementById('ship-id').value = '';
    modalTitle.textContent = 'Lançar nova espaçonave';
    modal.style.display = 'flex';
}

function openModalForEdit(ship) {
    shipForm.reset();
    document.getElementById('ship-id').value = ship.id;
    document.getElementById('ship-name').value = ship.name;
    document.getElementById('ship-type').value = ship.type;
    document.getElementById('ship-year').value = ship.launch_year;
    document.getElementById('ship-status').value = ship.status;
    modalTitle.textContent = `Edição: ${ship.name}`;
    modal.style.display = 'flex';
}

function closeModal() {
    modal.style.display = 'none';
    notificationArea.textContent = '';
}

function createShipCard(ship) {
    const card = document.createElement('div');
    card.className = 'card';
    card.innerHTML = `
        <h3>${ship.name} (ID: ${ship.id})</h3>
        <p>Tipo: ${ship.type}</p>
        <p>Ano de lançamento: ${ship.launch_year}</p>
        <p>Status: ${ship.status}</p>
        <div class="card-actions">
            <button class="edit-btn" data-ship-id="${ship.id}">Editar</button>
            <button class="delete-btn" data-ship-id="${ship.id}">Desativar</button>
        </div>
    `;
    return card;
}

// --- Lógica da API e Exibição ---

async function fetchAndDisplayFleet() {
    try {
        fleetListContainer.innerHTML = '<p>Carregando telemetria...</p>';
        const ships = await apiRequest('/spaceships');

        fleetListContainer.innerHTML = '';
        if (ships.length === 0) {
            fleetListContainer.innerHTML = '<p>Nenhuma espaçonave no registro.</p>';
            return;
        }
        ships.forEach(ship => {
            const card = createShipCard(ship);
            fleetListContainer.appendChild(card);
        });
    } catch (error) {
        fleetListContainer.innerHTML = `<p style="color: #ff6b6b;">Erro ao carregar frota: ${error.message}</p>`;
    }
}

async function handleSaveShip(event) {
    event.preventDefault();
    const shipId = document.getElementById('ship-id').value;
    const shipData = {
        name: document.getElementById('ship-name').value,
        type: document.getElementById('ship-type').value,
        launch_year: parseInt(document.getElementById('ship-year').value),
        status: document.getElementById('ship-status').value
    };

    try {
        let response;
        if (shipId) {
            // Atualização (PUT)
            response = await apiRequest(`/spaceships/${shipId}`, {
                method: 'PUT',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(shipData)
            });
            showNotification(`Espaçonave "${response.name}" atualizada com sucesso!`);
        } else {
            // Criação (POST)
            response = await apiRequest('/spaceships', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(shipData)
            });
            showNotification(`Espaçonave "${response.name}" lançada com sucesso! ID: ${response.id}`);
        }

        setTimeout(() => {
            closeModal();
            fetchAndDisplayFleet();
        }, 1500);

    } catch (error) {
        showNotification(error.message, true);
    }
}

async function handleDeleteShip(shipId) {
    if (!confirm(`Tem certeza de que deseja desativar a espaçonave com ID ${shipId}?`)) return;

    try {
        await apiRequest(`/spaceships/${shipId}`, { method: 'DELETE' });
        alert('Espaçonave desativada com sucesso.');
        fetchAndDisplayFleet();
    } catch (error) {
        alert(`Erro ao desativar: ${error.message}`);
    }
}

// --- Manipuladores de Eventos ---

document.addEventListener('DOMContentLoaded', fetchAndDisplayFleet);
loadFleetBtn.addEventListener('click', fetchAndDisplayFleet);
showCreateFormBtn.addEventListener('click', openModalForCreate);
cancelBtn.addEventListener('click', closeModal);
shipForm.addEventListener('submit', handleSaveShip);

fleetListContainer.addEventListener('click', async (event) => {
    const target = event.target;
    if (target.classList.contains('delete-btn')) {
        handleDeleteShip(target.dataset.shipId);
    }
    if (target.classList.contains('edit-btn')) {
        try {
            const ship = await apiRequest(`/spaceships/${target.dataset.shipId}`);
            openModalForEdit(ship);
        } catch (error) {
            alert(`Não foi possível carregar os dados para edição: ${error.message}`);
        }
    }
});

5. Testes Finais

  1. Inicie o servidor FastAPI: uvicorn main:app --reload
  2. Abra index.html no navegador (via Live Server).
  3. Verifique o ciclo completo:
    • A lista de naves deve carregar automaticamente.
    • Clique em "Lançar nova espaçonave", preencha o formulário e salve. Certifique-se de que a nova espaçonave aparece na lista.
    • Clique em "Editar" em qualquer espaçonave, altere os dados e salve. Certifique-se de que a informação foi atualizada.
    • Clique em "Desativar" em qualquer espaçonave, confirme a ação. Certifique-se de que ela desapareceu da lista.
    • Verifique todos os cenários de erro (dados inválidos, servidor parado).

Questionário de Fixação

1. Uma janela modal em uma interface web é...

2. O evento `DOMContentLoaded` ocorre quando...

3. Por que na versão final usamos um único formulário tanto para criação quanto para edição?

4. `data-ship-id="${ship.id}"` é um exemplo de...

5. A refatoração do código (por exemplo, mover a lógica para `api.js`) é necessária para...


🚀 Resumo do Capítulo

Você concluiu com sucesso a construção e o lançamento do seu "Centro de Controle de Voos".

  • 🖥️ Você criou uma interface HTML/CSS estruturada e estilizada.
  • ⚙️ Você escreveu código JavaScript limpo e modular, implementando um ciclo CRUD completo.
  • 🛰️ Seu frontend agora pode gerenciar totalmente o backend criado com FastAPI.

Parabéns pela conclusão bem-sucedida do Capítulo 4! Você percorreu todo o caminho, desde o envio de uma simples solicitação fetch até a criação de um aplicativo web completo que interage com sua própria API.