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.
- 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 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.
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>
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');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}`; }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); }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(); }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); }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); }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ÈGE 1 : Compter les secondes
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.
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
Mauvais : appeler setInterval à chaque clic sur Démarrer. Deux clics, deux minuteries, et l'affichage clignote entre deux valeurs.
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
Mauvais : créer l'AudioContext au chargement de la page. Les navigateurs bloquent tout son tant que l'utilisateur n'a pas interagi.
Correct : créer l'AudioContext dans le gestionnaire du premier clic.
- 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.
- 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.
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.
Chargement des commentaires…