Chapitre 4.6 : Création d'une interface simple pour l'API
Temps d'étude : 1 heure 15 minutes
1. Assemblage final : Lancement du "Centre de Contrôle de Vol"
Nous avons étudié tous les systèmes individuellement : configuré "l'antenne" (Fetch
), appris à envoyer des "commandes" (GET
, POST
, DELETE
), développé des "protocoles d'urgence" (gestion des erreurs).
Il est temps de rassembler tous les composants et de lancer notre CCV – une interface complète et interactive pour la gestion de la flotte spatiale.
Notre objectif :
- Créer une interface unifiée, propre et claire.
- Implémenter un cycle CRUD complet : création, affichage, mise à jour et suppression de vaisseaux.
- Ajouter un retour visuel pour l'utilisateur (chargement, succès, erreur).
💡 Analogie spatiale :
Nous passons des consoles de test individuelles à l'écran principal du CCV. Il doit contenir tous les boutons et indicateurs nécessaires pour qu'un seul opérateur puisse gérer toute la flotte sans basculer entre des dizaines de systèmes différents.
2. Conception de l'interface : "Tableau de bord"
Nous aurons besoin d'un HTML plus structuré. Nous utiliserons des "cartes" pour afficher les vaisseaux et une fenêtre modale pour leur édition.
Mise à jour de index.html
:
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>CCV v2.0 - Gestion de la Flotte</title>
<link rel="stylesheet" href="style.css"> <!-- Connexion des styles -->
</head>
<body>
<header>
<h1>Panneau de contrôle de la flotte spatiale</h1>
</header>
<main>
<section id="fleet-controls">
<button id="load-fleet-btn">Actualiser la liste de la flotte</button>
<button id="show-create-form-btn">Lancer un nouvel appareil</button>
</section>
<section id="fleet-display">
<h2>Composition actuelle de la flotte</h2>
<div id="fleet-list" class="cards-container">
<!-- Les cartes des vaisseaux seront ici -->
</div>
</section>
</main>
<!-- Fenêtre modale pour la création/édition (initialement cachée) -->
<div id="modal" class="modal-overlay" style="display: none;">
<div class="modal-content">
<h2 id="modal-title">Lancement d'un nouvel appareil</h2>
<form id="ship-form">
<input type="hidden" id="ship-id">
<input type="text" id="ship-name" placeholder="Nom" required>
<input type="text" id="ship-type" placeholder="Type" required>
<input type="number" id="ship-year" placeholder="Année de lancement" required>
<input type="text" id="ship-status" placeholder="Statut" required>
<div class="modal-actions">
<button type="submit" id="save-btn">Enregistrer</button>
<button type="button" id="cancel-btn">Annuler</button>
</div>
</form>
<div id="notification-area"></div>
</div>
</div>
<script src="api.js"></script>
<script src="app.js"></script>
</body>
</html>
3. Ajout du design "spatial" : style.css
Créez le fichier style.css
pour que notre CCV ait une apparence digne.
/* 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; }
/* Styles pour la fenêtre modale */
.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. Reconstitution complète de la logique : app.js
Nous allons maintenant écrire la version finale de app.js
, en combinant toutes nos connaissances.
// app.js
// --- Éléments 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');
// --- Fonctions pour l'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 = 'Lancement d'un nouvel appareil';
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 = `Modification : ${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>Type : ${ship.type}</p>
<p>Année de lancement : ${ship.launch_year}</p>
<p>Statut : ${ship.status}</p>
<div class="card-actions">
<button class="edit-btn" data-ship-id="${ship.id}">Modifier</button>
<button class="delete-btn" data-ship-id="${ship.id}">Mettre au rebut</button>
</div>
`;
return card;
}
// --- Logique API et affichage ---
async function fetchAndDisplayFleet() {
try {
fleetListContainer.innerHTML = '<p>Chargement de la télémétrie...</p>';
const ships = await apiRequest('/spaceships');
fleetListContainer.innerHTML = '';
if (ships.length === 0) {
fleetListContainer.innerHTML = '<p>Aucun appareil dans le registre.</p>';
return;
}
ships.forEach(ship => {
const card = createShipCard(ship);
fleetListContainer.appendChild(card);
});
} catch (error) {
fleetListContainer.innerHTML = `<p style="color: #ff6b6b;">Erreur de chargement de la flotte : ${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) {
// Mise à jour (PUT)
response = await apiRequest(`/spaceships/${shipId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(shipData)
});
showNotification(`L'appareil "${response.name}" a été mis à jour avec succès !`);
} else {
// Création (POST)
response = await apiRequest('/spaceships', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(shipData)
});
showNotification(`L'appareil "${response.name}" a été lancé avec succès ! ID : ${response.id}`);
}
setTimeout(() => {
closeModal();
fetchAndDisplayFleet();
}, 1500);
} catch (error) {
showNotification(error.message, true);
}
}
async function handleDeleteShip(shipId) {
if (!confirm(`Êtes-vous sûr de vouloir mettre au rebut l'appareil avec l'ID ${shipId} ?`)) return;
try {
await apiRequest(`/spaceships/${shipId}`, { method: 'DELETE' });
alert('L'appareil a été mis au rebut avec succès.');
fetchAndDisplayFleet();
} catch (error) {
alert(`Erreur lors de la mise au rebut : ${error.message}`);
}
}
// --- Gestionnaires d'événements ---
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(`Impossible de charger les données pour la modification : ${error.message}`);
}
}
});
5. Tests finaux
- Lancez le serveur FastAPI :
uvicorn main:app --reload
- Ouvrez
index.html
dans le navigateur (via Live Server). - Vérifiez le cycle complet :
- La liste des vaisseaux doit se charger automatiquement.
- Cliquez sur "Lancer un nouvel appareil", remplissez le formulaire et enregistrez. Assurez-vous que le nouveau vaisseau apparaît dans la liste.
- Cliquez sur "Modifier" sur n'importe quel vaisseau, modifiez les données et enregistrez. Assurez-vous que les informations ont été mises à jour.
- Cliquez sur "Mettre au rebut" sur n'importe quel vaisseau, confirmez l'action. Assurez-vous qu'il disparaît de la liste.
- Vérifiez tous les scénarios d'erreur (données incorrectes, arrêt du serveur).
Quiz pour la consolidation
🚀 Résumé du chapitre
Vous avez achevé avec succès la construction et le lancement de votre "Centre de Contrôle de Mission".
- 🖥️ Vous avez créé une interface HTML/CSS structurée et stylisée.
- ⚙️ Vous avez écrit un code JavaScript propre et modulaire, implémentant un cycle CRUD complet.
- 🛰️ Votre frontend peut désormais gérer entièrement le backend créé avec FastAPI.
Félicitations pour avoir terminé le Chapitre 4 ! Vous avez parcouru tout le chemin, de l'envoi d'une simple requête fetch
à la création d'une application web complète qui interagit avec votre propre API.