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
- Inicie o servidor FastAPI:
uvicorn main:app --reload
- Abra
index.html
no navegador (via Live Server). - 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
🚀 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.