postroit-mne-rezhim-dnya-esl/script.js

488 lines
20 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.

(function () {
"use strict";
var STORAGE_KEY = "rezim-day-1";
var WEEKS = 4;
var WD = ["вс", "пн", "вт", "ср", "чт", "пт", "сб"];
var DEFAULTS = {
wake: "07:00",
bedtime: "22:30",
schedule: [
{ time: "07:00", title: "Подъём и стакан воды" },
{ time: "07:10", title: "Зарядка 10–15 минут" },
{ time: "07:30", title: "Завтрак" },
{ time: "08:00", title: "Дорога и начало дела" },
{ time: "13:00", title: "Обед" },
{ time: "13:30", title: "Короткий отдых 15 минут" },
{ time: "14:00", title: "Работа / учёба" },
{ time: "18:00", title: "Конец рабочего дня" },
{ time: "19:00", title: "Ужин" },
{ time: "20:00", title: "Спорт / прогулка" },
{ time: "21:30", title: "Без экранов, вечер без спешки" },
{ time: "22:30", title: "Сон" }
],
habits: [
{ id: "h1", name: "Спорт", goal: 3, note: "Хотя бы 30 минут движения", done: [] },
{ id: "h2", name: "30 минут чтения", goal: 5, note: "Книга перед сном вместо ленты", done: [] },
{ id: "h3", name: "Дневник 3 строк", goal: 7, note: "Что получилось, что улучшить", done: [] },
{ id: "h4", name: "Готовить дома", goal: 5, note: "Завтрак или ужин", done: [] }
]
};
var state = null;
var serverLogReady = false;
// ---------- даты ----------
function iso(d) {
var p = function (n) { return String(n).padStart(2, "0"); };
return d.getFullYear() + "-" + p(d.getMonth() + 1) + "-" + p(d.getDate());
}
function daysAgo(n) { var d = new Date(); d.setDate(d.getDate() - n); return d; }
function lastNDays() {
var a = [];
for (var i = WEEKS * 7 - 1; i >= 0; i--) a.push(iso(daysAgo(i)));
return a;
}
function fmtDate(d, withYear) {
var months = ["января", "февраля", "марта", "апреля", "мая", "июня", "июля", "августа", "сентября", "октября", "ноября", "декабря"];
var s = d.getDate() + " " + months[d.getMonth()];
if (withYear) s += " " + d.getFullYear();
return s;
}
function todayStr() { return iso(new Date()); }
// ---------- данные ----------
function freshLog() {
var log = {};
lastNDays().forEach(function (d) { log[d] = { done: [], habits: {} }; });
return log;
}
function loadLocal() {
var data = null;
try { data = JSON.parse(localStorage.getItem(STORAGE_KEY)); } catch (_) {}
return data;
}
function saveLocal() {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (_) {}
}
function todayEntry() {
var t = todayStr();
if (!state.log[t]) { state.log[t] = { done: [], habits: {} }; }
return state.log[t];
}
function applyLocal() {
var local = loadLocal();
if (local && local.log && Object.keys(local.log).length) {
state.log = Object.assign(freshLog(), local.log);
} else {
state.log = seedLog();
}
saveLocal();
}
function seedLog() {
var log = freshLog();
lastNDays().forEach(function (d, i) {
var e = log[d];
if (d === todayStr()) return;
if (i % 7 === 5) { e.done = []; e.sleep = 8.5; return; }
e.done = state.schedule.slice(0, i % 6 === 0 ? state.schedule.length - 2 : state.schedule.length - 1).map(function (it) { return it.time; });
e.sleep = Math.round((7 + (i % 3) * 0.5) * 10) / 10;
});
return log;
}
function seedDemoDays() {
if (loadLocal()) return;
state.log = seedLog();
saveLocal();
}
// ---------- сервер ----------
function pushToServer() {
var payload = {
wake: state.wake,
bedtime: state.bedtime,
schedule: state.schedule,
habits: state.habits,
log: state.log
};
fetch("api/state", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
}).then(pushOk).catch(function () { pushFail(); });
}
var pushTimer = null;
function pushOk() {
if (pushTimer) { clearTimeout(pushTimer); pushTimer = null; }
if ("serviceWorker" in navigator && !serverLogReady) {
pushTimer = setTimeout(function () {
pushTimer = null;
if ("serviceWorker" in navigator) pushFail();
}, 600);
} else {
serverLogReady = true;
}
}
function pushFail() {
toast("Не удалось сохранить на сервере — изменения пока только на этом устройстве", "warn");
}
// ---------- утилиты ----------
function toast(msg, tone) {
var stack = document.getElementById("toasts");
var el = document.createElement("div");
el.className = "kt-ai-toast";
if (tone) el.setAttribute("data-tone", tone);
el.textContent = msg;
stack.appendChild(el);
setTimeout(function () { el.remove(); }, 4500);
}
function sleepHours() {
function parts(v) { var ab = String(v || "").split(":"); return [parseInt(ab[0], 10) || 0, parseInt(ab[1], 10) || 0]; }
var b = parts(state.bedtime), w = parts(state.wake);
var bm = b[0] * 60 + b[1], wm = w[0] * 60 + w[1];
if (bm >= wm) return 24 - (bm - wm) / 60;
return (1440 - bm + wm) / 60;
}
// ---------- KPI и статусы ----------
function currentDayProgress(e) {
var total = state.schedule.length;
var done = (e && e.done ? e.done : []).filter(function (tm) {
return state.schedule.some(function (it) { return it.time === tm; });
}).length;
return { total: total, done: done, pct: total ? Math.round((done / total) * 100) : 0 };
}
function weekAvgProgress() {
var days = lastNDays().slice(-7);
var sum = 0;
days.forEach(function (d) {
var e = state.log[d];
if (e) sum += currentDayProgress(e).pct;
});
return Math.round(sum / 7);
}
function currentDayStreak() {
var days = lastNDays();
var streak = 0;
for (var i = days.length - 1; i >= 0; i--) {
var e = state.log[days[i]];
if (e && e.done.length >= state.schedule.length && e.done.length > 0) streak++;
else if (days[i] !== todayStr()) break;
}
return streak === 0 && (function () { var e = state.log[todayStr()]; return e && e.done.length > 0; })() ? 1 : streak;
}
// ---------- рендер ----------
function renderTopbar() {
var now = new Date();
document.getElementById("dateChip").textContent = WD[now.getDay()] + " · " + fmtDate(now);
}
function renderKpi() {
var t = todayEntry();
var cur = currentDayProgress(t);
document.getElementById("kpiDone").textContent = cur.done + " из " + cur.total;
var weekAvg = weekAvgProgress();
document.getElementById("kpiWeek").textContent = weekAvg + " %";
document.querySelector("#kWeek .pfill").style.width = weekAvg + "%";
document.getElementById("kpiStreak").textContent = currentDayStreak();
var st = sleepHours().toFixed(1).replace('.', ',');
document.getElementById("kpiSleep").textContent = st + " ч";
}
function renderDayList() {
var t = todayEntry();
var rows = state.schedule.map(function (it, idx) {
var ck = t.done.indexOf(it.time) !== -1;
return '<div class="kt-ai-row item-row" data-idx="' + idx + '">' +
'<label class="kt-ai-checkbox item-check"><input type="checkbox" ' + (ck ? "checked" : "") + ' aria-label="' + it.title + '"></label>' +
'<span><span class="time">' + it.time + '</span> <span class="item-name">' + it.title + '</span></span>' +
'<span class="end">' + (ck ? '<span class="kt-ai-status-pill" data-status="ok">готово</span>' : '') + '</span>' +
'</div>';
}).join("");
var total = state.schedule.length;
var done = t.done.filter(function (tm) { return state.schedule.some(function (it) { return it.time === tm; }); }).length;
document.getElementById("dayList").innerHTML =
'<div class="kt-ai-table-wrap">' +
'<table class="kt-ai-table">' +
'<thead><tr><th style="width:36px"></th><th>Час</th><th>Пункт</th><th></th></tr></thead>' +
'<tbody>' + rows + '</tbody>' +
'</table>' +
'</div>';
document.getElementById("progressBar").style.width = (total ? (done / total) * 100 : 0) + "%";
document.getElementById("progressText").textContent = done + " из " + total;
}
function renderWeekGrid() {
var days = lastNDays().slice(-7);
var head = days.map(function (d) {
var dt = new Date(d + "T12:00:00");
var isToday = d === todayStr();
var p = currentDayProgress(state.log[d]);
var on = d === todayStr() || p.pct > 0;
return '<span data-dkeyday="' + d + '" style="justify-self:center;display:flex;flex-direction:column;align-items:center;gap:4px;cursor:pointer">' +
'<span style="font-size:var(--kt-ai-text-2xs);color:' + (isToday ? "var(--kt-ai-fg)" : "var(--kt-ai-fg-muted)") + ';font-weight:' + (isToday ? "600" : "400") + '">' + WD[dt.getDay()] + "</span>" +
'<i style="height:14px;width:14px;border-radius:50%;background:' + (on ? "var(--kt-ai-primary)" : "var(--kt-ai-bg-hover)") + (isToday ? ";outline:2px solid var(--kt-ai-primary);outline-offset:2px" : "") + '"></i>' +
"</span>";
}).join("");
var grid = '<div class="wcell" style="background:none;border:none;padding:0" data-head="1">' + head + "</div>";
state.habits.forEach(function (h) {
var cells = days.map(function (d) {
var on = h.done.indexOf(d) !== -1;
var isToday = d === todayStr();
return "<i data-habid='" + h.id + "' data-day='" + d + "'" + (on ? " class='on'" : "") + (isToday ? " data-today='1'" : "") + " title='" + d + "'></i>";
}).join("");
var weekDone = days.filter(function (d) { return h.done.indexOf(d) !== -1; }).length;
grid += '<div class="wcell" data-habid="' + h.id + '" role="button" tabindex="0" style="cursor:pointer">' +
"<span>" + h.name + "</span>" +
'<div class="wcells">' + cells + "</div>" +
"<b class='" + (weekDone >= h.goal ? "full" : "") + "'>" + weekDone + " / " + h.goal + "</b>" +
"</div>";
});
document.getElementById("weekGrid").innerHTML = grid;
}
function renderSleep() {
var b = document.querySelector('[data-sleepkey="bedtime"]');
var w = document.querySelector('[data-sleepkey="wake"]');
if (b) b.innerHTML = '<svg class="kt-icon" aria-hidden="true"><use href="#moon"></use></svg><span>' + state.bedtime + "</span>";
if (w) w.innerHTML = '<svg class="kt-icon" aria-hidden="true"><use href="#sun"></use></svg><span>' + state.wake + "</span>";
}
function renderAll() {
renderTopbar();
renderKpi();
renderDayList();
renderWeekGrid();
renderSleep();
}
// ---------- действия ----------
function afterChange() { saveLocal(); pushToServer(); renderAll(); }
function toggleItem(time) {
var t = todayEntry();
var i = t.done.indexOf(time);
if (i === -1) t.done.push(time);
else t.done.splice(i, 1);
if (t.done.length === state.schedule.length && typeof t.sleep !== "number") t.sleep = sleepHours();
afterChange();
}
function toggleHabitDay(habId, day) {
var h = state.habits.find(function (x) { return x.id === habId; });
if (!h) return;
var i = h.done.indexOf(day);
if (i === -1) h.done.push(day);
else h.done.splice(i, 1);
afterChange();
}
function markAllToday() {
var t = todayEntry();
var allDone = t.done.length >= state.schedule.length;
state.schedule.forEach(function (it) {
var i = t.done.indexOf(it.time);
if (allDone && i !== -1) t.done.splice(i, 1);
if (!allDone && i === -1) t.done.push(it.time);
});
if (typeof t.sleep !== "number") t.sleep = sleepHours();
toast(allDone ? "День сброшен" : "Все пункты отмечены — отличный день", "ok");
afterChange();
}
// ---------- drawer: день ----------
var drawerDay = null;
function openDayDrawer(day) {
drawerDay = day;
var dt = new Date(day + "T12:00:00");
var e = state.log[day] || { done: [], habits: {} };
var cur = currentDayProgress(e);
var isToday = day === todayStr();
var pill = document.getElementById("dayDrawerPill");
if (isToday) {
pill.setAttribute("data-status", cur.done === 0 ? "info" : "warn");
pill.textContent = cur.done + " из " + cur.total + " — твой текущий день";
} else if (cur.pct === 100) {
pill.setAttribute("data-status", "ok"); pill.textContent = "День по плану — 100%";
} else if (cur.done === 0) {
pill.setAttribute("data-status", "risk"); pill.textContent = "День не выполнен";
} else {
pill.setAttribute("data-status", "warn"); pill.textContent = "Выполнено на " + cur.pct + " %";
}
document.getElementById("dayDrawerDate").textContent = WD[dt.getDay()] + ", " + fmtDate(dt, true);
var rows = state.schedule.map(function (it) {
var ck = (e.done || []).indexOf(it.time) !== -1;
return '<div class="kt-ai-row">' +
'<span class="time">' + it.time + "</span><span>" + it.title + "</span>" +
'<span class="end"><span class="kt-ai-status-pill" data-status="' + (ck ? "ok" : "info") + '">' + (ck ? "готово" : "не сделано") + "</span></span></div>";
}).join("");
if (typeof e.sleep === "number") {
rows += '<div class="kt-ai-row"><span class="time"></span><span>Сон за этот день</span>' +
'<span class="end"><span class="kt-ai-status-pill" data-status="' + (e.sleep >= 7 ? "ok" : "warn") + '">' + e.sleep.toFixed(1).replace(".", ",") + " ч</span></span></div>";
}
document.getElementById("dayDrawerList").innerHTML = rows || '<div class="kt-ai-empty"><div class="title">Нет данных</div></div>';
document.getElementById("dayDrawer").style.display = "block";
}
// ---------- drawer: привычка ----------
var drawerHabitId = null;
function openHabitDrawer(habId) {
var h = state.habits.find(function (x) { return x.id === habId; });
if (!h) return;
drawerHabitId = habId;
document.getElementById("habitDrawerTitle").textContent = h.name;
document.getElementById("habitDrawerNote").textContent = h.note || "";
renderHabitDrawerCells();
document.getElementById("habitDrawer").style.display = "block";
}
function renderHabitDrawerCells() {
var h = state.habits.find(function (x) { return x.id === drawerHabitId; });
if (!h) return;
var days = lastNDays().slice(-7);
var wrap = document.getElementById("habitDrawerCells");
wrap.innerHTML = days.map(function (d) {
var dt = new Date(d + "T12:00:00");
var on = h.done.indexOf(d) !== -1;
return "<i data-habid2='" + h.id + "' data-day='" + d + "'" + (on ? " class='on'" : "") + (d === todayStr() ? " data-today='1'" : "") + " title='" + WD[dt.getDay()] + " · " + d + "'></i>";
}).join("");
}
function deleteHabit() {
if (!drawerHabitId) return;
state.habits = state.habits.filter(function (x) { return x.id !== drawerHabitId; });
drawerHabitId = null;
document.getElementById("habitDrawer").style.display = "none";
afterChange();
toast("Привычка удалена");
}
// ---------- modal: новая привычка ----------
function openHabitModal() {
document.getElementById("hName").value = "";
document.getElementById("hNote").value = "";
document.getElementById("hDays").value = "5";
document.getElementById("habitOverlay").style.display = "flex";
document.getElementById("habitModal").hidden = false;
setTimeout(function () { document.getElementById("hName").focus(); }, 50);
}
function closeHabitModal() {
document.getElementById("habitOverlay").style.display = "none";
document.getElementById("habitModal").hidden = true;
}
function saveHabit() {
var name = document.getElementById("hName").value.trim();
if (!name) { toast("Дай привычке название", "warn"); return; }
state.habits.push({
id: "h" + Date.now().toString(36),
name: name,
note: document.getElementById("hNote").value.trim(),
goal: parseInt(document.getElementById("hDays").value, 10) || 5,
done: []
});
closeHabitModal();
afterChange();
toast("Привычка добавлена", "ok");
}
function closeDrawers() {
document.getElementById("dayDrawer").style.display = "none";
document.getElementById("habitDrawer").style.display = "none";
drawerDay = null; drawerHabitId = null;
}
// ---------- события ----------
function bind() {
var dayList = document.getElementById("dayList");
dayList.addEventListener("change", function (ev) {
var cb = ev.target.closest("input[type=checkbox]");
if (!cb) return;
var row = cb.closest("[data-idx]");
var it = state.schedule[parseInt(row.getAttribute("data-idx"), 10)];
if (it) toggleItem(it.time);
});
dayList.addEventListener("click", function (ev) {
if (ev.target.closest(".kt-ai-checkbox")) return;
var name = ev.target.closest(".item-name");
if (!name) return;
var row = name.closest("[data-idx]");
var it = state.schedule[parseInt(row.getAttribute("data-idx"), 10)];
if (it) toast(it.time + " · " + it.title + " — отметь галочкой, когда сделаешь", "info");
});
var grid = document.getElementById("weekGrid");
grid.addEventListener("click", function (ev) {
var dcell = ev.target.closest("[data-dkeyday]");
if (dcell) { openDayDrawer(dcell.getAttribute("data-dkeyday")); return; }
var hcell = ev.target.closest(".wcell[data-habid]");
if (hcell) openHabitDrawer(hcell.getAttribute("data-habid"));
});
grid.addEventListener("keydown", function (ev) {
if (ev.key !== "Enter" && ev.key !== " ") return;
var cell = ev.target.closest(".wcell[data-habid]");
if (!cell) return;
ev.preventDefault();
openHabitDrawer(cell.getAttribute("data-habid"));
});
document.getElementById("habitDrawer").addEventListener("click", function (ev) {
var c = ev.target.closest("i[data-habid2]");
if (c) toggleHabitDay(c.getAttribute("data-habid2"), c.getAttribute("data-day"));
});
document.getElementById("btnToday").addEventListener("click", markAllToday);
document.getElementById("btnAddHabit").addEventListener("click", openHabitModal);
document.getElementById("btnHabitCancel").addEventListener("click", closeHabitModal);
document.getElementById("btnHabitSave").addEventListener("click", saveHabit);
document.getElementById("btnDeleteHabit").addEventListener("click", deleteHabit);
document.getElementById("habitOverlay").addEventListener("click", closeHabitModal);
document.addEventListener("keydown", function (ev) {
if (ev.key !== "Escape") return;
closeDrawers(); closeHabitModal();
});
["#day", "#habits", "#sleep"].forEach(function (sel) {
var a = document.querySelector('.kt-ai-nav-item[href="' + sel + '"]');
if (!a) return;
a.addEventListener("click", function (ev) {
ev.preventDefault();
document.querySelector(sel).scrollIntoView({ behavior: "smooth", block: "start" });
a.classList.add("is-active");
document.querySelectorAll(".kt-ai-nav-item").forEach(function (x) { if (x !== a) x.classList.remove("is-active"); });
});
});
}
// ---------- старт ----------
(function init() {
state = {
wake: DEFAULTS.wake,
bedtime: DEFAULTS.bedtime,
schedule: DEFAULTS.schedule.map(function (x) { return Object.assign({}, x); }),
habits: DEFAULTS.habits.map(function (x) { return { id: x.id, name: x.name, note: x.note, goal: x.goal, done: x.done.concat() }; }),
log: {}
};
seedDemoDays();
applyLocal();
bind();
renderAll();
fetch("api/state")
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (data) {
if (data && data.log && Object.keys(data.log).length) {
state.wake = data.wake || state.wake;
state.bedtime = data.bedtime || state.bedtime;
state.schedule = data.schedule && data.schedule.length ? data.schedule : state.schedule;
state.habits = data.habits ? data.habits : state.habits;
state.log = Object.assign(freshLog(), data.log);
serverLogReady = true;
saveLocal();
renderAll();
}
})
.catch(function () {});
})();
})();