Aller au contenu
← 1001 Widgets
N° 004 · ●○○

Pomodoro

Un minuteur Pomodoro : 25 minutes de travail, 5 de pause, et un signal quand ça bascule. Le bon exercice pour apprendre à gérer le temps qui passe en JavaScript.

HTMLJS
Ce qu'on apprend
  • Calculer le temps restant depuis une heure de fin
  • Démarrer et arrêter une minuterie sans doublon
  • Jouer un son avec l'API Web Audio
Pourquoi ce langage

Pourquoi un minuteur ? Parce que le temps est le piège classique du débutant. On croit qu'il suffit de retirer une seconde toutes les 1000 ms, et le minuteur se met à dériver dès que l'onglet passe en arrière-plan.

Ici, on ne compte pas les secondes : on retient l'heure de fin, et on recalcule le temps restant à chaque affichage. Le minuteur reste juste, même quand le navigateur ralentit.

Pourquoi en JavaScript simple ? Un minuteur tient en une cinquantaine de lignes. Pas besoin de framework pour comprendre setInterval, Date.now() et la mise à jour de la page.

Comment ça marche
  1. LE HTML

    Un libellé, le temps restant, deux boutons. L'élément <output> annonce le temps aux lecteurs d'écran.

    <div class="pomodoro">
      <p id="phase">Travail</p>
      <output id="temps" aria-live="polite">25:00</output>
      <button id="lancer" type="button">Démarrer</button>
      <button id="reset" type="button">Remettre à zéro</button>
    </div>
  2. L'ÉTAT

    On garde la phase en cours, le temps restant, et l'heure de fin quand le minuteur tourne.

    const DUREES = { travail: 25 * 60 * 1000, pause: 5 * 60 * 1000 };
    let phase = 'travail';
    let restant = DUREES.travail;
    let fin = null;
    let minuterie = null;
    
    const libelle = document.getElementById('phase');
    const temps = document.getElementById('temps');
    const lancer = document.getElementById('lancer');
  3. L'AFFICHAGE

    Des millisecondes vers mm:ss. Le titre de l'onglet suit, pour voir le temps depuis un autre onglet.

    function afficher(ms) {
      const s = Math.ceil(ms / 1000);
      const mm = String(Math.floor(s / 60)).padStart(2, '0');
      const ss = String(s % 60).padStart(2, '0');
      temps.textContent = `${mm}:${ss}`;
      document.title = `${mm}:${ss} · ${libelle.textContent}`;
    }
  4. LE TIC

    On ne retire pas une seconde : on recalcule ce qui reste jusqu'à l'heure de fin.

    function tic() {
      restant = fin - Date.now();
      if (restant <= 0) return basculer();
      afficher(restant);
    }
  5. DÉMARRER ET METTRE EN PAUSE

    Le même bouton fait les deux. On arrête toujours l'ancienne minuterie avant d'en créer une.

    function demarrerOuPause() {
      if (minuterie) {
        clearInterval(minuterie);
        minuterie = null;
        lancer.textContent = 'Reprendre';
        return;
      }
      fin = Date.now() + restant;
      minuterie = setInterval(tic, 250);
      lancer.textContent = 'Pause';
      tic();
    }
  6. BASCULER

    Fin d'une phase : on passe à l'autre, on repart de sa durée, et on prévient.

    function basculer() {
      phase = phase === 'travail' ? 'pause' : 'travail';
      libelle.textContent = phase === 'travail' ? 'Travail' : 'Pause';
      restant = DUREES[phase];
      fin = Date.now() + restant;
      bip();
      afficher(restant);
    }
  7. LE SIGNAL

    Un bip court avec l'API Web Audio, sans fichier son.

    let audio = null;
    function bip() {
      if (!audio) return;
      const o = audio.createOscillator();
      o.frequency.value = 880;
      o.connect(audio.destination);
      o.start();
      o.stop(audio.currentTime + 0.2);
    }
  8. LES ÉCOUTEURS

    Le son ne peut naître qu'après un clic : on crée l'AudioContext au premier Démarrer.

    lancer.addEventListener('click', () => {
      audio ??= new AudioContext();
      demarrerOuPause();
    });
    document.getElementById('reset').addEventListener('click', () => {
      clearInterval(minuterie);
      minuterie = null;
      phase = 'travail';
      libelle.textContent = 'Travail';
      restant = DUREES.travail;
      lancer.textContent = 'Démarrer';
      afficher(restant);
    });
Pièges

PIÈGE 1 : Compter les secondes

Piège

Mauvais : retirer 1000 ms au temps restant dans un setInterval de 1000 ms. Dès que l'onglet passe en arrière-plan, le navigateur ralentit les minuteries et le compte prend plusieurs secondes de retard.

Fix

Correct : retenir l'heure de fin avec Date.now() + durée, puis recalculer le temps restant à chaque tic. Le retard ne s'accumule jamais.

PIÈGE 2 : Lancer deux minuteries

Piège

Mauvais : appeler setInterval à chaque clic sur Démarrer. Deux clics, deux minuteries, et l'affichage clignote entre deux valeurs.

Fix

Correct : garder la référence renvoyée par setInterval, et appeler clearInterval avant d'en créer une nouvelle.

PIÈGE 3 : Le son qui ne sort pas

Piège

Mauvais : créer l'AudioContext au chargement de la page. Les navigateurs bloquent tout son tant que l'utilisateur n'a pas interagi.

Fix

Correct : créer l'AudioContext dans le gestionnaire du premier clic.

PERSONNALISER
  • Durées réglables : remplacez les valeurs de DUREES par deux champs <input type="number"> lus au clic sur Démarrer.
  • Couleur par phase : ajoutez une classe .is-pause sur le conteneur dans basculer(), et laissez le CSS changer la couleur du temps.
  • Un autre son : changez o.frequency.value (440 pour plus grave, 1320 pour plus aigu), ou jouez trois bips courts à la suite.
OPTIMISATION
  • Un tic toutes les 250 ms suffit : l'affichage ne change qu'une fois par seconde, et on évite un décalage visible d'une seconde au démarrage.
  • Après quatre cycles de travail, passez à une longue pause de 15 minutes : un compteur de cycles suffit.
  • Pour prévenir même quand l'onglet est caché, ajoutez l'API Notification, avec la permission demandée au premier clic.
VERS REACT

L'heure de fin et la minuterie vont dans un useRef : elles changent sans devoir redessiner le composant.

Le temps affiché va dans un useState, mis à jour par le tic.

Le useEffect qui crée la minuterie renvoie une fonction qui appelle clearInterval : sans elle, la minuterie continue de tourner après la fermeture du composant.

Variantes
Commentaires
0/1000

Chargement des commentaires…