264 lines
12 KiB
JavaScript
264 lines
12 KiB
JavaScript
/* myadhd.dev — client-side tools. No accounts, no network; everything in localStorage. */
|
|
(function () {
|
|
'use strict';
|
|
var $ = function (s, r) { return (r || document).querySelector(s); };
|
|
var $$ = function (s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); };
|
|
function store(k, v) {
|
|
if (v === undefined) { try { return JSON.parse(localStorage.getItem('myadhd.' + k)); } catch (e) { return null; } }
|
|
localStorage.setItem('myadhd.' + k, JSON.stringify(v));
|
|
}
|
|
function today() { return new Date().toISOString().slice(0, 10); }
|
|
|
|
/* ---------- tabs ---------- */
|
|
function showTab(name) {
|
|
$$('.tool').forEach(function (t) { t.hidden = (t.id !== name); });
|
|
$$('.tab').forEach(function (t) { t.classList.toggle('on', t.dataset.tab === name); });
|
|
if (!location.hash.slice(1)) {} // keep url in sync without loops
|
|
try { history.replaceState(null, '', '#' + name); } catch (e) {}
|
|
}
|
|
$$('.tab').forEach(function (t) {
|
|
t.addEventListener('click', function (e) { e.preventDefault(); showTab(t.dataset.tab); });
|
|
});
|
|
var initial = location.hash.slice(1);
|
|
showTab(['breaker', 'timer', 'dopamine', 'now'].indexOf(initial) !== -1 ? initial : 'breaker');
|
|
|
|
/* ---------- task breaker ---------- */
|
|
var breakerKey = 'breaker';
|
|
function breakerTemplates(task) {
|
|
return [
|
|
{ t: 'Write down what “done” looks like for: ' + task + '.', mins: 2, done: false },
|
|
{ t: 'Gather everything you need for “' + task + '” in one spot.', mins: 5, done: false },
|
|
{ t: 'Do the ugliest possible first version of “' + task + '”. No polish allowed.', mins: 5, done: false },
|
|
{ t: 'Set a 10-minute timer and start “' + task + '”. Starting is the whole job.', mins: 10, done: false },
|
|
{ t: 'Momentum check: keep going, or park it with a note for next time.', mins: 2, done: false },
|
|
{ t: 'Close the loop: tidy up and write down the very next step.', mins: 2, done: false }
|
|
];
|
|
}
|
|
function breakerRender() {
|
|
var b = store(breakerKey);
|
|
$('#breaker-out').hidden = !b;
|
|
if (!b) return;
|
|
var ul = $('#breaker-steps');
|
|
ul.innerHTML = '';
|
|
b.steps.forEach(function (s, i) {
|
|
var li = document.createElement('li');
|
|
li.className = s.done ? 'done' : '';
|
|
var tick = document.createElement('button');
|
|
tick.className = 'tick'; tick.type = 'button';
|
|
tick.setAttribute('aria-label', 'toggle done');
|
|
tick.addEventListener('click', function () {
|
|
s.done = !s.done; store(breakerKey, b); breakerRender();
|
|
});
|
|
var txt = document.createElement('span');
|
|
txt.className = 'txt'; txt.contentEditable = 'true'; txt.textContent = s.t;
|
|
txt.addEventListener('blur', function () { s.t = txt.textContent.trim(); store(breakerKey, b); });
|
|
var mins = document.createElement('span');
|
|
mins.className = 'mins'; mins.textContent = s.mins ? s.mins + ' min' : '';
|
|
li.appendChild(tick); li.appendChild(txt); li.appendChild(mins);
|
|
ul.appendChild(li);
|
|
});
|
|
var done = b.steps.filter(function (s) { return s.done; }).length;
|
|
$('#breaker-progress').style.width = b.steps.length ? (100 * done / b.steps.length) + '%' : '0';
|
|
$('#breaker-count').textContent = done + ' of ' + b.steps.length + ' steps done' +
|
|
(done === b.steps.length && b.steps.length ? ' — look at you. 🎉' : '');
|
|
}
|
|
$('#breaker-form').addEventListener('submit', function (e) {
|
|
e.preventDefault();
|
|
var task = $('#breaker-input').value.trim();
|
|
if (!task) return;
|
|
store(breakerKey, { task: task, steps: breakerTemplates(task) });
|
|
breakerRender();
|
|
});
|
|
$('#breaker-add').addEventListener('click', function () {
|
|
var b = store(breakerKey); if (!b) return;
|
|
b.steps.push({ t: 'Next step…', mins: 0, done: false });
|
|
store(breakerKey, b); breakerRender();
|
|
});
|
|
$('#breaker-reset').addEventListener('click', function () {
|
|
localStorage.removeItem('myadhd.' + breakerKey);
|
|
$('#breaker-input').value = '';
|
|
breakerRender();
|
|
});
|
|
breakerRender();
|
|
|
|
/* ---------- focus timer ---------- */
|
|
var CIRC = 628.3;
|
|
var timer = { total: 25 * 60, left: 25 * 60, running: false, phase: 'focus', iv: null };
|
|
function timerPaint() {
|
|
var m = Math.floor(timer.left / 60), s = timer.left % 60;
|
|
$('#timer-time').textContent = (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s;
|
|
$('#ring-fg').style.strokeDashoffset = CIRC * (1 - timer.left / timer.total);
|
|
var ph = $('#timer-phase');
|
|
ph.textContent = timer.running ? (timer.phase === 'focus' ? 'focus — you got this' : 'break — actually rest') : ph.textContent;
|
|
ph.className = 'timer-phase' + (timer.phase === 'break' ? ' break' : '');
|
|
}
|
|
function beep() {
|
|
try {
|
|
var ctx = new (window.AudioContext || window.webkitAudioContext)();
|
|
[[660, 0], [880, 0.18]].forEach(function (n) {
|
|
var o = ctx.createOscillator(), g = ctx.createGain();
|
|
o.frequency.value = n[0]; o.type = 'sine';
|
|
g.gain.setValueAtTime(0.001, ctx.currentTime + n[1]);
|
|
g.gain.exponentialRampToValueAtTime(0.12, ctx.currentTime + n[1] + 0.02);
|
|
g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + n[1] + 0.4);
|
|
o.connect(g); g.connect(ctx.destination);
|
|
o.start(ctx.currentTime + n[1]); o.stop(ctx.currentTime + n[1] + 0.45);
|
|
});
|
|
} catch (e) {}
|
|
}
|
|
function timerStop() { clearInterval(timer.iv); timer.iv = null; timer.running = false; }
|
|
$('#timer-start').addEventListener('click', function () {
|
|
if (timer.running) { timerStop(); $('#timer-start').textContent = 'resume'; timerPaint(); return; }
|
|
timer.running = true;
|
|
$('#timer-start').textContent = 'pause';
|
|
timer.iv = setInterval(function () {
|
|
timer.left--;
|
|
if (timer.left <= 0) {
|
|
timerStop(); beep();
|
|
if (timer.phase === 'focus') {
|
|
store('sessions', (store('sessions') || 0) + 1);
|
|
timer.phase = 'break';
|
|
$('#timer-note').textContent = 'Focus round done (' + store('sessions') + ' total). Stand up. Drink water. Actually take the break.';
|
|
} else {
|
|
timer.phase = 'focus';
|
|
$('#timer-note').textContent = 'Break over. Pick a length and go again — or stop here, that is allowed too.';
|
|
}
|
|
timer.total = timer.left = (timer.phase === 'break' ? 5 : 25) * 60;
|
|
$('#timer-start').textContent = 'start';
|
|
}
|
|
timerPaint();
|
|
}, 1000);
|
|
timerPaint();
|
|
});
|
|
$('#timer-reset').addEventListener('click', function () {
|
|
timerStop(); timer.left = timer.total; $('#timer-start').textContent = 'start';
|
|
timerPaint();
|
|
});
|
|
$$('.preset').forEach(function (p) {
|
|
p.addEventListener('click', function () {
|
|
timerStop();
|
|
timer.total = timer.left = (+p.dataset.min) * 60;
|
|
timer.phase = p.classList.contains('break') ? 'break' : 'focus';
|
|
$('#timer-start').textContent = 'start';
|
|
timerPaint();
|
|
});
|
|
});
|
|
timerPaint();
|
|
|
|
/* ---------- dopamine menu ---------- */
|
|
var MENU = {
|
|
5: [
|
|
'Put on one song and tidy one single surface.',
|
|
'Drink a big glass of water like it is a magic potion.',
|
|
'Step outside for two minutes and name five sounds you hear.',
|
|
'Stretch your arms and neck slowly — like a cat waking up.',
|
|
'Text someone one true compliment. No context, no apology.',
|
|
'Draw the nearest object as badly as you possibly can.',
|
|
'Open the window and take ten slow breaths.',
|
|
'Rearrange exactly three things on your desk.',
|
|
'Do ten squats or one ridiculous dance move.',
|
|
'Write down three things you can see right now that you like.'
|
|
],
|
|
15: [
|
|
'Walk once around the block — phone stays home or in pocket.',
|
|
'Build the fanciest snack possible from what you already have.',
|
|
'Write a short thank-you note to your future self.',
|
|
'Doodle your current mood as a weather report.',
|
|
'Do a ten-minute follow-along stretch video.',
|
|
'Clean one drawer. One. Set a timer so it cannot grow.',
|
|
'Read five pages of any book you own.',
|
|
'Water (or rescue) one plant.',
|
|
'Have a shower, but treat it like a whole concert.',
|
|
'Learn three words in a language you do not speak.'
|
|
],
|
|
30: [
|
|
'Cook something new with a video recipe playing.',
|
|
'Go for a 25-minute walk with one podcast episode.',
|
|
'Call someone you like. With your voice. Wild, we know.',
|
|
'Build something with zero rules: LEGO, blanket fort, collage.',
|
|
'Brain-dump everything in your head onto paper for ten minutes.',
|
|
'Deep-clean the one object that quietly bothers you most.',
|
|
'Follow a 30-minute workout video at whatever intensity you like.',
|
|
'Walk or bike to a spot nearby you have never actually been.',
|
|
'Pack a small comfort bag for your worst brain days.',
|
|
'Ten minutes journaling, ten minutes stretching, ten minutes staring.'
|
|
],
|
|
60: [
|
|
'Long walk or bike ride somewhere with trees.',
|
|
'One-hour hobby sprint — make something terrible and be proud.',
|
|
'Bake something from scratch. Eat evidence.',
|
|
'One-room tidy: music loud, timer rules, no mercy.',
|
|
'Wandering trip (thrift store, library) with a $10 budget.',
|
|
'Digital reset: close twenty tabs, clear the inbox as far as you can.',
|
|
'Follow a full workout or yoga class at home.',
|
|
'Meal-prep one lunch so tomorrow-you says thanks.',
|
|
'Take a notebook to a café or library and people-watch or sketch.',
|
|
'Watch a documentary about something absurd and become an expert.'
|
|
]
|
|
};
|
|
var lastPick = null;
|
|
function menuPick(mins) {
|
|
var pool = MENU[mins].filter(function (x) { return x !== lastPick; });
|
|
var item = pool[Math.floor(Math.random() * pool.length)];
|
|
lastPick = item;
|
|
$('#menu-text').textContent = item;
|
|
$('#menu-item').hidden = false;
|
|
$('#menu-actions').hidden = false;
|
|
}
|
|
$$('.menu-pick').forEach(function (b) {
|
|
b.addEventListener('click', function () { menuPick(+b.dataset.min); });
|
|
});
|
|
$('#menu-reroll').addEventListener('click', function () {
|
|
var mins = $$('.menu-pick').filter(function (b) { return b.classList.contains('on'); })[0];
|
|
menuPick(mins ? +mins.dataset.min : 5);
|
|
});
|
|
function paintStreak() {
|
|
var s = store('streak');
|
|
$('#menu-streak').innerHTML = s && s.streak ?
|
|
'Streak: <span class="streak-num">' + s.streak + ' day' + (s.streak > 1 ? 's' : '') + '</span> of healthy dopamine. ' +
|
|
(s.last === today() ? 'Today is already logged.' : '') : '';
|
|
}
|
|
$('#menu-done').addEventListener('click', function () {
|
|
var s = store('streak') || { last: '', streak: 0 };
|
|
if (s.last !== today()) {
|
|
var y = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
|
|
s.streak = (s.last === y) ? s.streak + 1 : 1;
|
|
s.last = today();
|
|
store('streak', s);
|
|
}
|
|
$('#menu-item').hidden = true;
|
|
$('#menu-actions').hidden = true;
|
|
paintStreak();
|
|
});
|
|
paintStreak();
|
|
|
|
/* ---------- one thing ---------- */
|
|
var nowSaved = store('now');
|
|
if (nowSaved && nowSaved.text) {
|
|
$('#now-text').textContent = nowSaved.text;
|
|
$('#now-form').hidden = true;
|
|
$('#now-view').hidden = false;
|
|
}
|
|
$('#now-form').addEventListener('submit', function (e) {
|
|
e.preventDefault();
|
|
var t = $('#now-input').value.trim();
|
|
if (!t) return;
|
|
store('now', { text: t });
|
|
$('#now-text').textContent = t;
|
|
$('#now-form').hidden = true;
|
|
$('#now-view').hidden = false;
|
|
});
|
|
$('#now-edit').addEventListener('click', function () {
|
|
$('#now-view').hidden = true;
|
|
$('#now-form').hidden = false;
|
|
$('#now-input').value = '';
|
|
$('#now-input').focus();
|
|
});
|
|
$('#now-done').addEventListener('click', function () {
|
|
localStorage.removeItem('myadhd.now');
|
|
$('#now-view').hidden = true;
|
|
$('#now-form').hidden = false;
|
|
$('#now-input').value = '';
|
|
$('#now-input').focus();
|
|
});
|
|
})();
|