// ── 23-sprints.js — Sprint mode ──
const STORE_SPRINTS = 'wl_sprints_v1';
let sprintLog = [];
let _sprintActive = false;
let _sprintIntention = '';
let _sprintDuration = 25; // minutes
let _sprintEntryId = null;
let _onSprintEnd = null;
const SPRINT_DURATIONS = [15, 25, 45, 60];
/**
* Loads sprint history from localStorage into the module-level `sprintLog` array.
* Falls back to an empty array and logs a warning on parse failure.
* @returns {void}
*/
function loadSprintLog() {
try {
sprintLog = JSON.parse(localStorage.getItem(STORE_SPRINTS) || '[]');
} catch (e) {
sprintLog = [];
wlLog.warn('loadSprintLog: failed to parse sprint log from localStorage', e);
}
}
/**
* Persists the current `sprintLog` array to localStorage.
* @returns {void}
*/
function saveSprintLog() {
localStorage.setItem(STORE_SPRINTS, JSON.stringify(sprintLog));
}
/**
* Shows the sprint setup panel, clears the intention input, resets the
* duration selection to 25 minutes, and focuses the intention field.
* @returns {void}
*/
function openSprintSetup() {
const el = document.getElementById('sprintSetup');
if (!el) return;
el.style.display = '';
document.getElementById('sprintIntention').value = '';
_sprintDuration = 25;
renderSprintDurations();
document.getElementById('sprintIntention').focus();
}
/**
* Renders the duration chip row inside #sprintDurations, marking the
* currently selected duration active. Attaches click listeners to each chip.
* @returns {void}
*/
function renderSprintDurations() {
const el = document.getElementById('sprintDurations');
if (!el) return;
el.innerHTML = SPRINT_DURATIONS.map(
(d) =>
`<button class="add-btn sprint-dur-btn${d === _sprintDuration ? ' sprint-dur-active' : ''}"
data-dur="${d}">${d}m</button>`
).join('');
el.querySelectorAll('.sprint-dur-btn').forEach((btn) => {
btn.addEventListener('click', () => {
_sprintDuration = parseInt(btn.dataset.dur, 10);
renderSprintDurations();
});
});
}
/**
* Commits a new sprint: reads the intention from the input, creates a time-log
* entry, starts the main timer, starts the Pomodoro for `_sprintDuration`
* minutes, and sets `_onSprintEnd` so the review panel is shown when the ring
* reaches zero. No-ops with a focus call if the intention field is empty.
* @returns {void}
*/
function startSprint() {
_sprintIntention = document.getElementById('sprintIntention').value.trim();
if (!_sprintIntention) {
wlLog.info('startSprint: rejected — empty intention');
document.getElementById('sprintIntention').focus();
return;
}
wlLog.info('startSprint: starting', { duration: _sprintDuration });
document.getElementById('sprintSetup').style.display = 'none';
_sprintActive = true;
const entry = {
id: Date.now() + '',
text: _sprintIntention,
tag: selectedTag,
ts: safeRoundedStart(),
date: dk(new Date()),
_sprintDuration,
};
entries.push(entry);
_sprintEntryId = entry.id;
save();
if (activeTimer) stopTimer();
startTimer(entry.id);
const focusIntention = document.getElementById('sprintFocusIntention');
if (focusIntention) {
focusIntention.textContent = _sprintIntention;
focusIntention.style.display = '';
}
// Start pomodoro for the sprint duration
initPomo(_sprintDuration);
startPomo();
_onSprintEnd = showSprintReview;
render();
}
/**
* Called from `pomoDone()` when the Pomodoro ring reaches zero.
* Fires the registered `_onSprintEnd` callback if one is set, then clears it
* so it cannot fire twice.
* @returns {void}
*/
function notifyPomodoroEnd() {
if (_onSprintEnd) {
wlLog.info('notifyPomodoroEnd: firing sprint-end callback');
const fn = _onSprintEnd;
_onSprintEnd = null;
fn();
} else {
wlLog.info('notifyPomodoroEnd: no callback registered');
}
}
/**
* Stops the main timer and renders the sprint review panel inside #sprintReview.
* Populates the intention label, wires outcome buttons (yes / partly / no),
* and on save: appends the sprint to `sprintLog`, tags the entry with the
* outcome, persists both, hides the panel, and triggers a full render.
* @returns {void}
*/
function showSprintReview() {
stopTimer();
const el = document.getElementById('sprintReview');
if (!el) return;
el.style.display = '';
document.getElementById('sprintReviewIntention').textContent = `"${_sprintIntention}"`;
document.getElementById('sprintReviewNote').value = '';
const outcomesEl = document.getElementById('sprintOutcomes');
outcomesEl.innerHTML = [
['yes', '✓ Yes', '#1d9e75'],
['partly', '◑ Partly', '#ef9f27'],
['no', '✗ No', '#d32f2f'],
]
.map(
([val, label, color]) =>
`<button class="add-btn sprint-outcome-btn" data-outcome="${val}"
style="flex:1;color:${color};border-color:${color}44">${label}</button>`
)
.join('');
let _outcome = null;
outcomesEl.querySelectorAll('.sprint-outcome-btn').forEach((btn) => {
btn.addEventListener('click', () => {
_outcome = btn.dataset.outcome;
outcomesEl
.querySelectorAll('.sprint-outcome-btn')
.forEach((b) => (b.style.fontWeight = b === btn ? '600' : ''));
});
});
document.getElementById('sprintReviewSave').onclick = () => {
if (!_outcome) {
wlLog.info('showSprintReview: rejected — no outcome chosen');
alert('Please choose an outcome.');
return;
}
const note = document.getElementById('sprintReviewNote').value.trim();
loadSprintLog();
sprintLog.push({
id: _sprintEntryId,
intention: _sprintIntention,
duration: _sprintDuration,
outcome: _outcome,
note,
ts: Date.now(),
});
saveSprintLog();
wlLog.info('showSprintReview: sprint reviewed', {
duration: _sprintDuration,
outcome: _outcome,
hasNote: !!note,
});
const entry = entries.find((e) => e.id === _sprintEntryId);
if (entry) {
entry._sprintOutcome = _outcome;
save();
}
el.style.display = 'none';
_sprintActive = false;
const fi = document.getElementById('sprintFocusIntention');
if (fi) fi.style.display = 'none';
render();
};
}
/**
* Registers all sprint UI event listeners: the sprint mode button, start,
* cancel, and Enter-key shortcut on the intention input.
* Called once on DOMContentLoaded.
* @returns {void}
*/
function initSprints() {
document.getElementById('sprintModeBtn')?.addEventListener('click', openSprintSetup);
document.getElementById('sprintStartBtn')?.addEventListener('click', startSprint);
document.getElementById('sprintCancel')?.addEventListener('click', () => {
const el = document.getElementById('sprintSetup');
if (el) el.style.display = 'none';
});
document.getElementById('sprintIntention')?.addEventListener('keydown', (e) => {
if (e.key === 'Enter') startSprint();
});
}