Skip to content
← 1001 Widgets
N° 036 · ●●○

Kitchen timers

Pasta, an egg and rice at once: several timers side by side, each with its own pause, bar and sound. The right exercise to handle a list of items that all live at the same time.

JS
What you'll learn
  • Run several timers with a single loop
  • Compute the time left from an end time
  • Make a beep with the Web Audio API
Why this language

Pourquoi des minuteurs de cuisine ? En plein service, rien ne cuit au même rythme. Un seul minuteur ne suffit pas, et une pile de téléphones non plus.

Chaque minuteur retient son heure de fin, pas un nombre de secondes qui diminue. Le temps restant est la différence avec l'heure actuelle : même si l'onglet dort une minute, il reste juste. Une seule boucle parcourt toute la liste, plutôt qu'un setInterval par minuteur.

Le signal sonore est fabriqué à la volée par la Web Audio API, sans aucun fichier son.

Pourquoi en JavaScript simple ? Un tableau d'objets, une boucle et un oscillateur. La liste se trie pour mettre en haut ce qui sonne bientôt.

How it works
  1. UN MINUTEUR = UNE HEURE DE FIN

    On ne décompte pas : on compare l'heure de fin à maintenant.

    let minuteurs = [];
    function ajouter(nom, secondes) {
      minuteurs.push({ id: crypto.randomUUID(), nom, total: secondes * 1000,
                       fin: Date.now() + secondes * 1000, pause: null, fini: false });
    }
  2. LE TEMPS RESTANT

    En pause, on garde le reste figé ; sinon on le calcule.

    const reste = m => m.pause !== null ? m.pause : Math.max(0, m.fin - Date.now());
  3. PAUSE ET REPRISE

    function basculer(m) {
      if (m.pause !== null) { m.fin = Date.now() + m.pause; m.pause = null; }
      else m.pause = reste(m);
    }
  4. UNE SEULE BOUCLE POUR TOUS

    setInterval(() => {
      for (const m of minuteurs) {
        if (!m.fini && reste(m) === 0) { m.fini = true; bip(); }
      }
      afficher();
    }, 250);
  5. LE BIP

    Un oscillateur à 880 Hz, avec un volume qui monte puis retombe pour éviter le clic.

    const audio = new AudioContext();
    function bip() {
      const o = audio.createOscillator(), g = audio.createGain(), t = audio.currentTime;
      o.frequency.value = 880;
      g.gain.setValueAtTime(0.0001, t);
      g.gain.exponentialRampToValueAtTime(0.25, t + 0.02);
      g.gain.exponentialRampToValueAtTime(0.0001, t + 0.5);
      o.connect(g).connect(audio.destination);
      o.start(t); o.stop(t + 0.55);
    }
Pitfalls

PIÈGE 1 : Un setInterval par minuteur

Pitfall

Mauvais : lancer un intervalle pour chaque minuteur. On finit par en oublier un, qui tourne encore après suppression.

Fix

Correct : une seule boucle qui parcourt la liste, et une liste qu'on filtre pour supprimer.

PIÈGE 2 : Décompter les secondes

Pitfall

Mauvais : restant -= 1 à chaque seconde. Dans un onglet en arrière-plan, les tics ralentissent et les pâtes sont trop cuites.

Fix

Correct : retenir l'heure de fin et calculer la différence à chaque passage.

PIÈGE 3 : Un son bloqué

Pitfall

Mauvais : créer l'AudioContext au chargement de la page. Les navigateurs le gardent muet tant que l'utilisateur n'a pas cliqué.

Fix

Correct : le créer, ou appeler resume, au premier clic sur un bouton.

PERSONNALISER
  • Une notification système avec l'API Notification quand l'onglet n'est pas visible.
  • Des préréglages sauvegardés : la cuisson de vos pâtes préférées en un clic.
  • Un bouton « + 1 min » pour prolonger une cuisson.
OPTIMISATION
  • Mettez le titre de l'onglet à jour avec le minuteur le plus proche : on le voit même sur un autre onglet.
  • Gardez la liste dans localStorage avec les heures de fin : un rechargement ne perd rien.
VERS REACT

La liste vit dans un useState ; un seul useEffect lance l'intervalle et le nettoie au démontage.

Un état « maintenant » mis à jour par l'intervalle force le rendu, et chaque reste se calcule à partir de lui.

L'AudioContext se garde dans un useRef, créé au premier clic.

Comments
0/1000

Loading comments…

Audience measurement

With your consent, this site measures its traffic with Google Analytics and the Meta pixel, via Google Tag Manager. Nothing is loaded until you choose, and you can change your mind at any time with the "Cookies" link at the bottom of the page.