L’histoire des consoles de jeux à travers une timeline interactive !

J’ai créé une « timeline interactive » qui retrace l’évolution des consoles, des vieilles boîtes en plastique des années 70 aux bêtes de guerre d’aujourd’hui type PS5 ou XBOX. Que vous soyez un nostalgique de la Game Boy ou un noob qui pense que la PS5 la meilleure console du monde … ce projet est pour vous.

Au programme de ce petit développement :

  • Un voyage dans le temps avec les consoles cultes, leurs dates de sortie, leurs ventes et leur impact (spoiler : certaines ont floppé, mais on les aime quand même). Je ne sais pas encore les informations que j’afficherais, pour le moment je me suis uniquement attelé à coder l’application.
  • Des cercles pour visualiser les ventes : plus c’est gros plus c’est vendu. Moins c’est gros plus c’est niche (ou oublié).

Comment ça marche ?

  • Une interface d’administration pour ajouter, mettre à jours, supprimer des consoles de jeux.
  • Une timeline horizontale pour sauter par dizaine d’année comme un déplacement en Delorean !
  • Des cartes qui glissent de gauche à droite avec des animations fluides (parce que c’est joli).
  • Un cercle des ventes pour chaque console, histoire de savoir si elle a cartonné ou fini au fond d’un placard.
  • Une fiche pour chaque console lorsque l’on clique sur un item.

Pourquoi j’ai fait ça ?
Parce qu’on adore les jeux vidéo et qu’on voulait rendre leur histoire plus fun et accessible. Et aussi parce que j’avais ce projet dans les cartons depuis 10 ans, et qu’avec un petit coup de main de l’IA j’ai pu le concrétiser (cette application a été codé en 3 ou 4 heures de cette façon).

Technos utilisées :

Et après ?
Saisir les données, ajouter de vrai photos, … je verrais si je décide de pousser le concept ou non.
Demo >> http://dev.borninthe80s.fr/projets/timemachine/timeline.php

# Page Administration
<?php
// Activer l'affichage des erreurs
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);

// Connexion à la base de données SQLite
$databasePath = __DIR__ . '/database.db';
$db = new SQLite3($databasePath);

// Traitement du formulaire d'ajout ou de modification de console
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (isset($_POST['ajouter_console'])) {
        // Ajout d'une nouvelle console
        $nom = $_POST['nom'];
        $annee_sortie = $_POST['annee_sortie'];
        $marque_id = $_POST['marque_id'];
        $nombre_ventes = $_POST['nombre_ventes'];
        $description = $_POST['description'];

        $stmt = $db->prepare('
            INSERT INTO consoles (nom, annee_sortie, marque_id, nombre_ventes, description)
            VALUES (:nom, :annee_sortie, :marque_id, :nombre_ventes, :description)
        ');
        $stmt->bindValue(':nom', $nom, SQLITE3_TEXT);
        $stmt->bindValue(':annee_sortie', $annee_sortie, SQLITE3_INTEGER);
        $stmt->bindValue(':marque_id', $marque_id, SQLITE3_INTEGER);
        $stmt->bindValue(':nombre_ventes', $nombre_ventes, SQLITE3_INTEGER);
        $stmt->bindValue(':description', $description, SQLITE3_TEXT);
        $stmt->execute();
    } elseif (isset($_POST['modifier_console'])) {
        // Modification d'une console existante
        $id = $_POST['id'];
        $nom = $_POST['nom'];
        $annee_sortie = $_POST['annee_sortie'];
        $marque_id = $_POST['marque_id'];
        $nombre_ventes = $_POST['nombre_ventes'];
        $description = $_POST['description'];

        $stmt = $db->prepare('
            UPDATE consoles
            SET nom = :nom, annee_sortie = :annee_sortie, marque_id = :marque_id, nombre_ventes = :nombre_ventes, description = :description
            WHERE id = :id
        ');
        $stmt->bindValue(':id', $id, SQLITE3_INTEGER);
        $stmt->bindValue(':nom', $nom, SQLITE3_TEXT);
        $stmt->bindValue(':annee_sortie', $annee_sortie, SQLITE3_INTEGER);
        $stmt->bindValue(':marque_id', $marque_id, SQLITE3_INTEGER);
        $stmt->bindValue(':nombre_ventes', $nombre_ventes, SQLITE3_INTEGER);
        $stmt->bindValue(':description', $description, SQLITE3_TEXT);
        $stmt->execute();
    }
}

// Traitement de la suppression d'une console
if (isset($_GET['supprimer_console'])) {
    $id = $_GET['supprimer_console'];
    $db->exec("DELETE FROM consoles WHERE id = $id");
}

// Récupération des consoles avec les marques
$consoles = $db->query('
    SELECT consoles.id, consoles.nom, consoles.annee_sortie, consoles.nombre_ventes, consoles.description, marques.nom as marque_nom
    FROM consoles
    LEFT JOIN marques ON consoles.marque_id = marques.id
');

// Récupération des marques pour le formulaire
$marques = $db->query('SELECT * FROM marques');

// Récupération des données de la console à modifier (si applicable)
$console_a_modifier = null;
if (isset($_GET['modifier_console'])) {
    $id = $_GET['modifier_console'];
    $result = $db->query("SELECT * FROM consoles WHERE id = $id");
    $console_a_modifier = $result->fetchArray(SQLITE3_ASSOC);
}
?>

<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Gestion des consoles</title>
    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <div class="container mt-5">
        <h1 class="text-center mb-4">Gestion des consoles de jeux</h1>

        <!-- Formulaire d'ajout/modification de console -->
        <form method="POST" class="mb-4">
            <div class="row g-3">
                <!-- Champ caché pour l'ID de la console (utilisé en cas de modification) -->
                <?php if ($console_a_modifier) : ?>
                    <input type="hidden" name="id" value="<?= $console_a_modifier['id'] ?>">
                <?php endif; ?>

                <div class="col-md-4">
                    <input type="text" name="nom" class="form-control" placeholder="Nom de la console" value="<?= $console_a_modifier ? $console_a_modifier['nom'] : '' ?>" required>
                </div>
                <div class="col-md-2">
                    <input type="number" name="annee_sortie" class="form-control" placeholder="Année de sortie" value="<?= $console_a_modifier ? $console_a_modifier['annee_sortie'] : '' ?>" required>
                </div>
                <div class="col-md-3">
                    <select name="marque_id" class="form-control" required>
                        <option value="">Sélectionnez une marque</option>
                        <?php while ($marque = $marques->fetchArray(SQLITE3_ASSOC)) : ?>
                            <option value="<?= $marque['id'] ?>" <?= ($console_a_modifier && $console_a_modifier['marque_id'] == $marque['id']) ? 'selected' : '' ?>>
                                <?= htmlspecialchars($marque['nom']) ?>
                            </option>
                        <?php endwhile; ?>
                    </select>
                </div>
                <div class="col-md-2">
                    <input type="number" name="nombre_ventes" class="form-control" placeholder="Nombre de ventes" value="<?= $console_a_modifier ? $console_a_modifier['nombre_ventes'] : '' ?>" required>
                </div>
                <div class="col-md-8">
                    <textarea name="description" class="form-control" placeholder="Description" required><?= $console_a_modifier ? $console_a_modifier['description'] : '' ?></textarea>
                </div>
                <div class="col-md-2">
                    <?php if ($console_a_modifier) : ?>
                        <button type="submit" name="modifier_console" class="btn btn-warning w-100">Modifier</button>
                    <?php else : ?>
                        <button type="submit" name="ajouter_console" class="btn btn-success w-100">Ajouter</button>
                    <?php endif; ?>
                </div>
            </div>
        </form>

        <!-- Liste des consoles -->
        <table class="table table-striped">
            <thead>
                <tr>
                    <th>ID</th>
                    <th>Nom</th>
                    <th>Année</th>
                    <th>Marque</th>
                    <th>Ventes</th>
                    <th>Description</th>
                    <th>Actions</th>
                </tr>
            </thead>
            <tbody>
                <?php while ($console = $consoles->fetchArray(SQLITE3_ASSOC)) : ?>
                    <tr>
                        <td><?= htmlspecialchars($console['id']) ?></td>
                        <td><?= htmlspecialchars($console['nom']) ?></td>
                        <td><?= htmlspecialchars($console['annee_sortie']) ?></td>
                        <td><?= htmlspecialchars($console['marque_nom']) ?></td>
                        <td><?= htmlspecialchars($console['nombre_ventes']) ?></td>
                        <td><?= htmlspecialchars($console['description']) ?></td>
                        <td>
                            <a href="?modifier_console=<?= $console['id'] ?>" class="btn btn-primary btn-sm">Modifier</a>
                            <a href="?supprimer_console=<?= $console['id'] ?>" class="btn btn-danger btn-sm" onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette console ?')">Supprimer</a>
                        </td>
                    </tr>
                <?php endwhile; ?>
            </tbody>
        </table>
    </div>

    <!-- Bootstrap JS (optionnel) -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
# Timeline
<?php
// Activer l'affichage des erreurs
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);

// Connexion à la base de données SQLite
$databasePath = __DIR__ . '/database.db';
$db = new SQLite3($databasePath);

// Vérifier si la base de données existe
if (!file_exists($databasePath)) {
    die("La base de données n'existe pas à l'emplacement spécifié : $databasePath");
}

// Vérifier la connexion à la base de données
if (!$db) {
    die("Erreur de connexion à la base de données.");
}

// Récupération des consoles avec les marques
$consoles = $db->query('
    SELECT consoles.id, consoles.nom, consoles.annee_sortie, consoles.nombre_ventes, consoles.description, marques.nom as marque_nom
    FROM consoles
    LEFT JOIN marques ON consoles.marque_id = marques.id
    ORDER BY consoles.annee_sortie ASC
');

// Vérifier si la requête a réussi
if (!$consoles) {
    die("Erreur lors de l'exécution de la requête : " . $db->lastErrorMsg());
}

// Récupérer les années uniques pour la timeline horizontale
$annees = [];
while ($console = $consoles->fetchArray(SQLITE3_ASSOC)) {
    $annees[] = $console['annee_sortie'];
}

// Vérifier si des années ont été trouvées
if (empty($annees)) {
    die("Aucune donnée trouvée dans la base de données.");
}

$annees = array_unique($annees); // Supprimer les doublons
sort($annees); // Trier les années par ordre croissant

// Réinitialiser le pointeur de résultat pour réutiliser $consoles
$consoles->reset();

// Créer des décennies pour la timeline horizontale
$decennies = [];
foreach ($annees as $annee) {
    $decennie = floor($annee / 10) * 10; // Exemple : 1987 -> 1980
    if (!in_array($decennie, $decennies)) {
        $decennies[] = $decennie;
    }
}
sort($decennies); // Trier les décennies par ordre croissant
?>
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Timeline des consoles de jeux</title>

    <style>
        /* Styles CSS */
        *,
        *::before,
        *::after {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        a {
            color: inherit;
        }

        body {
            font: normal 16px/1.5 "Helvetica Neue", sans-serif;
            background:rgb(0, 0, 0);
            color: #fff;
            overflow-x: hidden;
            padding-bottom: 50px;
        }

        /* Section d'introduction */
        .intro {
            background:rgb(255, 0, 21);
            padding: 100px 0;
        }

        .container {
            width: 90%;
            max-width: 1200px;
            margin: 0 auto;
            text-align: center;
        }

        h1 {
            font-size: 2.5rem;
        }

        .timeline-horizontale {
    position: fixed;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    background: #333;
    padding: 10px;
    overflow-x: auto;
    white-space: nowrap;
    z-index: 1001;
    width: 100%;
    text-align: center; /* Centrer les liens */
}

        /* Section de la timeline verticale */
        .timeline ul {
            background:rgb(0, 0, 0);
            padding: 50px 0;
        }

        .timeline ul li {
            list-style-type: none;
            position: relative;
            width: 6px;
            margin: 0 auto;
            padding-top: 50px;
            background: #fff;
        }

        .timeline ul li::after {
            content: "";
            position: absolute;
            left: 50%;
            bottom: 0;
            transform: translateX(-50%);
            width: 30px;
            height: 30px;
            border-radius: 50%;
            background: inherit;
            z-index: 1;
            transition: background 0.5s ease-in-out, width 0.5s ease-in-out, height 0.5s ease-in-out;
        }

        .timeline ul li.size-1::after {
            width: 10px;
            height: 10px;
        }

        .timeline ul li.size-2::after {
            width: 15px;
            height: 15px;
        }

        .timeline ul li.size-3::after {
            width: 20px;
            height: 20px;
        }

        .timeline ul li.size-4::after {
            width: 25px;
            height: 25px;
        }

        .timeline ul li.size-5::after {
            width: 30px;
            height: 30px;
        }

        .timeline ul li.size-6::after {
            width: 35px;
            height: 35px;
        }

        .timeline ul li.size-7::after {
            width: 40px;
            height: 40px;
        }

        .timeline ul li.size-8::after {
            width: 45px;
            height: 45px;
        }

        .timeline ul li.size-9::after {
            width: 50px;
            height: 50px;
        }

        .timeline ul li.size-10::after {
            width: 55px;
            height: 55px;
        }

        .timeline ul li div {
            position: relative;
            bottom: 0;
            width: 400px;
            padding: 15px;
            background: #ff0000;
        }

        .timeline ul li div::before {
            content: "";
            position: absolute;
            bottom: 7px;
            width: 0;
            height: 0;
            border-style: solid;
        }

        .timeline ul li:nth-child(odd) div {
            left: 45px;
        }

        .timeline ul li:nth-child(odd) div::before {
            left: -15px;
            border-width: 8px 16px 8px 0;
            border-color: transparent #ff0000 transparent transparent;
        }

        .timeline ul li:nth-child(even) div {
            left: -439px;
        }

        .timeline ul li:nth-child(even) div::before {
            right: -15px;
            border-width: 8px 0 8px 16px;
            border-color: transparent transparent transparent #ff0000;
        }

        time {
            display: block;
            font-size: 1.2rem;
            font-weight: bold;
            margin-bottom: 8px;
        }

        /* Effets d'animation */
        .timeline ul li.in-view::after {
            background: #ff0000;
        }

        .timeline ul li div {
            visibility: hidden;
            opacity: 0;
            transition: all 0.5s ease-in-out;
        }

        .timeline ul li:nth-child(odd) div {
            transform: translate3d(200px, 0, 0);
        }

        .timeline ul li:nth-child(even) div {
            transform: translate3d(-200px, 0, 0);
        }

        .timeline ul li.in-view div {
            transform: none;
            visibility: visible;
            opacity: 1;
        }

        /* Media queries */
        @media screen and (max-width: 900px) {
            .timeline ul li div {
                width: 250px;
            }
            .timeline ul li:nth-child(even) div {
                left: -289px;
            }
        }

        @media screen and (max-width: 600px) {
            .timeline ul li {
                margin-left: 20px;
            }
            .timeline ul li div {
                width: calc(100vw - 91px);
            }
            .timeline ul li:nth-child(even) div {
                left: 45px;
            }
            .timeline ul li:nth-child(even) div::before {
                left: -15px;
                border-width: 8px 16px 8px 0;
                border-color: transparent #ff0000 transparent transparent;
            }
        }

        /* Footer */
        .page-footer {
            position: fixed;
            right: 0;
            bottom: 20px;
            display: flex;
            align-items: center;
            padding: 5px;
            color: black;
            background: rgba(255, 255, 255, 0.65);
        }

        .page-footer a {
            display: flex;
            margin-left: 4px;
        }


        .popup {
    display: none; /* Caché par défaut */
    position: fixed; /* Reste en place même lors du défilement */
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.8); /* Fond semi-transparent */
    justify-content: center; /* Centre horizontalement */
    align-items: center; /* Centre verticalement */
    z-index: 1000; /* S'assure qu'il est au-dessus des autres éléments */
}

.popup-content {
    background: rgba(0, 0, 0, 0.8);
    padding: 20px;
    border-radius: 10px;
    max-width: 500px;
    width: 90%;
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
    text-align: center; /* Centre le texte */
}

.popup-content img {
    max-width: 100%; /* L'image ne dépasse pas la largeur du conteneur */
    height: auto; /* Maintient le ratio de l'image */
    border-radius: 5px;
    margin-bottom: 15px;
}

    </style>


</head>
<body>
    <!-- Timeline horizontale -->
    <div class="timeline-horizontale">
        <?php if (!empty($decennies)) : ?>
            <?php foreach ($decennies as $decennie) : ?>
                <a href="#decennie-<?= $decennie ?>"><?= $decennie ?></a>
            <?php endforeach; ?>
        <?php else : ?>
            <p>Aucune décennie trouvée.</p>
        <?php endif; ?>
    </div>


    <!-- Section de la timeline verticale -->
    <section class="timeline">
        <ul>
            <?php if ($consoles) : ?>
                <?php while ($console = $consoles->fetchArray(SQLITE3_ASSOC)) : ?>
                    <?php
                    // Déterminer la taille du cercle en fonction des ventes
                    $tailleCercle = 1; // Par défaut, taille 1 (10px)
                    if ($console['nombre_ventes'] >= 100000000) {
                        $tailleCercle = 10;
                    } elseif ($console['nombre_ventes'] >= 90000000) {
                        $tailleCercle = 9;
                    } elseif ($console['nombre_ventes'] >= 80000000) {
                        $tailleCercle = 8;
                    } elseif ($console['nombre_ventes'] >= 70000000) {
                        $tailleCercle = 7;
                    } elseif ($console['nombre_ventes'] >= 60000000) {
                        $tailleCercle = 6;
                    } elseif ($console['nombre_ventes'] >= 50000000) {
                        $tailleCercle = 5;
                    } elseif ($console['nombre_ventes'] >= 40000000) {
                        $tailleCercle = 4;
                    } elseif ($console['nombre_ventes'] >= 30000000) {
                        $tailleCercle = 3;
                    } elseif ($console['nombre_ventes'] >= 20000000) {
                        $tailleCercle = 2;
                    }
                    ?>
                    <li id="decennie-<?= floor($console['annee_sortie'] / 10) * 10 ?>" class="size-<?= $tailleCercle ?>">
                        <div>
                            <time><?= htmlspecialchars($console['annee_sortie']) ?></time>
                            <p><strong><?= htmlspecialchars($console['nom']) ?></strong> (<?= htmlspecialchars($console['marque_nom']) ?>)</p>
                            <p><?= htmlspecialchars($console['description']) ?></p>
                            <p>Ventes : <?= htmlspecialchars($console['nombre_ventes']) ?> unités</p>
                        </div>
                    </li>
                <?php endwhile; ?>
            <?php else : ?>
                <li>
                    <div>
                        <p>Aucune console trouvée.</p>
                    </div>
                </li>
            <?php endif; ?>
        </ul>
    </section>

    <!-- Popup pour les détails de la console -->
    <div id="console-popup" class="popup">
        <div class="popup-content">
            <span class="close-popup">&times;</span>
            <h2 id="popup-title"></h2>
            <img id="popup-image" src="" alt="Image de la console">
            <p id="popup-description"></p>
            <p><strong>Année de sortie :</strong> <span id="popup-year"></span></p>
            <p><strong>Marque :</strong> <span id="popup-brand"></span></p>
            <p><strong>Ventes :</strong> <span id="popup-sales"></span> unités</p>
        </div>
    </div>

    <!-- Script JavaScript -->
    <script>
        (function () {
            "use strict";

            // Sélectionner tous les éléments de la timeline
            var items = document.querySelectorAll(".timeline li");

            // Vérifier si un élément est dans la vue
            function isElementInViewport(el) {
                var rect = el.getBoundingClientRect();
                return (
                    rect.top >= 0 &&
                    rect.left >= 0 &&
                    rect.bottom <=
                        (window.innerHeight || document.documentElement.clientHeight) &&
                    rect.right <= (window.innerWidth || document.documentElement.clientWidth)
                );
            }

            // Appliquer la classe "in-view" aux éléments visibles
            function callbackFunc() {
                for (var i = 0; i < items.length; i++) {
                    if (isElementInViewport(items[i])) {
                        items[i].classList.add("in-view");
                    }
                }
            }

            // Écouter les événements de chargement, redimensionnement et défilement
            window.addEventListener("load", callbackFunc);
            window.addEventListener("resize", callbackFunc);
            window.addEventListener("scroll", callbackFunc);

            // Gestion du clic sur la timeline horizontale
            document.querySelectorAll('.timeline-horizontale a').forEach(link => {
                link.addEventListener('click', function (e) {
                    e.preventDefault();
                    const targetId = this.getAttribute('href').substring(1); // Récupère l'ID sans le #
                    const targetElement = document.getElementById(targetId);
                    if (targetElement) {
                        targetElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
                    }
                });
            });

            // Gestion du popup
            document.querySelectorAll('.timeline ul li div').forEach(item => {
                item.addEventListener('click', function () {
                    const popup = document.getElementById('console-popup');
                    const title = this.querySelector('strong').textContent;
                    const year = this.querySelector('time').textContent;
                    const brand = this.querySelector('p:nth-of-type(1)').textContent.split('(')[1].replace(')', '');
                    const description = this.querySelector('p:nth-of-type(2)').textContent;
                    const sales = this.querySelector('p:nth-of-type(3)').textContent;

                    // Remplir le popup avec les données
                    document.getElementById('popup-title').textContent = title;
                    document.getElementById('popup-year').textContent = year;
                    document.getElementById('popup-brand').textContent = brand;
                    document.getElementById('popup-description').textContent = description;
                    document.getElementById('popup-sales').textContent = sales;

                    // Ajouter une image aléatoire (pour l'instant)
                    const randomImageUrl = `https://picsum.photos/500/300?random=${Math.floor(Math.random() * 100)}`;
                    document.getElementById('popup-image').src = randomImageUrl;

                    // Afficher le popup
                    popup.style.display = 'flex';
                });
            });

            // Fermer le popup
            document.querySelector('.close-popup').addEventListener('click', function () {
                document.getElementById('console-popup').style.display = 'none';
            });

            // Fermer le popup en cliquant à l'extérieur
            window.addEventListener('click', function (event) {
                const popup = document.getElementById('console-popup');
                if (event.target === popup) {
                    popup.style.display = 'none';
                }
            });
        })();
    </script>
</body>
</html>