diff --git a/README.md b/README.md index 73ff5c6..08e8f33 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ A simple habit tracker that runs entirely in your web browser. No installation, ## What it does - **Add habits** – type a habit (like "Drink water" or "Read 10 pages") and click **Add habit**. +- **Pick an emoji** – click the small **Emoji** button next to a habit (or next to the box when adding one) and choose from the list, or type/paste any emoji you like. Click it again any time to change it, or choose **No emoji** to remove it. - **Check them off** – click **Mark done today** each day you do the habit. Click it again to undo. - **See your streak** – each habit shows how many days in a row you've kept it up. If you haven't checked it off yet today, your streak from yesterday still counts until the day is over. - **30-day grid** – a row of squares shows the last 30 days (oldest on the left, today on the right). Green means done. Click any square to fix a day you forgot to mark. diff --git a/index.html b/index.html index f41dc85..03cb5af 100644 --- a/index.html +++ b/index.html @@ -144,6 +144,77 @@ margin-top: 24px; text-align: center; } + + /* Emoji for each habit */ + .emoji-btn { + flex: none; + width: 44px; + min-height: 40px; + padding: 0; + font-size: 1.4rem; + line-height: 1; + border-radius: 10px; + display: inline-flex; + align-items: center; + justify-content: center; + } + .emoji-btn.empty { + border-style: dashed; + color: var(--muted); + font-size: 0.75rem; + } + .emoji-btn:hover { border-color: var(--muted); } + .emoji-picker { + position: absolute; + z-index: 10; + width: 300px; + max-width: calc(100vw - 16px); + background: var(--card); + border: 1px solid var(--border); + border-radius: 12px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); + padding: 10px; + } + .emoji-picker .choices { + display: grid; + grid-template-columns: repeat(8, 1fr); + gap: 2px; + } + .emoji-picker .choices button { + border: none; + background: none; + font-size: 1.3rem; + line-height: 1; + padding: 5px 0; + border-radius: 6px; + } + .emoji-picker .choices button:hover, + .emoji-picker .choices button:focus { background: var(--empty); } + .emoji-picker .choices button.selected { background: var(--empty); outline: 2px solid var(--accent); } + .emoji-picker form { + display: flex; + gap: 6px; + margin-top: 10px; + } + .emoji-picker input { + flex: 1; + min-width: 0; + padding: 6px 8px; + font: inherit; + border: 1px solid var(--border); + border-radius: 8px; + } + .emoji-picker form button { padding: 6px 10px; } + .emoji-picker .remove { + display: block; + width: 100%; + margin-top: 8px; + padding: 6px 8px; + color: var(--muted); + font-size: 0.9rem; + } + .emoji-picker .remove:hover { color: var(--danger); } + @media (max-width: 480px) { .grid { gap: 2px; } .check-today { min-width: 0; flex: 1; } @@ -156,6 +227,7 @@

+
@@ -172,6 +244,14 @@ var STORAGE_KEY = "habitTracker.v1"; var DAYS_SHOWN = 30; + var EMOJI_CHOICES = [ + "💧", "📚", "🏃", "🧘", "💪", "🏋️", "🚶", "🚴", + "🏊", "🥗", "🍎", "🥤", "🍳", "😴", "🛏️", "⏰", + "🦷", "🧴", "💊", "🧹", "🧺", "🌱", "🐕", "☕", + "🚭", "📵", "💻", "🧠", "✍️", "📝", "📖", "🎸", + "🎹", "🎨", "🗣️", "📞", "🙏", "❤️", "😊", "☀️", + "🌙", "💰", "🎯", "⭐", "✅", "🔥", "🌟", "🏆" + ]; // ---------- Dates (always local time, as "YYYY-MM-DD") ---------- function dateKey(d) { @@ -202,6 +282,7 @@ return { id: String(h.id || makeId()), name: h.name, + emoji: typeof h.emoji === "string" ? cleanEmoji(h.emoji) : "", done: (h.done && typeof h.done === "object") ? h.done : {} }; }); @@ -216,6 +297,31 @@ alert("Sorry, your habits could not be saved in this browser."); } } + // Keep just the first emoji (or character) someone typed or pasted. + function cleanEmoji(text) { + text = String(text || "").trim(); + if (!text) return ""; + var first = ""; + if (window.Intl && Intl.Segmenter) { + first = new Intl.Segmenter(undefined, { granularity: "grapheme" }) + .segment(text)[Symbol.iterator]().next().value.segment; + } else { + var chars = Array.from(text); + first = chars[0]; + for (var i = 1; i < chars.length; i++) { + var c = chars[i]; + var code = c.codePointAt(0); + if (c === "\uFE0F" || (code >= 0x1F3FB && code <= 0x1F3FF) || (code >= 0xE0020 && code <= 0xE007F)) { + first += c; + } else if (c === "\u200D" && i + 1 < chars.length) { + first += c + chars[++i]; + } else { + break; + } + } + } + return first.slice(0, 32); + } function makeId() { return Date.now().toString(36) + Math.random().toString(36).slice(2, 8); } @@ -241,11 +347,113 @@ render(); } + // ---------- Emoji picker ---------- + var picker = null; // { el, anchor } + + function showEmojiOn(btn, emoji, habitName) { + btn.textContent = emoji || "Emoji"; + btn.classList.toggle("empty", !emoji); + var what = habitName ? ' for "' + habitName + '"' : " for the new habit"; + btn.title = (emoji ? "Change emoji" : "Pick an emoji") + what; + btn.setAttribute("aria-label", btn.title); + } + + function closePicker(returnFocus) { + if (!picker) return; + var anchor = picker.anchor; + picker.el.remove(); + picker = null; + anchor.setAttribute("aria-expanded", "false"); + if (returnFocus && document.body.contains(anchor)) anchor.focus(); + } + + function openPicker(anchor, current, onPick) { + if (picker && picker.anchor === anchor) { closePicker(true); return; } + closePicker(false); + + var el = document.createElement("div"); + el.className = "emoji-picker"; + el.setAttribute("role", "dialog"); + el.setAttribute("aria-label", "Choose an emoji"); + + function choose(emoji) { + closePicker(true); + onPick(emoji); + } + + var choices = document.createElement("div"); + choices.className = "choices"; + var focusTarget = null; + EMOJI_CHOICES.forEach(function (emoji) { + var b = document.createElement("button"); + b.type = "button"; + b.textContent = emoji; + b.setAttribute("aria-label", "Use " + emoji); + if (emoji === current) { b.className = "selected"; focusTarget = b; } + b.addEventListener("click", function () { choose(emoji); }); + choices.appendChild(b); + }); + el.appendChild(choices); + + var form = document.createElement("form"); + form.autocomplete = "off"; + var input = document.createElement("input"); + input.type = "text"; + input.placeholder = "Or type/paste any emoji"; + input.setAttribute("aria-label", "Type or paste your own emoji"); + if (current && EMOJI_CHOICES.indexOf(current) === -1) input.value = current; + var useBtn = document.createElement("button"); + useBtn.type = "submit"; + useBtn.textContent = "Use"; + form.appendChild(input); + form.appendChild(useBtn); + form.addEventListener("submit", function (e) { + e.preventDefault(); + var emoji = cleanEmoji(input.value); + if (!emoji) { input.focus(); return; } + choose(emoji); + }); + el.appendChild(form); + + if (current) { + var removeBtn = document.createElement("button"); + removeBtn.type = "button"; + removeBtn.className = "remove"; + removeBtn.textContent = "No emoji"; + removeBtn.addEventListener("click", function () { choose(""); }); + el.appendChild(removeBtn); + } + + document.body.appendChild(el); + picker = { el: el, anchor: anchor }; + anchor.setAttribute("aria-expanded", "true"); + + // Place it just below the button, kept inside the window. + var rect = anchor.getBoundingClientRect(); + var pageW = document.documentElement.clientWidth; + var left = rect.left; + if (left + el.offsetWidth > pageW - 8) left = pageW - 8 - el.offsetWidth; + if (left < 8) left = 8; + el.style.left = (left + window.scrollX) + "px"; + el.style.top = (rect.bottom + window.scrollY + 6) + "px"; + + (focusTarget || choices.firstChild).focus(); + } + + document.addEventListener("mousedown", function (e) { + if (picker && !picker.el.contains(e.target) && !picker.anchor.contains(e.target)) closePicker(false); + }); + document.addEventListener("keydown", function (e) { + if (picker && e.key === "Escape") closePicker(true); + }); + window.addEventListener("resize", function () { closePicker(false); }); + // ---------- Rendering ---------- var listEl = document.getElementById("habitList"); var renderedDay = dateKey(daysAgo(0)); function render() { + closePicker(false); renderedDay = dateKey(daysAgo(0)); document.getElementById("todayLabel").textContent = "Today is " + new Date().toLocaleDateString(undefined, { @@ -272,6 +480,23 @@ var top = document.createElement("div"); top.className = "habit-top"; + var emojiBtn = document.createElement("button"); + emojiBtn.type = "button"; + emojiBtn.className = "emoji-btn"; + emojiBtn.setAttribute("data-emoji-for", habit.id); + emojiBtn.setAttribute("aria-haspopup", "dialog"); + emojiBtn.setAttribute("aria-expanded", "false"); + showEmojiOn(emojiBtn, habit.emoji, habit.name); + emojiBtn.addEventListener("click", function () { + openPicker(emojiBtn, habit.emoji, function (emoji) { + habit.emoji = emoji; + save(); + render(); + var again = listEl.querySelector('[data-emoji-for="' + habit.id + '"]'); + if (again) again.focus(); + }); + }); + var name = document.createElement("div"); name.className = "habit-name"; name.textContent = habit.name; @@ -318,6 +543,7 @@ render(); }); + top.appendChild(emojiBtn); top.appendChild(name); top.appendChild(streakEl); top.appendChild(checkBtn); @@ -359,14 +585,26 @@ } // ---------- Adding habits ---------- + var newEmoji = ""; + var newEmojiBtn = document.getElementById("newEmojiBtn"); + showEmojiOn(newEmojiBtn, newEmoji); + newEmojiBtn.addEventListener("click", function () { + openPicker(newEmojiBtn, newEmoji, function (emoji) { + newEmoji = emoji; + showEmojiOn(newEmojiBtn, newEmoji); + }); + }); + document.getElementById("addForm").addEventListener("submit", function (e) { e.preventDefault(); var input = document.getElementById("habitInput"); var name = input.value.trim(); if (!name) { input.focus(); return; } - habits.push({ id: makeId(), name: name.slice(0, 80), done: {} }); + habits.push({ id: makeId(), name: name.slice(0, 80), emoji: newEmoji, done: {} }); save(); input.value = ""; + newEmoji = ""; + showEmojiOn(newEmojiBtn, newEmoji); render(); input.focus(); });