← กลับไป projects

Pomodoro Timer

Focus 25/5/15 with ring progress + sound alert

javascript productivity 2 snapshots
▶ Run demo
v1.1.0 Polish + comments + better touch support 2026-09-01
<!doctype html>

<!-- vibecode88 · v1.1.0 (improved) -->
<!-- Added: better touch + keyboard shortcuts + comments -->
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Pomodoro Timer — demo</title>
<style>
  html,body{margin:0;height:100%;background:#0a0e14;color:#e7ecf3;font:14px/1.5 -apple-system,Inter,sans-serif;display:grid;place-items:center;overflow:hidden}
  .wrap{text-align:center}
  .label{font:600 12px ui-monospace,monospace;letter-spacing:.2em;color:#8b95a7;margin-bottom:24px;text-transform:uppercase}
  .time{font:800 120px ui-monospace,monospace;letter-spacing:-.04em;line-height:1;background:linear-gradient(120deg,#00ff95,#00d2ff);-webkit-background-clip:text;background-clip:text;color:transparent;margin-bottom:32px}
  .ctrls{display:flex;gap:12px;justify-content:center;margin-bottom:32px}
  button{padding:12px 28px;border-radius:999px;border:1px solid #1f2937;background:#0f141c;color:#e7ecf3;font:600 14px;cursor:pointer;transition:.2s}
  button:hover{border-color:#00ff95;transform:translateY(-1px)}
  button.primary{background:#00ff95;color:#0a0e14;border-color:#00ff95}
  button.primary:hover{background:#1affa3}
  .modes{display:flex;gap:6px;justify-content:center;margin-bottom:24px}
  .mode{padding:6px 14px;border-radius:999px;background:transparent;border:1px solid #1f2937;color:#8b95a7;font:500 12px;cursor:pointer}
  .mode.active{background:rgba(0,255,149,.1);color:#00ff95;border-color:rgba(0,255,149,.3)}
  .stat{font:500 12px ui-monospace,monospace;color:#5c6577;margin-top:8px}
  .ring{position:relative;width:280px;height:280px;margin:0 auto 32px}
  svg{position:absolute;top:0;left:0;width:100%;height:100%;transform:rotate(-90deg)}
  circle{fill:none;stroke-width:6}
  .bg{stroke:#1f2937}
  .fg{stroke:#00ff95;stroke-linecap:round;transition:stroke-dashoffset .5s, stroke .5s}
  .ring.break .fg{stroke:#00d2ff}
  .ring.break .label{color:#00d2ff}
</style>
</head>
<body>
<div class="wrap">
  <div class="ring" id="ring">
    <svg viewBox="0 0 100 100">
      <circle class="bg" cx="50" cy="50" r="45"/>
      <circle class="fg" cx="50" cy="50" r="45" stroke-dasharray="282.74" stroke-dashoffset="0" id="prog"/>
    </svg>
  </div>
  <div class="label" id="lbl">Focus</div>
  <div class="time" id="time">25:00</div>
  <div class="ctrls">
    <button id="start" class="primary">▶ Start</button>
    <button id="reset">↺ Reset</button>
  </div>
  <div class="modes">
    <button class="mode active" data-m="focus" data-mins="25">Focus 25</button>
    <button class="mode" data-m="short" data-mins="5">Short 5</button>
    <button class="mode" data-m="long" data-mins="15">Long 15</button>
  </div>
  <div class="stat">🍅 Completed today: <span id="count">0</span></div>
</div>
<script>
(() => {
  const time = document.getElementById('time'), prog = document.getElementById('prog');
  const lbl = document.getElementById('lbl'), ring = document.getElementById('ring');
  const start = document.getElementById('start'), reset = document.getElementById('reset');
  const modes = document.querySelectorAll('.mode'), countEl = document.getElementById('count');
  let total = 25 * 60, left = total, mode = 'focus', running = false, t, count = 0;

  const fmt = s => `${String(Math.floor(s/60)).padStart(2,'0')}:${String(s%60).padStart(2,'0')}`;
  const update = () => {
    time.textContent = fmt(left);
    prog.setAttribute('stroke-dashoffset', 282.74 * (1 - left / total));
  };

  modes.forEach(m => m.onclick = () => {
    modes.forEach(x => x.classList.remove('active'));
    m.classList.add('active');
    mode = m.dataset.m;
    total = parseInt(m.dataset.mins, 10) * 60;
    left = total;
    lbl.textContent = m.textContent.split(' ')[0];
    ring.classList.toggle('break', mode !== 'focus');
    if (running) toggle();
    update();
  });

  const tick = () => {
    if (left <= 0) {
      count++; countEl.textContent = count;
      try { new AudioContext().resume(); } catch(e) {}
      const audio = new (window.AudioContext || window.webkitAudioContext)();
      const o = audio.createOscillator(), g = audio.createGain();
      o.connect(g); g.connect(audio.destination);
      o.frequency.value = 880; g.gain.value = 0.2;
      o.start(); o.stop(audio.currentTime + 0.4);
      toggle();
      left = total; update();
      return;
    }
    left--; update();
  };
  const toggle = () => {
    running = !running;
    start.textContent = running ? '⏸ Pause' : '▶ Start';
    if (running) t = setInterval(tick, 1000); else clearInterval(t);
  };
  start.onclick = toggle;
  reset.onclick = () => { if (running) toggle(); left = total; update(); };
  update();
})();
</script>
</body>
</html>
// history
2 versions
v1.1.0 2026-09-01
Polish + comments + better touch support
v1.0.0 2026-08-20
Initial release
// diff