📚 Cours HTML/CSS/JS

Projet Parking Express - Application Web Complète

Apprenez à créer une application web moderne avec backend Node.js et frontend interactif

🎯 Vue d'ensemble du projet

Technologies utilisées

HTML5 CSS3 JavaScript ES6+ Node.js Express.js

🖥️ 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

parking_express/
├── 📄 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

🌐 Client (Navigateur)
HTML + CSS + JavaScript
⬇️ HTTP Requests
🖥️ Serveur Express
API REST + Cache + Cron
⬇️ Fetch Data
📊 API OpenData Paris
Données de stationnement

🔄 Flux de données

  1. 1. Le serveur récupère les données toutes les heures
  2. 2. Les données sont mises en cache en mémoire
  3. 3. Le client fait des requêtes à l'API locale
  4. 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

GET /api/health

Vérifie l'état du serveur et du cache

GET /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 gzip : Ajouter le middleware compression() pour réduire la taille des réponses
🗄️
Base de données : Remplacer le cache mémoire par Redis pour la scalabilité
📊
Monitoring : Intégrer des outils de surveillance comme Morgan pour les logs
🔐
HTTPS : Forcer HTTPS en production avec 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.