PatchHub

patchhub / habit-tracker Public

Track daily habits and streaks

habit-tracker / index.html 11129 bytes · raw
1<!DOCTYPE html>
2<html lang="en">
3<head>
4<meta charset="UTF-8">
5<meta name="viewport" content="width=device-width, initial-scale=1">
6<title>Habit Tracker</title>
7<style>
8 :root {
9 --bg: #f5f6f8;
10 --card: #ffffff;
11 --text: #1f2933;
12 --muted: #6b7785;
13 --border: #dde2e8;
14 --accent: #2f9e5b;
15 --accent-dark: #237a46;
16 --empty: #e6e9ee;
17 --danger: #c0392b;
18 }
19 * { box-sizing: border-box; }
20 body {
21 margin: 0;
22 font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
23 background: var(--bg);
24 color: var(--text);
25 line-height: 1.4;
26 }
27 main {
28 max-width: 760px;
29 margin: 0 auto;
30 padding: 24px 16px 48px;
31 }
32 h1 { margin: 0 0 4px; font-size: 1.8rem; }
33 .today-label { color: var(--muted); margin: 0 0 20px; }
34
35 form.add {
36 display: flex;
37 gap: 8px;
38 margin-bottom: 24px;
39 }
40 form.add input {
41 flex: 1;
42 padding: 10px 12px;
43 font-size: 1rem;
44 border: 1px solid var(--border);
45 border-radius: 8px;
46 background: var(--card);
47 }
48 button {
49 font: inherit;
50 cursor: pointer;
51 border-radius: 8px;
52 border: 1px solid var(--border);
53 background: var(--card);
54 padding: 8px 14px;
55 }
56 button.primary {
57 background: var(--accent);
58 border-color: var(--accent);
59 color: #fff;
60 font-weight: 600;
61 }
62 button.primary:hover { background: var(--accent-dark); }
63
64 .empty-state {
65 text-align: center;
66 color: var(--muted);
67 padding: 40px 16px;
68 border: 2px dashed var(--border);
69 border-radius: 12px;
70 }
71
72 .habit {
73 background: var(--card);
74 border: 1px solid var(--border);
75 border-radius: 12px;
76 padding: 16px;
77 margin-bottom: 14px;
78 }
79 .habit-top {
80 display: flex;
81 align-items: center;
82 gap: 12px;
83 flex-wrap: wrap;
84 }
85 .habit-name {
86 flex: 1;
87 min-width: 120px;
88 font-size: 1.1rem;
89 font-weight: 600;
90 word-break: break-word;
91 }
92 .streak {
93 color: var(--muted);
94 font-size: 0.95rem;
95 white-space: nowrap;
96 }
97 .streak strong { color: var(--text); font-size: 1.1rem; }
98 .check-today {
99 min-width: 150px;
100 }
101 .check-today.done {
102 background: var(--accent);
103 border-color: var(--accent);
104 color: #fff;
105 font-weight: 600;
106 }
107 .icon-btn {
108 background: none;
109 border: none;
110 color: var(--muted);
111 padding: 6px 8px;
112 font-size: 0.9rem;
113 }
114 .icon-btn:hover { color: var(--text); text-decoration: underline; }
115 .icon-btn.delete:hover { color: var(--danger); }
116
117 .grid {
118 display: grid;
119 grid-template-columns: repeat(30, 1fr);
120 gap: 3px;
121 margin-top: 14px;
122 }
123 .day {
124 aspect-ratio: 1 / 1;
125 border-radius: 3px;
126 background: var(--empty);
127 border: none;
128 padding: 0;
129 min-width: 0;
130 }
131 .day.done { background: var(--accent); }
132 .day.today { outline: 2px solid var(--text); outline-offset: 1px; }
133 .day:hover { filter: brightness(0.9); }
134 .grid-labels {
135 display: flex;
136 justify-content: space-between;
137 color: var(--muted);
138 font-size: 0.8rem;
139 margin-top: 4px;
140 }
141 .hint {
142 color: var(--muted);
143 font-size: 0.85rem;
144 margin-top: 24px;
145 text-align: center;
146 }
147 @media (max-width: 480px) {
148 .grid { gap: 2px; }
149 .check-today { min-width: 0; flex: 1; }
150 }
151</style>
152</head>
153<body>
154<main>
155 <h1>Habit Tracker</h1>
156 <p class="today-label" id="todayLabel"></p>
157
158 <form class="add" id="addForm" autocomplete="off">
159 <input id="habitInput" type="text" maxlength="80" placeholder="New habit, e.g. &quot;Drink water&quot;" aria-label="New habit name">
160 <button type="submit" class="primary">Add habit</button>
161 </form>
162
163 <div id="habitList"></div>
164
165 <p class="hint">Tip: click any square in the 30-day grid to mark or unmark that day.
166 Your habits are saved in this browser only.</p>
167</main>
168
169<script>
170(function () {
171 "use strict";
172
173 var STORAGE_KEY = "habitTracker.v1";
174 var DAYS_SHOWN = 30;
175
176 // ---------- Dates (always local time, as "YYYY-MM-DD") ----------
177 function dateKey(d) {
178 var m = String(d.getMonth() + 1).padStart(2, "0");
179 var day = String(d.getDate()).padStart(2, "0");
180 return d.getFullYear() + "-" + m + "-" + day;
181 }
182 function daysAgo(n) {
183 var d = new Date();
184 d.setHours(12, 0, 0, 0); // midday avoids daylight-saving edge cases
185 d.setDate(d.getDate() - n);
186 return d;
187 }
188 function prettyDate(d) {
189 return d.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" });
190 }
191
192 // ---------- Storage ----------
193 function load() {
194 try {
195 var raw = localStorage.getItem(STORAGE_KEY);
196 if (!raw) return [];
197 var data = JSON.parse(raw);
198 if (!Array.isArray(data)) return [];
199 return data.filter(function (h) {
200 return h && typeof h.name === "string";
201 }).map(function (h) {
202 return {
203 id: String(h.id || makeId()),
204 name: h.name,
205 done: (h.done && typeof h.done === "object") ? h.done : {}
206 };
207 });
208 } catch (e) {
209 return [];
210 }
211 }
212 function save() {
213 try {
214 localStorage.setItem(STORAGE_KEY, JSON.stringify(habits));
215 } catch (e) {
216 alert("Sorry, your habits could not be saved in this browser.");
217 }
218 }
219 function makeId() {
220 return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
221 }
222
223 var habits = load();
224
225 // ---------- Logic ----------
226 function currentStreak(habit) {
227 // Count consecutive completed days ending today.
228 // If today isn't checked yet, the streak from yesterday still counts.
229 var start = habit.done[dateKey(daysAgo(0))] ? 0 : 1;
230 var count = 0;
231 for (var i = start; ; i++) {
232 if (habit.done[dateKey(daysAgo(i))]) count++;
233 else break;
234 }
235 return count;
236 }
237 function toggleDay(habit, key) {
238 if (habit.done[key]) delete habit.done[key];
239 else habit.done[key] = true;
240 save();
241 render();
242 }
243
244 // ---------- Rendering ----------
245 var listEl = document.getElementById("habitList");
246 var renderedDay = dateKey(daysAgo(0));
247
248 function render() {
249 renderedDay = dateKey(daysAgo(0));
250 document.getElementById("todayLabel").textContent =
251 "Today is " + new Date().toLocaleDateString(undefined, {
252 weekday: "long", year: "numeric", month: "long", day: "numeric"
253 });
254
255 listEl.innerHTML = "";
256
257 if (habits.length === 0) {
258 var empty = document.createElement("div");
259 empty.className = "empty-state";
260 empty.textContent = "No habits yet. Add your first one above!";
261 listEl.appendChild(empty);
262 return;
263 }
264
265 var todayKey = dateKey(daysAgo(0));
266
267 habits.forEach(function (habit) {
268 var card = document.createElement("section");
269 card.className = "habit";
270
271 // Top row: name, streak, today's button, rename/delete
272 var top = document.createElement("div");
273 top.className = "habit-top";
274
275 var name = document.createElement("div");
276 name.className = "habit-name";
277 name.textContent = habit.name;
278
279 var streak = currentStreak(habit);
280 var streakEl = document.createElement("div");
281 streakEl.className = "streak";
282 streakEl.title = "Current streak";
283 var strong = document.createElement("strong");
284 strong.textContent = (streak > 0 ? "🔥 " : "") + streak;
285 streakEl.appendChild(strong);
286 streakEl.appendChild(document.createTextNode(streak === 1 ? " day in a row" : " days in a row"));
287
288 var doneToday = !!habit.done[todayKey];
289 var checkBtn = document.createElement("button");
290 checkBtn.type = "button";
291 checkBtn.className = "check-today" + (doneToday ? " done" : "");
292 checkBtn.textContent = doneToday ? "✓ Done today" : "Mark done today";
293 checkBtn.setAttribute("aria-pressed", doneToday ? "true" : "false");
294 checkBtn.addEventListener("click", function () { toggleDay(habit, todayKey); });
295
296 var renameBtn = document.createElement("button");
297 renameBtn.type = "button";
298 renameBtn.className = "icon-btn";
299 renameBtn.textContent = "Rename";
300 renameBtn.addEventListener("click", function () {
301 var newName = prompt("Rename habit:", habit.name);
302 if (newName === null) return;
303 newName = newName.trim();
304 if (!newName) return;
305 habit.name = newName.slice(0, 80);
306 save();
307 render();
308 });
309
310 var delBtn = document.createElement("button");
311 delBtn.type = "button";
312 delBtn.className = "icon-btn delete";
313 delBtn.textContent = "Delete";
314 delBtn.addEventListener("click", function () {
315 if (!confirm('Delete "' + habit.name + '" and all its history?')) return;
316 habits = habits.filter(function (h) { return h.id !== habit.id; });
317 save();
318 render();
319 });
320
321 top.appendChild(name);
322 top.appendChild(streakEl);
323 top.appendChild(checkBtn);
324 top.appendChild(renameBtn);
325 top.appendChild(delBtn);
326 card.appendChild(top);
327
328 // 30-day grid, oldest on the left, today on the right
329 var grid = document.createElement("div");
330 grid.className = "grid";
331 for (var i = DAYS_SHOWN - 1; i >= 0; i--) {
332 (function (d) {
333 var key = dateKey(d);
334 var isDone = !!habit.done[key];
335 var cell = document.createElement("button");
336 cell.type = "button";
337 cell.className = "day" + (isDone ? " done" : "") + (key === todayKey ? " today" : "");
338 var label = prettyDate(d) + (isDone ? ": done" : ": not done");
339 cell.title = label + " (click to change)";
340 cell.setAttribute("aria-label", label);
341 cell.addEventListener("click", function () { toggleDay(habit, key); });
342 grid.appendChild(cell);
343 })(daysAgo(i));
344 }
345 card.appendChild(grid);
346
347 var labels = document.createElement("div");
348 labels.className = "grid-labels";
349 var left = document.createElement("span");
350 left.textContent = prettyDate(daysAgo(DAYS_SHOWN - 1));
351 var right = document.createElement("span");
352 right.textContent = "Today";
353 labels.appendChild(left);
354 labels.appendChild(right);
355 card.appendChild(labels);
356
357 listEl.appendChild(card);
358 });
359 }
360
361 // ---------- Adding habits ----------
362 document.getElementById("addForm").addEventListener("submit", function (e) {
363 e.preventDefault();
364 var input = document.getElementById("habitInput");
365 var name = input.value.trim();
366 if (!name) { input.focus(); return; }
367 habits.push({ id: makeId(), name: name.slice(0, 80), done: {} });
368 save();
369 input.value = "";
370 render();
371 input.focus();
372 });
373
374 // Keep in sync if the page stays open past midnight or is changed in another tab
375 setInterval(function () {
376 if (dateKey(daysAgo(0)) !== renderedDay) render();
377 }, 60 * 1000);
378 document.addEventListener("visibilitychange", function () {
379 if (!document.hidden && dateKey(daysAgo(0)) !== renderedDay) render();
380 });
381 window.addEventListener("storage", function (e) {
382 if (e.key === STORAGE_KEY) { habits = load(); render(); }
383 });
384
385 render();
386})();
387</script>
388</body>
389</html>