🎯 Vue d'ensemble du projet
Technologies utilisées
🖥️ Backend
- • Serveur Express.js
- • API REST
- • Cache en mémoire
- • Tâches programmées
- • Gestion CORS
🎨 Frontend
- • Interface responsive
- • Thème neon moderne
- • Recherche en temps réel
- • Navigation par onglets
- • Animations CSS
📁 Structure du projet
├── 📄 server.js # Serveur Express (backend)
├── 📄 package.json # Configuration et dépendances
├── 📄 cours.html # Ce cours (nouveau)
└── 📁 public/
└── 📄 index.html # Application web complète
🏗️ Architecture de l'application
🔄 Flux de données
- 1. Le serveur récupère les données toutes les heures
- 2. Les données sont mises en cache en mémoire
- 3. Le client fait des requêtes à l'API locale
- 4. L'interface se met à jour en temps réel
🖥️ Backend - server.js
Configuration initiale
// Imports des modules
import express from "express";
import fetch from "node-fetch";
import cron from "node-cron";
import cors from "cors";
import path from "path";
import { fileURLToPath } from "url";
const app = express();
const PORT = process.env.PORT || 3012;
Configuration CORS
Le CORS (Cross-Origin Resource Sharing) permet de contrôler quels domaines peuvent accéder à votre API :
const allowedOrigins = new Set([
"https://parking.krissclotilde.com",
"http://localhost:3012",
"http://localhost:5173" // Pour le développement
]);
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.has(origin)) {
callback(null, true);
} else {
callback(new Error("Accès non autorisé par CORS"));
}
}
}));
Cache en mémoire
Le cache évite de faire trop de requêtes à l'API externe et améliore les performances.
let cache = {
lastUpdate: null,
data: [],
};
async function loadData() {
try {
const res = await fetchWithTimeout(BASE_URL, { timeoutMs: 10000 });
if (!res.ok) {
throw new Error(`HTTP ${res.status} ${res.statusText}`);
}
const json = await res.json();
cache.data = json.results || [];
cache.lastUpdate = new Date();
console.log(`Cache mis à jour à ${cache.lastUpdate.toLocaleString("fr-FR")}`);
} catch (err) {
console.error("Impossible de charger les données :", err.message);
}
}
API Endpoints
/api/health
Vérifie l'état du serveur et du cache
/api/parkings
Retourne toutes les données des parkings
Tâche programmée (Cron)
// Mise à jour automatique toutes les heures
cron.schedule("0 * * * *", loadData, {
timezone: "Europe/Paris",
});
🎨 Frontend - Structure HTML
En-tête et métadonnées
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🅿️ Parking Paris - API Explorer</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
Navigation par onglets
L'interface utilise un système d'onglets pour organiser différents types de données.
<div class="flex flex-wrap gap-2 border-b border-blue-500 mb-4">
<button id="tab-parkings" class="tab-button px-4 py-2 font-medium rounded-t-lg bg-blue-500 text-white">
Parkings en ouvrage
</button>
<button id="tab-street" class="tab-button px-4 py-2 font-medium rounded-t-lg text-gray-500">
Stationnement voirie
</button>
<button id="tab-stats" class="tab-button px-4 py-2 font-medium rounded-t-lg text-gray-500">
Statistiques
</button>
</div>
Contrôles utilisateur
<div class="flex flex-wrap gap-4 mb-4">
<div class="flex-1 min-w-[16rem]">
<div class="relative">
<i data-lucide="search" class="absolute left-3 top-1/2 -translate-y-1/2"></i>
<input type="text" id="searchInput" placeholder="Rechercher..."
class="w-full pl-10 pr-4 py-2 border rounded-lg">
</div>
</div>
<button id="refreshButton" class="flex items-center px-4 py-2 bg-blue-500 text-white rounded-lg">
<i data-lucide="refresh-cw" class="w-4 h-4 mr-2"></i>
Actualiser
</button>
</div>
🎨 Styles CSS - Thème Neon
Variables CSS personnalisées
:root {
--bg: #0a0f1f;
--bg-2: #0d1229;
--fg: #e6f1ff;
--muted: #93a4c4;
--neon-cyan: #00eaff;
--neon-purple: #a855f7;
--neon-pink: #ff3d81;
--neon-lime: #c8ff00;
--card: #0e1531;
--card-2: #0b1026;
--ring: rgba(0, 234, 255, 0.5);
}
Arrière-plan dégradé
Utilise plusieurs dégradés radiaux pour créer un effet de profondeur.
body.theme-neon {
background:
radial-gradient(1200px 600px at 10% -10%, rgba(168, 85, 247, 0.12), transparent 60%),
radial-gradient(900px 500px at 110% 10%, rgba(0, 234, 255, 0.10), transparent 60%),
linear-gradient(180deg, var(--bg), var(--bg-2));
color: var(--fg);
}
Cartes avec bordures animées
.theme-neon .hover-card::before {
content: "";
position: absolute;
inset: -2px;
border-radius: 14px;
padding: 2px;
background: linear-gradient(120deg, var(--neon-cyan), transparent 20%, var(--neon-purple));
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
opacity: 0.35;
filter: blur(8px);
}
.theme-neon .hover-card:hover::before {
opacity: 0.8;
filter: blur(6px);
}
Effets de focus
.theme-neon input:focus, .theme-neon select:focus {
outline: none;
box-shadow: 0 0 0 3px var(--ring), 0 0 20px rgba(0, 234, 255, 0.2);
transform: translateY(-1px);
}
⚡ JavaScript - Logique applicative
Gestion de l'état
const state = {
allRaw: [], // Données brutes de l'API
grouped: [], // Données organisées
filtered: [], // Données après filtrage
lastUpdate: null, // Timestamp de dernière mise à jour
activeTab: "parkings", // Onglet actif
debounceTimer: null, // Timer pour la recherche
};
Récupération des données
async function fetchParkings() {
hideError();
setLoading(true);
try {
const res = await fetch(`${API_BASE}/parkings`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
state.allRaw = Array.isArray(json?.data) ? json.data : [];
state.grouped = groupByFacility(state.allRaw);
state.filtered = state.grouped.slice();
state.lastUpdate = json?.lastUpdate || null;
renderList();
} catch (e) {
showError("Impossible de charger les données");
console.error(e);
} finally {
setLoading(false);
}
}
Recherche avec debouncing
Le debouncing évite de faire trop de recherches pendant que l'utilisateur tape.
function debounce(fn, wait = 400) {
clearTimeout(state.debounceTimer);
state.debounceTimer = setTimeout(fn, wait);
}
function applySearch() {
const query = els.search.value.toLowerCase();
if (!query) {
state.filtered = state.grouped.slice();
} else {
state.filtered = state.grouped.filter((parking) => {
const searchableText = [
parking.nom || "",
parking.adresse || "",
parking.arrondissement || ""
].join(" ").toLowerCase();
return searchableText.includes(query);
});
}
renderList();
}
// Événement sur le champ de recherche
els.search.addEventListener("input", () => debounce(applySearch, 250));
Génération dynamique de cartes
function makeCard(parkingData) {
const name = pick(parkingData, ["nom", "nom_parking", "name"]) || "Parking sans nom";
const address = pick(parkingData, ["adresse", "address", "rue"]) || "Adresse non disponible";
const placesTotal = pick(parkingData, ["places_total", "nb_places", "capacite"]);
const placesDispo = pick(parkingData, ["counterfreeplaces", "places_disponibles"]);
return `
<div class="bg-white rounded-lg shadow-md p-4 border-l-4 border-blue-500 hover-card fade-in">
<div class="flex justify-between items-start mb-2">
<h3 class="text-lg font-semibold text-gray-800">${escapeHtml(name)}</h3>
<span class="text-sm bg-blue-100 text-blue-800 px-2 py-1 rounded">
${escapeHtml(String(arrondissement))}
</span>
</div>
<div class="space-y-2">
<div class="flex items-center text-gray-600">
<i data-lucide="map-pin" class="w-4 h-4 mr-2"></i>
<span class="text-sm">${escapeHtml(address)}</span>
</div>
${placesTotal ? `<div>Places totales: <strong>${placesTotal}</strong></div>` : ''}
${placesDispo ? `<div>Disponibles: <strong>${placesDispo}</strong></div>` : ''}
</div>
</div>
`;
}
🚀 Fonctionnalités avancées
Recherche intelligente
Recherche en temps réel avec debouncing pour optimiser les performances
Mise à jour auto
Cache actualisé automatiquement toutes les heures via cron job
Interface responsive
Design adaptatif avec Tailwind CSS et animations fluides
Sécurité renforcée
Protection XSS, validation des données et gestion CORS
Gestion des erreurs
function showError(message) {
els.errorMessage.textContent = message || "Erreur inconnue";
els.error.style.display = "flex";
}
function hideError() {
els.error.style.display = "none";
}
// Middleware Express pour les erreurs CORS
app.use((err, _req, res, next) => {
if (err?.message === "Accès non autorisé par CORS") {
return res.status(403).json({ error: err.message });
}
next(err);
});
🔒 Sécurité et bonnes pratiques
Protection XSS
Toujours échapper les données utilisateur avant de les injecter dans le DOM.
function escapeHtml(str) {
return String(str)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
}
Validation des données
// Fonction utilitaire pour extraire des données fiables
function pick(obj, keys) {
for (const key of keys) {
if (obj && obj[key] != null && obj[key] !== "") {
return obj[key];
}
}
return undefined;
}
// Vérification des types de données
const results = Array.isArray(json?.results) ? json.results : [];
Gestion des timeouts
async function fetchWithTimeout(url, { timeoutMs = 10000, ...options } = {}) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(url, {
...options,
signal: controller.signal
});
return res;
} finally {
clearTimeout(timeout);
}
}
🚀 Déploiement et optimisation
Scripts package.json
{
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "echo \"Error: no test specified\" && exit 1"
}
}
Variables d'environnement
Configurez le port via la variable d'environnement PORT pour le déploiement.
const PORT = process.env.PORT || 3012;
Commandes de lancement
🔧 Développement
npm run dev
🌟 Production
npm start
Optimisations recommandées
compression() pour réduire la taille des réponses
helmet()
🎓 Conclusion
Ce projet démontre une architecture web moderne complète avec :
🎯 Concepts maîtrisés :
- • API REST avec Express.js
- • Interface responsive et moderne
- • Gestion d'état JavaScript
- • Sécurité web (XSS, CORS)
- • Optimisation des performances
🚀 Prochaines étapes :
- • Tests unitaires et d'intégration
- • PWA et service workers
- • Déploiement avec Docker
- • API GraphQL
- • Framework frontend (React/Vue)
🌟 Félicitations ! Vous maîtrisez maintenant les bases du développement web full-stack moderne.