This is a submission for the Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady
What I Built
One Thing: a single-page app for the dreaded chore list. You dump the whole list once, and each day it shows you exactly ONE small next step β the smallest slice of one chore, about two minutes β and hides everything else until that step is done. Done for the day means done: the rest of the pile waits, patiently, until tomorrow. Who gets lazier: anyone whose chore list stares at them. The payoff is structural, not measured in minutes saved β one two-minute step instead of the whole pile staring at you.
Demo
Single HTML file, no build step, no server. Save the attached file and open it in any browser β everything runs locally on your device, and your list lives in that browser's local storage only.
Try it here:
https://StarNomad.github.io/one-thing.html β save that file and open it in any
browser.
Code
The complete code is the single attached HTML file: plain HTML, CSS, and JavaScript, no dependencies, no network calls. One file, readable end to end.
Source file:
https://StarNomad.github.io/one-thing.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>One Thing β Slow and Steady</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 560px; margin: 2rem auto; padding: 0 1rem; color: #222; line-height: 1.5; }
h1 { font-size: 1.8rem; margin-bottom: 0.2rem; }
.tagline { color: #666; margin-top: 0; }
#today-box { border: 2px solid #2e7d32; border-radius: 12px; padding: 1.2rem; margin: 1.5rem 0; background: #f1f8e9; }
#today-text { font-size: 1.4rem; font-weight: 600; }
.waiting { color: #666; }
button { font-size: 1rem; padding: 0.5rem 1rem; margin-right: 0.5rem; border-radius: 8px; border: 1px solid #999; background: #fff; cursor: pointer; }
button.primary { background: #2e7d32; color: #fff; border-color: #2e7d32; }
textarea { width: 100%; box-sizing: border-box; font-size: 1rem; padding: 0.5rem; border-radius: 8px; border: 1px solid #999; }
.done-trail { color: #666; font-size: 0.95rem; }
.done-trail li { margin-bottom: 0.2rem; }
.hint { color: #666; font-size: 0.95rem; }
</style>
</head>
<body>
<h1>π¦₯ One Thing</h1>
<p class="tagline">Slow and steady. One dreaded thing a day, in its smallest slice, and nothing else until it is done.</p>
<div id="today-box">
<div>Today's one thing:</div>
<div id="today-text"></div>
<p>
<button class="primary" id="done-btn">Done β that's enough for today</button>
<button id="skip-btn">Not this one β put it back in the pile</button>
</p>
<p class="waiting" id="waiting-text"></p>
</div>
<h2>Add to the pile</h2>
<p class="hint">Write the smallest first slice, about two minutes of work. Not "clean the garage" β "put one box by the door".</p>
<textarea id="new-chore" rows="2" placeholder="One dreaded thing, in its smallest sliceβ¦"></textarea>
<p><button class="primary" id="add-btn">Add it to the pile</button></p>
<h2>Done trail</h2>
<ul class="done-trail" id="done-list"></ul>
<p><button id="reset-btn">Clear everything and start over</button></p>
<p class="hint">Everything stays on this device. No account, no server, no data leaves this page. Your list lives in this browser's local storage only.</p>
<script>
(function () {
var KEY = 'onething-v1';
function todayKey() {
var d = new Date();
var m = '' + (d.getMonth() + 1);
var day = '' + d.getDate();
if (m.length === 1) m = '0' + m;
if (day.length === 1) day = '0' + day;
return d.getFullYear() + '-' + m + '-' + day;
}
function load() {
try {
var raw = localStorage.getItem(KEY);
if (raw) {
var parsed = JSON.parse(raw);
if (parsed && Array.isArray(parsed.chores)) return parsed;
}
} catch (e) { /* corrupted storage: start fresh */ }
return { chores: [], today: null };
}
function save(state) {
try { localStorage.setItem(KEY, JSON.stringify(state)); } catch (e) { /* storage full/blocked: keep going in memory */ }
}
function nextId(state) {
var max = 0;
state.chores.forEach(function (c) { if (c.id > max) max = c.id; });
return max + 1;
}
// Pick (or keep) today's chore. Pure over state + a date key: testable.
function pickToday(state, dateKey) {
if (!state.today || state.today.date !== dateKey) {
var open = state.chores.filter(function (c) { return !c.doneDate; });
state.today = open.length ? { date: dateKey, choreId: open[0].id, done: false } : { date: dateKey, choreId: null, done: false };
}
return state.today;
}
function findChore(state, id) {
for (var i = 0; i < state.chores.length; i++) {
if (state.chores[i].id === id) return state.chores[i];
}
return null;
}
function addChore(state, text) {
text = (text || '').trim();
if (!text) return null;
var chore = { id: nextId(state), text: text, doneDate: null };
state.chores.push(chore);
return chore;
}
function markDone(state, dateKey) {
var t = pickToday(state, dateKey);
if (t.choreId === null) return;
var chore = findChore(state, t.choreId);
if (chore) chore.doneDate = dateKey;
t.done = true;
}
// Skip: move today's chore to the back of the queue, pick the next open one.
function skipToday(state, dateKey) {
var t = pickToday(state, dateKey);
if (t.choreId === null) return;
var chore = findChore(state, t.choreId);
if (!chore) return;
state.chores = state.chores.filter(function (c) { return c.id !== chore.id; });
state.chores.push(chore);
var open = state.chores.filter(function (c) { return !c.doneDate; });
state.today = open.length ? { date: dateKey, choreId: open[0].id, done: false } : { date: dateKey, choreId: null, done: false };
}
function waitingCount(state) {
return state.chores.filter(function (c) { return !c.doneDate; }).length;
}
// ---- UI ----
var state = load();
var todayEl = document.getElementById('today-text');
var waitingEl = document.getElementById('waiting-text');
var doneListEl = document.getElementById('done-list');
var inputEl = document.getElementById('new-chore');
function render() {
var t = pickToday(state, todayKey());
save(state);
if (t.choreId === null) {
todayEl.textContent = 'Nothing waiting. Add something dreaded below.';
} else if (t.done) {
todayEl.textContent = 'Done for today. Enjoy the rest. π¦₯';
} else {
var chore = findChore(state, t.choreId);
todayEl.textContent = chore ? chore.text : 'Nothing waiting.';
}
var waiting = waitingCount(state);
waitingEl.textContent = waiting === 0
? 'The pile is empty.'
: waiting + ' more waiting, patiently. You do not have to look at them.';
var done = state.chores.filter(function (c) { return c.doneDate; }).slice(-10).reverse();
doneListEl.innerHTML = '';
done.forEach(function (c) {
var li = document.createElement('li');
li.textContent = c.doneDate + ' β ' + c.text;
doneListEl.appendChild(li);
});
}
document.getElementById('done-btn').addEventListener('click', function () {
markDone(state, todayKey());
render();
});
document.getElementById('skip-btn').addEventListener('click', function () {
skipToday(state, todayKey());
render();
});
document.getElementById('add-btn').addEventListener('click', function () {
var added = addChore(state, inputEl.value);
if (added) {
inputEl.value = '';
render();
}
});
document.getElementById('reset-btn').addEventListener('click', function () {
if (window.confirm('Clear the whole pile and the done trail?')) {
state = { chores: [], today: null };
render();
}
});
// Expose logic for testing (browser console / node harness).
window.OneThing = {
load: load, save: save, pickToday: pickToday, addChore: addChore,
markDone: markDone, skipToday: skipToday, waitingCount: waitingCount,
todayKey: todayKey
};
render();
})();
</script>
</body>
</html>
How I Built It
I deliberately did not bolt a model onto this. The project is built around the open web itself: one open-source HTML file, local storage, zero dependencies, nothing downloaded, nothing uploaded. The laziness reward here comes from removing machinery β no account, no cloud, no model download, no subscription β rather than adding a model. That keeps it tryable in seconds, on any device, forever.
Why Does Open Innovation Matter?
The closed version of this app is an account, a server, your chore list in someone else's database, a subscription, and a shutdown notice someday. The open version is a file you can read, copy, change, and keep: it works offline, costs nothing, and your list never leaves your device. Open is what makes "slow and steady" actually sustainable β a closed app needs you to keep paying attention to it; this one just sits there, patiently, and works.
Top comments (0)