DEV Community

SEN LLC
SEN LLC

Posted on

A Pomodoro Timer That Logs Every Session and Shows Weekly Stats — No Framework

A Pomodoro Timer That Logs Every Session and Shows Weekly Stats — No Framework

Most Pomodoro apps are just a countdown. This one logs every session with a category, persists to localStorage, and shows weekly stats as CSS-only bar charts. The timer logic is a set of pure functions operating on an immutable session object.

The Pomodoro Technique is simple: 25 minutes of focused work, 5-minute break, repeat 4 times, then a 15-minute long break. But tracking what you focused on and how much you accumulated over a week turns a timer into a productivity tool.

🔗 Live demo: https://sen.ltd/portfolio/pomodoro-focus/
📦 GitHub: https://github.com/sen-ltd/pomodoro-focus

Screenshot

Features:

  • 25/5/15 min timer (configurable)
  • 4-cycle tracking with automatic long breaks
  • Category labels (Work, Study, Exercise, Other)
  • Session log persisted to localStorage
  • Weekly stats (total minutes per category, daily session counts)
  • SVG circular progress ring
  • Web Notification + Web Audio API beep
  • Keyboard shortcuts (Space, R, 1-4)
  • Japanese / English, dark / light theme
  • Zero dependencies, 29 tests

Immutable session objects

Each Pomodoro session is a plain object that flows through pure functions:

export function createSession(category, duration) {
  return {
    id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
    category,
    duration,
    remaining: duration,
    status: 'idle',
    startedAt: null,
    completedAt: null,
  };
}
Enter fullscreen mode Exit fullscreen mode

startSession, tickSession, completeSession, abandonSession each return a new object. The main.js timer loop calls tickSession every second and renders the result.

Phase cycling

After each completed work session, getNextPhase determines what comes next:

export function getNextPhase(completedCount) {
  if (completedCount === 0) return 'work';
  if (completedCount % (DEFAULTS.cyclesBeforeLong * 2) === 0) return 'work';
  if (completedCount % 2 === 0) return 'work';
  if (Math.ceil(completedCount / 2) % DEFAULTS.cyclesBeforeLong === 0) return 'longBreak';
  return 'break';
}
Enter fullscreen mode Exit fullscreen mode

The pattern: work → break → work → break → work → break → work → long break → repeat. The function counts completed sessions (both work and break) to determine position in the cycle.

SVG progress ring

The countdown is visualized as a circular progress ring using SVG stroke-dasharray:

<circle r="90" cx="100" cy="100" 
        stroke-dasharray="565.48" 
        stroke-dashoffset="0" />
Enter fullscreen mode Exit fullscreen mode

As time progresses, stroke-dashoffset increases from 0 to the full circumference (2πr). The visual effect is the ring "unwinding" counterclockwise — a satisfying countdown that's more glanceable than digits alone.

Weekly stats without a chart library

The stats view uses pure CSS for bar charts:

.bar {
  height: calc(var(--value) / var(--max) * 100%);
  background: var(--accent);
}
Enter fullscreen mode Exit fullscreen mode

Each bar's height is computed as a percentage of the maximum value. CSS custom properties (--value, --max) are set via JavaScript. No Canvas, no SVG, no D3 — just div elements with dynamic heights.

Web Audio beep

When a session completes, a short sine wave plays:

const ctx = new AudioContext();
const osc = ctx.createOscillator();
osc.frequency.value = 800;
osc.connect(ctx.destination);
osc.start();
setTimeout(() => osc.stop(), 200);
Enter fullscreen mode Exit fullscreen mode

Three quick beeps (800Hz, 200ms each) with 100ms gaps. Enough to notice, not enough to annoy.

Series

This is entry #37 in my 100+ public portfolio series.

Top comments (0)