treker-privychek-kazahteleko/script.js

322 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* Трекер привычек — демо. Данные: демо-набор по умолчанию, правки — в 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 '<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#' + (ICONS[icon] || icon || DEFAULT_ICON) + '"></use></svg>';
}
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 '<div class="day"><span class="dot ' + on + isToday + '" data-day="' + s + '"></span><span class="dow">' + dow + "</span></div>";
}).join("");
var todayDone = h.checks[today];
var toggleLabel = todayDone ? "Сегодня: выполнено" : "Отметить за сегодня";
var toggleVariant = todayDone ? "ghost" : "primary";
var toggleIcon = todayDone ? "check" : "plus";
return (
'<article class="kt-ai-card habit" data-id="' + h.id + '">' +
'<div class="habit-top">' +
'<div class="habit-ic">' + iconUse(h.icon) + "</div>" +
'<div class="habit-meta">' +
'<div class="habit-name">' + escapeHtml(h.name) + "</div>" +
'<div class="habit-sub">Последние 7 дней · серия ' + st + " " + plural(st) + "</div>" +
"</div>" +
'<div class="habit-streak"><div class="num">' + st + '</div><div class="lbl">дней подряд</div></div>' +
'<button class="ic-btn" data-act="delete" aria-label="Удалить привычку: ' + escapeHtml(h.name) + '" title="Удалить"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#trash"></use></svg></button>' +
"</div>" +
'<div class="habit-body">' +
'<div class="habit-week">' + daysHtml + "</div>" +
'<span class="spacer"></span>' +
'<button class="kt-ai-btn" data-variant="' + toggleVariant + '" data-size="sm" data-act="toggle" type="button">' +
'<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:15px;height:15px"><use href="#' + toggleIcon + '"></use></svg> ' + toggleLabel +
"</button>" +
"</div>" +
"</article>"
);
}).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 { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[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();
})();