Persist countdown inputs and show completion bell in tab title
Some checks failed
/ publish (push) Failing after 2m20s
Some checks failed
/ publish (push) Failing after 2m20s
Co-authored-by: Shelley <shelley@exe.dev>
This commit is contained in:
parent
ebfd6e4ce1
commit
a42f897248
3 changed files with 90 additions and 1 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
import { Countdown, integer, adjustTime } from './timer.mjs';
|
import { Countdown, integer, adjustTime } from './timer.mjs';
|
||||||
|
import { SETTINGS_KEY, serializeSettings, parseSettings } from './settings.mjs';
|
||||||
|
|
||||||
const timer = new Countdown();
|
const timer = new Countdown();
|
||||||
const minutes = document.querySelector('#minutes');
|
const minutes = document.querySelector('#minutes');
|
||||||
|
|
@ -8,8 +9,17 @@ const startStop = document.querySelector('#start-stop');
|
||||||
const reset = document.querySelector('#reset');
|
const reset = document.querySelector('#reset');
|
||||||
const status = document.querySelector('#status');
|
const status = document.querySelector('#status');
|
||||||
const fields = [minutes, seconds];
|
const fields = [minutes, seconds];
|
||||||
|
const title = document.title;
|
||||||
let ticker;
|
let ticker;
|
||||||
|
|
||||||
|
function saveSettings() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(SETTINGS_KEY, serializeSettings(minutes.value, seconds.value));
|
||||||
|
} catch {
|
||||||
|
// The timer still works when storage is disabled or full.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function resize(field) {
|
function resize(field) {
|
||||||
field.style.width = `${Math.max(field === seconds ? 2 : 1, field.value.length)}ch`;
|
field.style.width = `${Math.max(field === seconds ? 2 : 1, field.value.length)}ch`;
|
||||||
field.dataset.valid = field.value;
|
field.dataset.valid = field.value;
|
||||||
|
|
@ -26,6 +36,7 @@ function render(updateFields = true) {
|
||||||
resize(field);
|
resize(field);
|
||||||
});
|
});
|
||||||
counter.classList.toggle('finished', timer.finished);
|
counter.classList.toggle('finished', timer.finished);
|
||||||
|
document.title = timer.finished ? `🔔 ${title}` : title;
|
||||||
startStop.textContent = timer.state === 'running' ? 'Stop' : 'Start';
|
startStop.textContent = timer.state === 'running' ? 'Stop' : 'Start';
|
||||||
const message = timer.finished ? 'Countdown finished' :
|
const message = timer.finished ? 'Countdown finished' :
|
||||||
timer.state === 'running' ? 'Running' :
|
timer.state === 'running' ? 'Running' :
|
||||||
|
|
@ -39,6 +50,7 @@ function render(updateFields = true) {
|
||||||
|
|
||||||
function edited() {
|
function edited() {
|
||||||
timer.edit(minutes.value, seconds.value);
|
timer.edit(minutes.value, seconds.value);
|
||||||
|
saveSettings();
|
||||||
render(false);
|
render(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,4 +105,12 @@ document.addEventListener('visibilitychange', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
render();
|
try {
|
||||||
|
const saved = parseSettings(localStorage.getItem(SETTINGS_KEY));
|
||||||
|
minutes.value = saved.minutes;
|
||||||
|
seconds.value = saved.seconds;
|
||||||
|
timer.edit(minutes.value, seconds.value);
|
||||||
|
} catch {
|
||||||
|
// Storage may be unavailable; retain the default idle countdown.
|
||||||
|
}
|
||||||
|
render(false);
|
||||||
|
|
|
||||||
26
site/countdown/settings.mjs
Normal file
26
site/countdown/settings.mjs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { integer } from './timer.mjs';
|
||||||
|
|
||||||
|
export const SETTINGS_KEY = 'countdown-settings';
|
||||||
|
|
||||||
|
export function serializeSettings(minutes, seconds) {
|
||||||
|
return JSON.stringify({
|
||||||
|
minutes: String(integer(minutes)),
|
||||||
|
seconds: String(integer(seconds)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseSettings(json) {
|
||||||
|
try {
|
||||||
|
const settings = JSON.parse(json);
|
||||||
|
if (typeof settings?.minutes === 'string' && /^\d+$/.test(settings.minutes) &&
|
||||||
|
typeof settings?.seconds === 'string' && /^\d+$/.test(settings.seconds)) {
|
||||||
|
return {
|
||||||
|
minutes: String(integer(settings.minutes)),
|
||||||
|
seconds: String(integer(settings.seconds)).padStart(2, '0'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Missing or invalid storage is equivalent to a fresh countdown.
|
||||||
|
}
|
||||||
|
return { minutes: '0', seconds: '00' };
|
||||||
|
}
|
||||||
43
site/countdown/settings.test.mjs
Normal file
43
site/countdown/settings.test.mjs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { serializeSettings, parseSettings } from './settings.mjs';
|
||||||
|
|
||||||
|
test('settings store only entered minutes and seconds, not countdown state', () => {
|
||||||
|
assert.deepEqual(JSON.parse(serializeSettings('2', '90')), {
|
||||||
|
minutes: '2', seconds: '90',
|
||||||
|
});
|
||||||
|
assert.deepEqual(parseSettings(serializeSettings('2', '90')), {
|
||||||
|
minutes: '2', seconds: '90',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('restored seconds have at least two digits and leading zeros are canonicalized', () => {
|
||||||
|
assert.deepEqual(parseSettings(serializeSettings('001', '005')), {
|
||||||
|
minutes: '1', seconds: '05',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('empty input is stored as zero', () => {
|
||||||
|
assert.deepEqual(parseSettings(serializeSettings('', '')), {
|
||||||
|
minutes: '0', seconds: '00',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('large entered integers round trip without losing precision', () => {
|
||||||
|
const minutes = '123456789012345678901234567890';
|
||||||
|
const seconds = '987654321098765432109876543210';
|
||||||
|
assert.deepEqual(parseSettings(serializeSettings(minutes, seconds)), { minutes, seconds });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('missing, malformed, or invalid settings restore the default countdown', () => {
|
||||||
|
for (const json of [
|
||||||
|
null, '', 'invalid json', 'null', '[]', '{}', '42',
|
||||||
|
'{"minutes": 1, "seconds": 2}',
|
||||||
|
'{"minutes": "-1", "seconds": "00"}',
|
||||||
|
'{"minutes": "0", "seconds": "1.5"}',
|
||||||
|
'{"minutes": "0", "seconds": ""}',
|
||||||
|
'{"minutes": "0", "seconds": "1e2"}',
|
||||||
|
]) {
|
||||||
|
assert.deepEqual(parseSettings(json), { minutes: '0', seconds: '00' });
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue