Skip to content

Capítulo 4.6: Creación de una interfaz sencilla para la API

Tiempo de estudio: 1 hora 15 minutos


1. Ensamblaje final: Lanzamiento del "Centro de Control de Misiones"

Hemos estudiado todos los sistemas por separado: configuramos la "antena" (Fetch), aprendimos a enviar "comandos" (GET, POST, DELETE), desarrollamos "protocolos de emergencia" (manejo de errores).

Ha llegado el momento de unir todos los componentes y lanzar nuestro MCC (Centro de Control de Misiones) — una interfaz completa e interactiva para la gestión de la flota espacial.

Nuestro objetivo:

  • Crear una interfaz única, limpia y comprensible.
  • Implementar un ciclo CRUD completo: creación, visualización, actualización y eliminación de naves.
  • Añadir retroalimentación visual para el usuario (carga, éxito, error).

💡 Analogía espacial:

Pasamos de consolas de prueba individuales a la pantalla principal del MCC. En ella deben estar todos los botones e indicadores necesarios para que un solo operador pueda gestionar toda la flota sin tener que cambiar entre docenas de sistemas diferentes.


2. Diseño de la interfaz: "Tablero de Instrumentos"

Necesitaremos un HTML más estructurado. Utilizaremos "tarjetas" para mostrar las naves y una ventana modal para editarlas.

Actualizamos index.html:

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>MCC v2.0 - Gestión de Flota</title>
    <link rel="stylesheet" href="style.css"> <!-- Conectamos los estilos -->
</head>
<body>
    <header>
        <h1>Panel de control de la flota espacial</h1>
    </header>

    <main>
        <section id="fleet-controls">
            <button id="load-fleet-btn">Actualizar lista de flota</button>
            <button id="show-create-form-btn">Lanzar nueva nave</button>
        </section>

        <section id="fleet-display">
            <h2>Composición actual de la flota</h2>
            <div id="fleet-list" class="cards-container">
                <!-- Las tarjetas de las naves irán aquí -->
            </div>
        </section>
    </main>

    <!-- Ventana modal para crear/editar (inicialmente oculta) -->
    <div id="modal" class="modal-overlay" style="display: none;">
        <div class="modal-content">
            <h2 id="modal-title">Lanzamiento de nueva nave</h2>
            <form id="ship-form">
                <input type="hidden" id="ship-id">
                <input type="text" id="ship-name" placeholder="Nombre" required>
                <input type="text" id="ship-type" placeholder="Tipo" required>
                <input type="number" id="ship-year" placeholder="Año de lanzamiento" required>
                <input type="text" id="ship-status" placeholder="Estado" required>
                <div class="modal-actions">
                    <button type="submit" id="save-btn">Guardar</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. Añadimos un diseño "espacial": style.css

Cree el archivo style.css para que nuestro MCC tenga una apariencia 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 la ventana 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. Reensamblaje completo de la lógica: app.js

Ahora escribiremos la versión final de app.js, uniendo todos nuestros conocimientos.

// 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');

// --- Funciones para la 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 = 'Lanzar nueva nave';
    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 = `Edición: ${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>Año de lanzamiento: ${ship.launch_year}</p>
        <p>Estado: ${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}">Dar de baja</button>
        </div>
    `;
    return card;
}

// --- Lógica de la API y visualización ---

async function fetchAndDisplayFleet() {
    try {
        fleetListContainer.innerHTML = '<p>Cargando telemetría...</p>';
        const ships = await apiRequest('/spaceships');

        fleetListContainer.innerHTML = '';
        if (ships.length === 0) {
            fleetListContainer.innerHTML = '<p>No hay naves en el registro.</p>';
            return;
        }
        ships.forEach(ship => {
            const card = createShipCard(ship);
            fleetListContainer.appendChild(card);
        });
    } catch (error) {
        fleetListContainer.innerHTML = `<p style="color: #ff6b6b;">Error al cargar la flota: ${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) {
            // Actualización (PUT)
            response = await apiRequest(`/spaceships/${shipId}`, {
                method: 'PUT',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(shipData)
            });
            showNotification(`¡Nave "${response.name}" actualizada con éxito!`);
        } else {
            // Creación (POST)
            response = await apiRequest('/spaceships', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(shipData)
            });
            showNotification(`¡Nave "${response.name}" lanzada con éxito! ID: ${response.id}`);
        }

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

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

async function handleDeleteShip(shipId) {
    if (!confirm(`¿Está seguro de que desea dar de baja la nave con ID ${shipId}?`)) return;

    try {
        await apiRequest(`/spaceships/${shipId}`, { method: 'DELETE' });
        alert('Nave dada de baja con éxito.');
        fetchAndDisplayFleet();
    } catch (error) {
        alert(`Error al dar de baja: ${error.message}`);
    }
}

// --- Manejadores 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(`No se pudieron cargar los datos para edición: ${error.message}`);
        }
    }
});

5. Pruebas finales

  1. Inicie el servidor FastAPI: uvicorn main:app --reload
  2. Abra index.html en el navegador (a través de Live Server).
  3. Verifique el ciclo completo:
    • La lista de naves debería cargarse automáticamente.
    • Haga clic en "Lanzar nueva nave", complete el formulario y guarde. Asegúrese de que la nueva nave aparezca en la lista.
    • Haga clic en "Editar" en cualquier nave, cambie los datos y guarde. Asegúrese de que la información se haya actualizado.
    • Haga clic en "Dar de baja" en cualquier nave, confirme la acción. Asegúrese de que desaparezca de la lista.
    • Verifique todos los escenarios de error (datos incorrectos, servidor detenido).

Cuestionario de consolidación

1. Una ventana modal en una interfaz web es...

2. El evento `DOMContentLoaded` ocurre cuando...

3. ¿Por qué en la versión final usamos un único formulario para crear y editar?

4. `data-ship-id="${ship.id}"` es un ejemplo de...

5. La refactorización del código (por ejemplo, mover la lógica a `api.js`) es necesaria para...


🚀 Resumen del capítulo

Has completado con éxito la construcción y el lanzamiento de tu "Centro de Control de Vuelo".

  • 🖥️ Has creado una interfaz HTML/CSS estructurada y estilizada.
  • ⚙️ Has escrito código JavaScript limpio y modular, implementando un ciclo CRUD completo.
  • 🛰️ Tu frontend ahora puede gestionar completamente el backend creado con FastAPI.

¡Felicidades por completar con éxito el Capítulo 4! Has recorrido el camino completo desde el envío de una simple solicitud fetch hasta la creación de una aplicación web completa que interactúa con tu propia API.