Skip to content

Commit d0eb8bc

Browse files
committed
feat(plays): add pomodoro timer play
create a pomodoro timer with focus, short break, and long break sessions. add scroll-based time selection, timer controls, and automatic session handling using react functional components and hooks.
1 parent 3c30ee2 commit d0eb8bc

8 files changed

Lines changed: 462 additions & 0 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React from 'react';
2+
3+
import TimerDisplay from './components/TimerDisplay';
4+
import TimerControls from './components/TimerControls';
5+
import SessionSelector from './components/SessionSelector';
6+
7+
import { usePomodoroTimer, SessionType } from './hooks/usePomodoroTimer';
8+
9+
import './styles.css';
10+
11+
function PomodoroTimer(): JSX.Element {
12+
const { session, timeLeft, start, pause, reset, updateSessionTime, changeSession } =
13+
usePomodoroTimer();
14+
15+
return (
16+
<div className="play-details">
17+
<div className="play-details-body">
18+
<div className="pomodoro-card">
19+
{/* Title */}
20+
<div className="pomodoro-title">Pomodoro Timer</div>
21+
22+
{/* Session Selector */}
23+
<SessionSelector current={session} onChange={changeSession} />
24+
25+
{/* Timer Display */}
26+
<TimerDisplay session={session} timeLeft={timeLeft} onTimeChange={updateSessionTime} />
27+
28+
{/* Controls */}
29+
<TimerControls onPause={pause} onReset={reset} onStart={start} />
30+
</div>
31+
</div>
32+
</div>
33+
);
34+
}
35+
36+
export default PomodoroTimer;

‎src/plays/pomodoro-timer/Readme.md‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Pomodoro Timer
2+
3+
A customizable Pomodoro Timer built with React and TypeScript that allows users to set focus and break durations. It includes start, pause, and reset controls and automatically switches between sessions. This play demonstrates React hooks, state management, timer logic, and modular component design.
4+
5+
## Play Demographic
6+
7+
- Language: ts
8+
- Level: Intermediate
9+
10+
## Creator Information
11+
12+
- User: deansereigns
13+
- Gihub Link: https://github.com/deansereigns
14+
- Blog:
15+
- Video:
16+
17+
## Implementation Details
18+
19+
Update your implementation idea and details here
20+
21+
## Consideration
22+
23+
Update all considerations(if any)
24+
25+
## Resources
26+
27+
Update external resources(if any)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import React from 'react';
2+
3+
interface Props {
4+
minutes: number;
5+
seconds: number;
6+
onChange: (minutes: number, seconds: number) => void;
7+
}
8+
9+
const DualTimePicker: React.FC<Props> = ({ minutes, seconds, onChange }) => {
10+
const wrap = (value: number, max: number) => {
11+
if (value < 0) return max;
12+
if (value > max) return 0;
13+
14+
return value;
15+
};
16+
17+
const changeMinutes = (delta: number) => {
18+
const newMinutes = wrap(minutes + delta, 59);
19+
onChange(newMinutes, seconds);
20+
};
21+
22+
const changeSeconds = (delta: number) => {
23+
const newSeconds = wrap(seconds + delta, 59);
24+
onChange(minutes, newSeconds);
25+
};
26+
27+
const Wheel = ({ value, onChangeFn }: { value: number; onChangeFn: (delta: number) => void }) => {
28+
const prev = wrap(value - 1, 59);
29+
const next = wrap(value + 1, 59);
30+
31+
return (
32+
<div className="wheel">
33+
<div className="wheel-item faded" onClick={() => onChangeFn(-1)}>
34+
{String(prev).padStart(2, '0')}
35+
</div>
36+
37+
<div className="wheel-item active">{String(value).padStart(2, '0')}</div>
38+
39+
<div className="wheel-item faded" onClick={() => onChangeFn(1)}>
40+
{String(next).padStart(2, '0')}
41+
</div>
42+
</div>
43+
);
44+
};
45+
46+
return (
47+
<div className="dual-picker">
48+
<Wheel value={minutes} onChangeFn={changeMinutes} />
49+
50+
<div className="separator">:</div>
51+
52+
<Wheel value={seconds} onChangeFn={changeSeconds} />
53+
</div>
54+
);
55+
};
56+
57+
export default DualTimePicker;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import React from 'react';
2+
import { SessionType } from '../hooks/usePomodoroTimer';
3+
4+
interface Props {
5+
current: SessionType;
6+
onChange: (session: SessionType) => void;
7+
}
8+
9+
const SessionSelector: React.FC<Props> = ({ current, onChange }) => {
10+
const sessions: SessionType[] = ['focus', 'shortBreak', 'longBreak'];
11+
12+
const labels: Record<SessionType, string> = {
13+
focus: 'Focus',
14+
shortBreak: 'Short Break',
15+
longBreak: 'Long Break'
16+
};
17+
18+
return (
19+
<div className="session-selector">
20+
{sessions.map((session) => (
21+
<button
22+
className={current === session ? 'session-btn active' : 'session-btn'}
23+
key={session}
24+
onClick={() => onChange(session)}
25+
>
26+
{labels[session]}
27+
</button>
28+
))}
29+
</div>
30+
);
31+
};
32+
33+
export default SessionSelector;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import React from 'react';
2+
3+
interface Props {
4+
onStart: () => void;
5+
onPause: () => void;
6+
onReset: () => void;
7+
}
8+
9+
const TimerControls: React.FC<Props> = ({ onStart, onPause, onReset }) => {
10+
return (
11+
<div className="timer-controls">
12+
<button className="control-btn start" onClick={onStart}>
13+
Start
14+
</button>
15+
16+
<button className="control-btn pause" onClick={onPause}>
17+
Pause
18+
</button>
19+
20+
<button className="control-btn reset" onClick={onReset}>
21+
Reset
22+
</button>
23+
</div>
24+
);
25+
};
26+
27+
export default TimerControls;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React, { useEffect, useRef, useState } from 'react';
2+
3+
import DualTimePicker from './DualTimePicker';
4+
5+
interface Props {
6+
session: string;
7+
timeLeft: number;
8+
onTimeChange: (seconds: number) => void;
9+
}
10+
11+
const TimerDisplay: React.FC<Props> = ({ session, timeLeft, onTimeChange }) => {
12+
const [editing, setEditing] = useState(false);
13+
14+
const containerRef = useRef<HTMLDivElement>(null);
15+
16+
const minutes = Math.floor(timeLeft / 60);
17+
18+
const seconds = timeLeft % 60;
19+
20+
const formatted = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
21+
22+
useEffect(() => {
23+
const handleClickOutside = (event: MouseEvent) => {
24+
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
25+
setEditing(false);
26+
}
27+
};
28+
29+
document.addEventListener('mousedown', handleClickOutside);
30+
31+
return () => document.removeEventListener('mousedown', handleClickOutside);
32+
}, []);
33+
34+
const handleChange = (m: number, s: number) => {
35+
onTimeChange(m * 60 + s);
36+
};
37+
38+
return (
39+
<div className="timer-display" ref={containerRef}>
40+
<div className="session-label">{session.toUpperCase()}</div>
41+
42+
{!editing && (
43+
<div className="timer-time" onClick={() => setEditing(true)}>
44+
{formatted}
45+
</div>
46+
)}
47+
48+
{editing && <DualTimePicker minutes={minutes} seconds={seconds} onChange={handleChange} />}
49+
</div>
50+
);
51+
};
52+
53+
export default TimerDisplay;
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { useEffect, useRef, useState } from 'react';
2+
3+
export type SessionType = 'focus' | 'shortBreak' | 'longBreak';
4+
5+
const DEFAULT_TIMES = {
6+
focus: 25 * 60,
7+
shortBreak: 5 * 60,
8+
longBreak: 15 * 60
9+
};
10+
11+
export const usePomodoroTimer = () => {
12+
const [session, setSession] = useState<SessionType>('focus');
13+
14+
const [timeLeft, setTimeLeft] = useState<number>(DEFAULT_TIMES.focus);
15+
16+
const [isRunning, setIsRunning] = useState<boolean>(false);
17+
18+
const intervalRef = useRef<NodeJS.Timeout | null>(null);
19+
20+
/* Timer logic */
21+
useEffect(() => {
22+
if (!isRunning) return;
23+
24+
intervalRef.current = setInterval(() => {
25+
setTimeLeft((prev) => {
26+
if (prev <= 1) {
27+
switchSession();
28+
29+
return 0;
30+
}
31+
32+
return prev - 1;
33+
});
34+
}, 1000);
35+
36+
return () => {
37+
if (intervalRef.current) clearInterval(intervalRef.current);
38+
};
39+
}, [isRunning]);
40+
41+
/* Switch session automatically */
42+
const switchSession = () => {
43+
if (session === 'focus') {
44+
changeSession('shortBreak');
45+
} else {
46+
changeSession('focus');
47+
}
48+
};
49+
50+
/* Manual session change */
51+
const changeSession = (newSession: SessionType) => {
52+
setSession(newSession);
53+
setTimeLeft(DEFAULT_TIMES[newSession]);
54+
setIsRunning(false);
55+
};
56+
57+
/* Start */
58+
const start = () => {
59+
setIsRunning(true);
60+
};
61+
62+
/* Pause */
63+
const pause = () => {
64+
setIsRunning(false);
65+
};
66+
67+
/* Reset */
68+
const reset = () => {
69+
setIsRunning(false);
70+
setTimeLeft(DEFAULT_TIMES[session]);
71+
};
72+
73+
/* Update time from scroll wheel */
74+
const updateSessionTime = (seconds: number) => {
75+
setTimeLeft(seconds);
76+
};
77+
78+
return {
79+
session,
80+
timeLeft,
81+
start,
82+
pause,
83+
reset,
84+
updateSessionTime,
85+
changeSession
86+
};
87+
};

0 commit comments

Comments
 (0)