/* Трекер привычек — демо. Данные: демо-набор по умолчанию, правки — в localStorage. */ (function () { "use strict"; var STORAGE_KEY = "kt_habit_tracker:v1"; var DAY_COUNT = 7; // показываем последние 7 дней var WEEKDAYS = ["вс", "пн", "вт", "ср", "чт", "пт", "сб"]; // Словарь иконок из спрайта ДС. var ICONS = { book: "book", dumbbell: "tool", droplet: "layers", moon: "moon", lightbulb: "lightbulb", check: "check", checklist: "checklist", sparkles: "sparkles", star: "trophy", trophy: "trophy", "check-circle": "check-circle", run: "rocket", rocket: "rocket", walk: "arrowRight" }; var ICON_BY_NAME_KEY = [ ["чит", "book"], ["чт", "book"], ["читан", "book"], ["спорт", "tool"], ["трени", "tool"], ["бег", "rocket"], ["зал", "tool"], ["вода", "layers"], ["пить", "layers"], ["hydration", "layers"], ["сон", "moon"], ["спать", "moon"], ["sleep", "moon"], ["медит", "sparkles"], ["relax", "sparkles"], ["идея", "lightbulb"], ["мысль", "lightbulb"], ["звезд", "trophy"], ["цель", "trophy"], ["trophy", "trophy"], ["heart", "check-circle"], ["люб", "check-circle"] ]; var DEFAULT_ICON = "checklist"; var DEMO = [ { id: "read", name: "Чтение", icon: "book" }, { id: "sport", name: "Спорт", icon: "tool" }, { id: "water", name: "Вода", icon: "layers" }, { id: "sleep", name: "Ранний подъём", icon: "moon" }, { id: "ideas", name: "Идеи на день", icon: "lightbulb"} ]; // Детерминированные демо-статусы за последние 7 дней (0 = не сделано, 1 = сделано). var DEMO_CHECKS = { read: [1, 1, 1, 1, 1, 1, 0], sport: [1, 0, 1, 1, 0, 1, 0], water: [1, 1, 1, 0, 1, 1, 1], sleep: [1, 1, 0, 1, 1, 1, 1], ideas: [0, 1, 1, 1, 1, 0, 1] }; // ---- Утилиты дат (локальные, без часовых поясов) ---- function toDateStr(d) { var y = d.getFullYear(); var m = String(d.getMonth() + 1).padStart(2, "0"); var day = String(d.getDate()).padStart(2, "0"); return y + "-" + m + "-" + day; } function todayStr() { return toDateStr(new Date()); } function lastDays(n) { var out = []; var now = new Date(); for (var i = n - 1; i >= 0; i--) { var d = new Date(now.getFullYear(), now.getMonth(), now.getDate() - i); out.push(toDateStr(d)); } return out; } // ---- Состояние ---- function blankChecks() { var out = {}; lastDays(DAY_COUNT).forEach(function (s) { out[s] = 0; }); return out; } function makeDemo() { var days = lastDays(DAY_COUNT); return DEMO.map(function (p) { var checks = {}; (DEMO_CHECKS[p.id] || [0,0,0,0,0,0,0]).forEach(function (v, idx) { checks[days[idx]] = v; }); return { id: p.id, name: p.name, icon: p.icon, checks: checks, addedAt: days[0] }; }); } function load() { try { var raw = localStorage.getItem(STORAGE_KEY); if (raw) { var p = JSON.parse(raw); if (p && typeof p === "object" && Array.isArray(p.habits) && p.habits.length) return p; } } catch (e) { /* повреждённое хранилище — начнём с демо */ } var demo = { habits: makeDemo(), demo: true }; save(demo); return demo; } function save(state) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (e) {} } var state = load(); // ---- Расчёты ---- // Серия дней подряд: считаем от самого недавнего дня до первого пропуска. function visibleStreak(habit, days7) { var reverse = days7.slice().reverse(); // от старого к новому var streak = 0; for (var i = 0; i < reverse.length; i++) { if (habit.checks[reverse[i]]) streak++; else break; } return streak; } // ---- DOM ---- var $ = function (id) { return document.getElementById(id); }; var habitsEl = $("habits"); var emptyEl = $("emptyState"); var kpiStrip = $("kpiStrip"); var demoReset = $("demoReset"); var toastStack = $("toastStack"); function fmtDate(str) { var p = str.split("-"); return p[2] + "." + p[1]; } function iconUse(icon) { return ''; } function render() { var days7 = lastDays(DAY_COUNT); var today = todayStr(); // подпись даты в шапке var now = new Date(); $("todayLabel").textContent = WEEKDAYS[now.getDay()] + ", " + fmtDate(today) + "." + String(now.getFullYear()); var habits = state.habits; if (!habits.length) { habitsEl.hidden = true; kpiStrip.style.display = "none"; emptyEl.hidden = false; demoReset.hidden = true; return; } emptyEl.hidden = true; habitsEl.hidden = false; kpiStrip.style.display = ""; // KPI — тихая метрик-полоса var doneToday = habits.filter(function (h) { return h.checks[today]; }).length; var best = 0, bestName = ""; habits.forEach(function (h) { var s = visibleStreak(h, days7); if (s > best) { best = s; bestName = h.name; } }); $("kpiHabits").textContent = String(habits.length); $("kpiDone").textContent = doneToday > 0 ? (doneToday + " / " + habits.length) : "—"; document.querySelector("#kpiDoneWrap .label").textContent = "сегодня выполнено"; $("kpiBest").textContent = best > 0 ? (best + " дн.") : "—"; var bestNameEl = $("kpiBestName"); bestNameEl.textContent = best > 0 ? bestName : ""; bestNameEl.style.display = best > 0 ? "" : "none"; // Список habitsEl.innerHTML = habits.map(function (h) { var st = visibleStreak(h, days7); var daysHtml = days7.map(function (s) { var on = h.checks[s] ? "on" : ""; var isToday = s === today ? " today" : ""; var dte = new Date(s); var dow = WEEKDAYS[dte.getDay()]; return '
' + dow + "
"; }).join(""); var todayDone = h.checks[today]; var toggleLabel = todayDone ? "Сегодня: выполнено" : "Отметить за сегодня"; var toggleVariant = todayDone ? "ghost" : "primary"; var toggleIcon = todayDone ? "check" : "plus"; return ( '
' + '
' + '
' + iconUse(h.icon) + "
" + '
' + '
' + escapeHtml(h.name) + "
" + '
Последние 7 дней · серия ' + st + " " + plural(st) + "
" + "
" + '
' + st + '
дней подряд
' + '' + "
" + '
' + '
' + daysHtml + "
" + '' + '" + "
" + "
" ); }).join(""); demoReset.hidden = state.demo !== true; } function plural(n) { var m10 = n % 10, m100 = n % 100; if (m10 === 1 && m100 !== 11) return "день"; if (m10 >= 2 && m10 <= 4 && (m100 < 12 || m100 > 14)) return "дня"; return "дней"; } function escapeHtml(s) { return String(s).replace(/[&<>"']/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]; }); } function habitById(id) { for (var i = 0; i < state.habits.length; i++) if (state.habits[i].id === id) return state.habits[i]; return null; } // ---- Действия ---- function toggleToday(id) { var h = habitById(id); if (!h) return; var t = todayStr(); h.checks[t] = h.checks[t] ? 0 : 1; state.demo = false; // правка — это уже данные приложения, не демо save(state); render(); toast(h.checks[t] ? "Отметили за сегодня" : "Сняли отметку за сегодня", "ok"); } function delHabit(id) { var h = habitById(id); if (!h) return; if (!confirm("Удалить привычку «" + h.name + "»?")) return; state.habits = state.habits.filter(function (x) { return x.id !== id; }); if (state.habits.length === 0) state.demo = false; save(state); render(); toast("Привычка удалена", "ok"); } function resetDemo() { if (!confirm("Сбросить все правки и вернуть демо-данные?")) return; state = { habits: makeDemo(), demo: true }; save(state); render(); toast("Возвращены демо-данные", "ok"); } // ---- Модал добавления ---- var modal = $("addModal"); var nameIn = $("addName"); var errEl = $("addError"); function openModal() { nameIn.value = ""; errEl.hidden = true; modal.hidden = false; setTimeout(function () { nameIn.focus(); }, 30); } function closeModal() { modal.hidden = true; } function detectIcon(name) { var low = name.toLowerCase(); for (var i = 0; i < ICON_BY_NAME_KEY.length; i++) { if (low.indexOf(ICON_BY_NAME_KEY[i][0]) !== -1) return ICON_BY_NAME_KEY[i][1]; } return DEFAULT_ICON; } function saveNew() { var name = nameIn.value.trim(); if (!name) { errEl.hidden = false; nameIn.setAttribute("data-invalid", "true"); return; } errEl.hidden = true; nameIn.removeAttribute("data-invalid"); var id = "h" + Date.now().toString(36); var icon = detectIcon(name); state.habits.unshift({ id: id, name: name, icon: icon, checks: blankChecks(), addedAt: todayStr() }); state.demo = false; save(state); closeModal(); render(); toast("Привычка «" + name + "» добавлена", "ok"); } // ---- Тост ---- function toast(msg, tone) { var t = document.createElement("div"); t.className = "kt-ai-toast"; if (tone) t.setAttribute("data-tone", tone); t.textContent = msg; toastStack.appendChild(t); setTimeout(function () { t.style.opacity = "0"; t.style.transition = "opacity .3s"; setTimeout(function () { t.remove(); }, 300); }, 2200); } // ---- События ---- $("addBtn").addEventListener("click", openModal); $("emptyAddBtn").addEventListener("click", openModal); $("addCancel").addEventListener("click", closeModal); $("addSave").addEventListener("click", saveNew); $("resetBtn").addEventListener("click", resetDemo); nameIn.addEventListener("keydown", function (e) { if (e.key === "Enter") saveNew(); }); modal.addEventListener("click", function (e) { if (e.target === modal) closeModal(); }); document.addEventListener("keydown", function (e) { if (e.key === "Escape" && !modal.hidden) closeModal(); }); habitsEl.addEventListener("click", function (e) { var card = e.target.closest(".habit"); if (!card) return; var id = card.getAttribute("data-id"); var act = e.target.closest("[data-act]"); if (!act) return; var a = act.getAttribute("data-act"); if (a === "toggle") toggleToday(id); else if (a === "delete") delHabit(id); }); render(); })();