Skip to content

Commit 2d81367

Browse files
feat/boilerplate-code (#2)
1 parent 2d586b6 commit 2d81367

7 files changed

Lines changed: 637 additions & 124 deletions

File tree

src/js/core/timer.js

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// ========= START Live Coding: Timer Durations and State =========
2+
/**
3+
* Defines the duration in seconds for each Pomodoro mode. This centralized
4+
* configuration allows for easy adjustments to the timer settings.
5+
* @const
6+
* @type {Object<string, number>}
7+
*/
8+
export const DURATIONS = {
9+
focus: 25 * 60,
10+
'short-break': 5 * 60,
11+
'long-break': 15 * 60,
12+
};
13+
14+
/**
15+
* Creates the initial state object for the Pomodoro application.
16+
* This function ensures a consistent starting state, including the default mode,
17+
* timer duration, and empty task list.
18+
*
19+
* @returns {object} The default application state.
20+
*/
21+
export function createPomodoroState() {
22+
return {
23+
mode: 'focus', // The current timer mode (e.g., 'focus', 'short-break').
24+
remainingSeconds: DURATIONS.focus, // Time left in the current session.
25+
completedPomodoros: 0, // Number of completed focus sessions.
26+
isRunning: false, // Flag indicating if the timer is active.
27+
tasks: [], // Array of task objects associated with the session.
28+
};
29+
}
30+
// ========= END Live Coding =========
31+
32+
/**
33+
* Retrieves the configured duration for a given timer mode.
34+
*
35+
* @param {string} mode - The mode ('focus', 'short-break', 'long-break') whose duration is needed.
36+
* @returns {number} The duration in seconds for the specified mode. Defaults to 'focus' duration if the mode is invalid.
37+
*/
38+
export function getDurationForMode(mode) {
39+
return DURATIONS[mode] || DURATIONS.focus;
40+
}
41+
42+
/**
43+
* Returns a new state object transitioned to the specified mode.
44+
* This pure function calculates the new state without modifying the original,
45+
* setting the timer to the correct duration for the new mode and ensuring
46+
.
47+
*
48+
* @param {object} state - The current application state.
49+
* @param {string} mode - The target mode to switch to.
50+
* @returns {object} A new state object reflecting the mode change.
51+
*/
52+
// ========= START Live Coding: Mode Switching =========
53+
export function switchMode(state, mode) {
54+
const nextDuration = getDurationForMode(mode);
55+
return {
56+
...state,
57+
mode,
58+
remainingSeconds: nextDuration,
59+
isRunning: false,
60+
};
61+
}
62+
// ========= END Live Coding =========
63+
64+
/**
65+
* Processes a single tick of the timer, returning the next state.
66+
* If time is remaining, it decrements the timer by one second.
67+
* If the timer reaches zero, it transitions to the next logical mode
68+
* (e.g., from 'focus' to a break) and marks the cycle as completed.
69+
*
70+
* @param {object} state - The current application state.
71+
* @returns {{nextState: object, completedCycle: boolean}} An object containing the updated state and a flag indicating if the timer session finished.
72+
*/
73+
// ========= START Live Coding: Tick Logic =========
74+
export function tickTimer(state) {
75+
// If there's more than a second left, just decrement the timer.
76+
if (state.remainingSeconds > 1) {
77+
return {
78+
nextState: { ...state, remainingSeconds: state.remainingSeconds - 1 },
79+
completedCycle: false,
80+
};
81+
}
82+
83+
// --- Timer completion and mode transition logic ---
84+
let completedPomodoros = state.completedPomodoros;
85+
let nextMode;
86+
87+
// If a focus session ends, increment the pomodoro count.
88+
// Transition to a long break after every 4 pomodoros, otherwise a short break.
89+
if (state.mode === 'focus') {
90+
completedPomodoros += 1;
91+
nextMode = completedPomodoros % 4 === 0 ? 'long-break' : 'short-break';
92+
} else {
93+
// If a break ends, always transition back to focus.
94+
nextMode = 'focus';
95+
}
96+
97+
const nextDuration = getDurationForMode(nextMode);
98+
99+
// Return the new state for the next cycle.
100+
return {
101+
nextState: {
102+
...state,
103+
mode: nextMode,
104+
remainingSeconds: nextDuration,
105+
completedPomodoros,
106+
isRunning: false,
107+
},
108+
completedCycle: true, // Signal that the session has ended.
109+
};
110+
}
111+
// ========= END Live Coding =========
112+
113+
/**
114+
* Formats a duration in total seconds into a MM:SS string.
115+
*
116+
* @param {number} totalSeconds - The time in seconds to format.
117+
* @returns {string} The formatted time string (e.g., "25:00").
118+
*/
119+
export function formatTime(totalSeconds) {
120+
const minutes = Math.floor(totalSeconds / 60)
121+
.toString()
122+
.padStart(2, '0');
123+
const seconds = (totalSeconds % 60).toString().padStart(2, '0');
124+
return `${minutes}:${seconds}`;
125+
}

src/js/features/taskManager.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
export const MAX_TASKS = 10;
2+
3+
export function addTask(state, { title, notes }) {
4+
/*
5+
* Assignment (CRUD - Create):
6+
* Implement adding a task with title and notes, enforcing MAX_TASKS and required title.
7+
* Return the updated state with the new task appended.
8+
*/
9+
return state;
10+
}
11+
12+
export function editTask(state, taskId, updates) {
13+
/*
14+
* Assignment (CRUD - Update):
15+
* Find the task by id, update its fields, and return new state with the edited task.
16+
* Throw if the task is not found.
17+
*/
18+
return state;
19+
}
20+
21+
export function removeTask(state, taskId) {
22+
/*
23+
* Assignment (CRUD - Delete):
24+
* Remove the task matching taskId and return the new state.
25+
*/
26+
return state;
27+
}

src/js/main.js

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import {
2+
createPomodoroState,
3+
switchMode,
4+
tickTimer,
5+
getDurationForMode,
6+
formatTime,
7+
} from './core/timer.js';
8+
import { updateModeButtons } from './ui/render.js';
9+
10+
/**
11+
* Initializes the Pomodoro application by setting up the UI, state, and event listeners.
12+
* This function orchestrates the entire application, from DOM element selection to
13+
* timer management and task handling. It is designed to be self-contained and
14+
* can be safely run in both browser and server-side environments for testing.
15+
*
16+
* @param {Document} [doc=globalThis.document] - The document object to interact with,
17+
* allowing for dependency injection in non-browser environments.
18+
*/
19+
export function initializePomodoroApp(
20+
doc = globalThis.document === undefined ? null : globalThis.document
21+
) {
22+
// Abort initialization if the document or its core methods are unavailable.
23+
if (!doc?.getElementById) {
24+
return;
25+
}
26+
27+
// --- DOM Element Selection ---
28+
// Retrieve all necessary DOM elements for the application to function.
29+
// If any critical element is missing, the function will exit early.
30+
const timerDisplay = doc.getElementById('timer-display');
31+
const modeButtons = doc.querySelectorAll('[data-mode]');
32+
const startButton = doc.getElementById('start-btn');
33+
const pauseButton = doc.getElementById('pause-btn');
34+
const resetButton = doc.getElementById('reset-btn');
35+
const iterationCount = doc.getElementById('iteration-count');
36+
37+
// Ensure all critical UI components are present before proceeding.
38+
if (
39+
!timerDisplay ||
40+
!startButton ||
41+
!pauseButton ||
42+
!resetButton ||
43+
!iterationCount
44+
) {
45+
console.error('A critical UI element is missing from the DOM.');
46+
return;
47+
}
48+
49+
// --- State and Interval Management ---
50+
let state = createPomodoroState();
51+
let intervalId = null;
52+
53+
/**
54+
* Stops the main timer interval, preventing further ticks.
55+
* This function clears the active interval and updates the state to reflect
56+
* that the timer is no longer running.
57+
*/
58+
function stopTimer() {
59+
if (intervalId) {
60+
clearInterval(intervalId);
61+
intervalId = null;
62+
}
63+
state = { ...state, isRunning: false };
64+
}
65+
66+
// ========= START Live Coding: Render Pipeline =========
67+
/**
68+
* Main render pipeline.
69+
* This function synchronizes the entire UI with the current application state.
70+
* It updates the timer display, pomodoro completion count, and task list.
71+
* It also ensures that UI elements like mode buttons reflect the current state.
72+
*/
73+
function render() {
74+
timerDisplay.textContent = formatTime(state.remainingSeconds);
75+
iterationCount.textContent = `${state.completedPomodoros}`;
76+
// Update the visual state of mode selection buttons.
77+
updateModeButtons(modeButtons, state.mode);
78+
}
79+
// ========= END Live Coding =========
80+
81+
// ========= START Live Coding: Timer Controls (Start/Pause/Reset) =========
82+
/**
83+
* Starts the timer loop.
84+
* If the timer is not already running, it sets up a `setInterval` to tick
85+
* every second. On each tick, it updates the state and re-renders the UI.
86+
* If a pomodoro cycle completes, it automatically stops the timer.
87+
*/
88+
function startTimer() {
89+
if (intervalId) {
90+
return; // Prevent multiple intervals from running simultaneously.
91+
}
92+
state = { ...state, isRunning: true };
93+
intervalId = setInterval(() => {
94+
const { nextState, completedCycle } = tickTimer(state);
95+
state = nextState;
96+
render(); // Update UI every second.
97+
98+
// Stop the timer if the session (e.g., focus, break) has ended.
99+
if (completedCycle) {
100+
stopTimer();
101+
}
102+
}, 1000);
103+
}
104+
105+
/**
106+
* Pauses the timer by stopping the interval.
107+
*/
108+
function pauseTimer() {
109+
stopTimer();
110+
render(); // Ensure UI reflects the paused state.
111+
}
112+
113+
/**
114+
* Resets the timer to the initial state for the 'focus' mode.
115+
* It stops any active timer and restores the default duration.
116+
*/
117+
function resetTimer() {
118+
stopTimer();
119+
state = switchMode(
120+
{ ...state, remainingSeconds: getDurationForMode('focus') },
121+
'focus'
122+
);
123+
render();
124+
}
125+
126+
// --- Event Listener Attachments ---
127+
128+
// Attach core timer controls.
129+
startButton.addEventListener('click', startTimer);
130+
pauseButton.addEventListener('click', pauseTimer);
131+
resetButton.addEventListener('click', resetTimer);
132+
// ========= END Live Coding =========
133+
134+
// ========= START Live Coding: Mode Switching Events =========
135+
// Attach listeners for mode-switching buttons (Focus, Short Break, Long Break).
136+
for (const button of modeButtons) {
137+
button.addEventListener('click', () => {
138+
stopTimer();
139+
const newMode = button.dataset.mode;
140+
state = switchMode(state, newMode);
141+
render();
142+
});
143+
}
144+
// ========= END Live Coding =========
145+
146+
// --- Initial Render ---
147+
// Perform an initial render to display the default state when the app loads.
148+
render();
149+
}
150+
151+
/**
152+
* Expose the application initialization function to the global scope for browser environments.
153+
* This allows the application to be started from an inline script tag or developer console.
154+
* The application is automatically initialized once the DOM is fully loaded.
155+
*/
156+
if (globalThis.window !== undefined) {
157+
globalThis.PomodoroApp = {
158+
initializePomodoroApp,
159+
};
160+
globalThis.addEventListener('DOMContentLoaded', () => initializePomodoroApp());
161+
}

src/js/ui/render.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* Updates the visual state of mode-switching buttons to reflect the active timer mode.
3+
* It adds an 'active' class to the button corresponding to the current mode and
4+
* removes it from all other mode buttons.
5+
*
6+
* @param {NodeListOf<HTMLButtonElement>} modeButtons - A collection of the mode-switching buttons.
7+
* @param {string} activeMode - The identifier of the currently active mode (e.g., 'focus').
8+
*/
9+
export function updateModeButtons(modeButtons, activeMode) {
10+
for (const button of modeButtons) {
11+
if (button.dataset.mode === activeMode) {
12+
button.classList.add('active');
13+
} else {
14+
button.classList.remove('active');
15+
}
16+
}
17+
}

src/pomodoro.html

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,40 @@
33
<head>
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6-
<title>GDG Countdown App</title>
6+
<title>Pomodoro Timer MVP</title>
7+
<link rel="stylesheet" href="./styles/pomodoro.css">
78
</head>
89
<body>
9-
<h1>GDG Countdown to internships</h1>
10-
<script src="./js/pomodoro.js"></script>
10+
<header class="page-header">
11+
<div>
12+
<p class="eyebrow">Live Coding Classroom</p>
13+
<h1>GDG Pomodoro</h1>
14+
<p class="lede">Focus timer with task notes, built for demonstrating DOM manipulation and state management.</p>
15+
</div>
16+
</header>
17+
18+
<main class="layout">
19+
<!-- ========= START Live Coding: Timer UI ========= -->
20+
<section class="panel timer-panel">
21+
<header class="panel-header">
22+
<div class="mode-switch">
23+
<button class="mode-btn active" data-mode="focus" aria-label="Switch to focus">Focus</button>
24+
<button class="mode-btn" data-mode="short-break" aria-label="Switch to short break">Short Break</button>
25+
<button class="mode-btn" data-mode="long-break" aria-label="Switch to long break">Long Break</button>
26+
</div>
27+
</header>
28+
29+
<div class="timer-display" id="timer-display" aria-live="polite">25:00</div>
30+
31+
<div class="controls">
32+
<button class="btn-primary" id="start-btn">Start</button>
33+
<button class="btn-secondary" id="pause-btn">Pause</button>
34+
<button class="btn-ghost" id="reset-btn">Reset</button>
35+
</div>
36+
</section>
37+
<!-- ========= END Live Coding ========= -->
38+
</main>
39+
40+
<script type="module" src="./js/main.js"></script>
1141
</body>
12-
</html>
42+
</html>

0 commit comments

Comments
 (0)