Exercice 02 - Révision JavaScript

Mise en situation

Tu fais partie de l’équipe “contrôle parental” d’un studio de jeux vidéo. Avant de laisser quelqu’un lancer Grand Theft Auto 6, le site doit vérifier l’âge de la personne selon la région (simulation de règles).

Ton mandat : compléter le JavaScript pour :


Modalités


À faire (étapes)

  1. Créer un dossier nommé exer02
  2. Copier-coller le gabarit HTML dans un fichier index.html.
  3. Copier-coller le CSS dans un fichier style.css
  4. Copier-coller le JavaScript à compléter dans un fichier main.js
  5. Compléter toutes les sections TODO. (Faites un CTRL+F, cherchez TODO)
  6. Tester avec les scénarios en haut du fichier JS.
  7. Mode expert. (si vous avez le temps et la motivation ^^)

Gabarit HTML (à copier)

<!DOCTYPE html>
<html lang="fr" data-bs-theme="dark">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Validation d’âge - GTA VI</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
    <link rel="stylesheet" href="style.css">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
    <script src="main.js" defer></script>
</head>

<body>
    <div class="gta-bg min-vh-100 py-5">
        <div class="container" style="max-width: 900px;">
            <header class="d-flex flex-column flex-md-row align-items-md-end justify-content-between gap-2 mb-4">
                <div>
                    <h1 class="display-6 mb-1 fw-bold">Validation d’âge - GTA VI</h1>
                    <p class="text-body-secondary mb-0">Simulation d’accès selon région (NA / EU / Custom)</p>
                </div>

                <span class="badge rounded-pill gta-badge px-3 py-2">Contrôle parental</span>
            </header>

            <div class="card gta-card shadow-sm border-0 mb-4">
                <div class="card-body p-4">
                    <form id="ageForm" novalidate class="row g-3">
                        <div class="col-12 col-md-6">
                            <label for="birthDate" class="form-label fw-semibold">Date de naissance</label>
                            <input id="birthDate" type="date" class="form-control gta-input" required />
                            <div id="birthDateHelp" class="form-text">Choisis ta date de naissance.</div>
                        </div>

                        <div class="col-12 col-md-6">
                            <label for="region" class="form-label fw-semibold">Région</label>
                            <select id="region" class="form-select gta-input">
                                <option value="NA">Amérique du Nord (min 17)</option>
                                <option value="EU">Europe (min 18)</option>
                                <option value="CUSTOM">Custom</option>
                            </select>
                        </div>

                        <!-- IMPORTANT: d-none au lieu de display:none (Bootstrap) -->
                        <div id="customWrap" class="col-12 d-none">
                            <div class="p-3 rounded-4 gta-panel border">
                                <label for="customAge" class="form-label fw-semibold mb-1">Âge minimum (Custom)</label>
                                <input id="customAge" type="number" class="form-control gta-input" min="0" max="99"
                                    value="21" />
                                <div class="form-text">Ex.: 21 pour une restriction renforcée.</div>
                            </div>
                        </div>

                        <div class="col-12 d-flex flex-wrap gap-2 mt-2">
                            <button id="checkBtn" type="submit" class="btn btn-primary px-4">
                                Vérifier
                            </button>
                            <button type="reset" class="btn btn-outline-light px-4">
                                Réinitialiser
                            </button>
                        </div>
                    </form>

                    <!-- Message en alert Bootstrap (ton JS peut changer les classes alert-*) -->
                    <div id="message" class="alert alert-secondary mt-4 mb-0" role="status" aria-live="polite">
                        Entre ta date de naissance, puis clique sur <strong>Vérifier</strong>.
                    </div>
                </div>
            </div>

            <div class="row g-4">
                <div class="col-12 col-lg-7">
                    <div class="card gta-card shadow-sm border-0">
                        <div class="card-body p-4">
                            <h2 class="h5 fw-semibold mb-3">Historique</h2>
                            <ul id="log" class="list-group list-group-flush gta-list">
                                <!-- items ajoutés par ton JS -->
                            </ul>
                        </div>
                    </div>
                </div>

                <div class="col-12 col-lg-5">
                    <div class="card gta-card shadow-sm border-0">
                        <div class="card-body p-4">
                            <h2 class="h5 fw-semibold mb-3">Stats</h2>
                            <p id="stats" class="text-body-secondary mb-0">Aucune tentative.</p>
                        </div>
                    </div>
                </div>
            </div>

            <footer class="mt-4 text-body-secondary small">
                Palette inspirée de l'esthétique Rockstar "GTA VI" (néon sur fond dark).
            </footer>
        </div>
    </div>
</body>

</html>

CSS (à copier)

/* Active un look dark “Rockstar VI vibe” */
:root {
    /* Fond & surfaces (violet / indigo sombre) */
    --gta-bg: #0b0b12;
    --gta-surface: rgba(255, 255, 255, 0.04);
    --gta-panel: rgba(255, 255, 255, 0.06);

    /* Accents (magenta + peach/orange + violet) */
    --gta-pink: #fb84d0;
    /* “border / hover accent” dans la palette VI theme */
    --gta-plum: #88366a;
    /* “accent color” */
    --gta-peach: #feaa63;
    /* accent orange */
    --gta-violet: #653152;
    /* dark plum */
    --gta-ink: #2d2637;
    /* fond sombre */
}

/* Bootstrap 5.3+ supporte data-bs-theme="dark"
   (si tu veux, ajoute <html data-bs-theme="dark">, sinon ça marche quand même) */
[data-bs-theme="dark"] {
    --bs-body-bg: var(--gta-bg);
    --bs-body-color: #eaeaf3;
    --bs-secondary-color: rgba(234, 234, 243, 0.75);
    --bs-border-color: rgba(255, 255, 255, 0.14);

    /* Les “couleurs Bootstrap” */
    --bs-primary: var(--gta-pink);
    --bs-primary-rgb: 251, 132, 208;

    --bs-secondary: #353c5e;
    /* indigo sombre */
    --bs-secondary-rgb: 53, 60, 94;

    --bs-warning: var(--gta-peach);
    --bs-warning-rgb: 254, 170, 99;

    --bs-info: #8aa7ff;
    /* bleu-violet doux (look “néon froid”) */
    --bs-info-rgb: 138, 167, 255;
}

/* FORCE les couleurs des boutons (override dur) */
.btn-primary {
    background-color: var(--gta-pink) !important;
    border-color: var(--gta-pink) !important;
    color: #140712 !important;
}

.btn-primary:hover,
.btn-primary:focus {
    background-color: var(--gta-peach) !important;
    border-color: var(--gta-peach) !important;
    color: #140712 !important;
}

.btn-primary:active {
    background-color: var(--gta-plum) !important;
    border-color: var(--gta-plum) !important;
}

/* Outline */
.btn-outline-primary {
    color: var(--gta-pink) !important;
    border-color: rgba(251, 132, 208, 0.75) !important;
}

.btn-outline-primary:hover,
.btn-outline-primary:focus {
    background-color: rgba(251, 132, 208, 0.16) !important;
    border-color: var(--gta-pink) !important;
    color: #ffffff !important;
}

/* Background “néon” style site VI (radial gradients subtils) */
.gta-bg {
    background: radial-gradient(900px 500px at 20% 10%,
            rgba(251, 132, 208, 0.18),
            transparent 60%),
        radial-gradient(900px 520px at 85% 15%,
            rgba(254, 170, 99, 0.14),
            transparent 60%),
        radial-gradient(1000px 600px at 55% 90%,
            rgba(101, 49, 82, 0.18),
            transparent 60%),
        var(--gta-bg);
}

/* Cartes & panels */
.gta-card {
    background: var(--gta-surface);
    backdrop-filter: blur(8px);
    border-radius: 1.25rem;
}

.gta-panel {
    background: var(--gta-panel);
    border-color: rgba(255, 255, 255, 0.14) !important;
}

/* Inputs */
.gta-input {
    background-color: rgba(0, 0, 0, 0.18) !important;
    border-color: rgba(255, 255, 255, 0.16) !important;
    color: var(--bs-body-color) !important;
}

.gta-input:focus {
    box-shadow: 0 0 0 0.25rem rgba(251, 132, 208, 0.18) !important;
    border-color: rgba(251, 132, 208, 0.55) !important;
}

/* Badge néon */
.gta-badge {
    background: linear-gradient(90deg, var(--gta-pink), var(--gta-peach));
    color: #120914;
    box-shadow: 0 0 22px rgba(251, 132, 208, 0.25),
        0 0 18px rgba(254, 170, 99, 0.18);
}

/* List group look */
.gta-list .list-group-item {
    background: transparent;
    border-color: rgba(255, 255, 255, 0.1);
    color: var(--bs-body-color);
}

JavaScript à compléter (à copier)

/*
Scénarios de test (à essayer) :
1) NA, 17 ans pile aujourd’hui => OK
2) NA, 16 ans 11 mois => REFUS
3) EU, 18 ans => OK
4) EU, 17 ans => REFUS
5) CUSTOM=21, 20 ans => REFUS
6) Date future => Invalide / refus
*/

// ====== DOM refs ======
const form = document.querySelector("#ageForm");
const birthDateInput = document.querySelector("#birthDate");
const regionSelect = document.querySelector("#region");
const customWrap = document.querySelector("#customWrap");
const customAgeInput = document.querySelector("#customAge");
const message = document.querySelector("#message");
const log = document.querySelector("#log");
const stats = document.querySelector("#stats");

// ====== Données (objets) ======
const RULES = {
  NA: 17,
  EU: 18
  // CUSTOM géré à part
};

// Historique des tentatives (array d'objets)
let attempts = []; // { birth, region, minAge, age, allowed, at }

// ====== Fonctions pures (testables) ======
// Pourquoi "pures" ? Elles ne modifient pas l'état global et retournent
// toujours le même résultat pour les mêmes entrées. Faciles à tester !
function calculateAge(birthDate, today = new Date()) {
  // TODO:
  // 1) Calculer l'âge "brut" en années (année courante - année naissance)
  // 2) Si l'anniversaire n'est pas encore passé cette année, enlever 1
  // 3) Retourner l'âge (number)
  //
  // Indice: compare month/day
  // today.getMonth() (0-11), today.getDate() (1-31)
}

function getMinAge(region, customMinAge) {
  // TODO:
  // - Si region === "CUSTOM", retourner customMinAge (converti en number)
  // - Sinon retourner RULES[region]
  // - Si region inconnue, retourner NaN (ou une valeur qui force l'erreur)
}

function canPlay(age, minAge) {
  // (OK de garder cette fonction simple) avec juste un retour booléen
  return age >= minAge;
}

// ====== Rendu UI ======
function setMessage(text, kind) {
  // kind: "ok" | "no" | "warn"
  message.textContent = text;

  // Reset classes et applique le style Bootstrap approprié
  message.className = "alert mt-4 mb-0";
  const alertStyles = {
    ok: "alert-success",
    no: "alert-danger",
    warn: "alert-warning"
  };
  message.classList.add(alertStyles[kind] || "alert-secondary"); // Si kind n'est pas dans alertStyles, on utilise alert-secondary
}

function renderMessage(allowed, age, minAge) {
  // TODO:
  // - Si allowed true: " Accès autorisé (âge X, min Y)"
  // - Sinon: "Accès refusé (âge X, min Y)"
  // - Utiliser setMessage(...)
  // Exemple attendu:
  // setMessage(`Accès autorisé (âge ${age}, min ${minAge})`, "ok");
}

function appendLogItem(item) {
  const li = document.createElement("li");
  li.textContent =
    `${item.at} | dob=${item.birth} | region=${item.region} | âge=${item.age} | min=${item.minAge} | ` +
    (item.allowed ? "OK" : "REFUS");
  log.prepend(li);
}

function renderStats() {
  // TODO: si 0 tentative => texte "Aucune tentative."
  // Sinon:
  // - total = attempts.length
  // - allowedCount = attempts.filter(...)
  // - avgAge = attempts.reduce(...) / total (arrondi)
  // - afficher: "Tentatives: X | Autorisées: Y | Âge moyen: Z"
  //
  // Obligatoire: utiliser filter ET reduce
}

// ====== Événements ======
regionSelect.addEventListener("change", () => {
  customWrap.classList.toggle("d-none", regionSelect.value !== "CUSTOM");
});

form.addEventListener("submit", (e) => {
  e.preventDefault();

  let birthDateValue = birthDateInput.value; // "YYYY-MM-DD"
  let region = regionSelect.value;
  let customMinAge = Number(customAgeInput.value);

  // TODO: validations minimales
  // - si birthDateValue vide => setMessage("...", "warn") et return
  // - si date future => setMessage("...", "warn") et return

  let birthDate = new Date(birthDateValue + "T00:00:00");

  let age = calculateAge(birthDate); // TODO à coder
  let minAge = getMinAge(region, customMinAge); // TODO à coder

  // TODO: si age invalide (NaN ou < 0) OU minAge invalide => message warn + return

  let allowed = canPlay(age, minAge);

  let item = {
    birth: birthDateValue,
    region,
    minAge,
    age,
    allowed,
    at: new Date().toISOString()
  };

  attempts.push(item);

  renderMessage(allowed, age, minAge);
  appendLogItem(item);
  renderStats();
});

Conseils de pro


Mode expert