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
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,
};
}
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';
}
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" />
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);
}
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);
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.
- 📦 Repo: https://github.com/sen-ltd/pomodoro-focus
- 🌐 Live: https://sen.ltd/portfolio/pomodoro-focus/
- 🏢 Company: https://sen.ltd/

Top comments (0)