/
sc_coder
/
MentalCheckList
Обзор
Документация
Войти
/
sc_coder
/
MentalCheckList
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
app.js
522 строки
19 KB
ScCoder
Показывать картинку кота по проценту результата теста
14 июл 2026, 09:50
14 июл 2026, 09:50
0483506
Код
Авторство
О чём код?
/** * Точка входа приложения "Ментальный Чек-Лист". * Отвечает только за DOM: рендеринг и обработчики событий. * Вся бизнес-логика вынесена в logic.js, всё хранение — в storage.js. */ (function () { 'use strict'; var Logic = window.MCLLogic; var Storage = window.MCLStorage; var REMINDER_CHECK_INTERVAL_MS = 30000; var state = { questions: [], threshold: Logic.DEFAULT_THRESHOLD, todayISO: Logic.formatDateISO(new Date()), todayAnswers: {}, quizIndex: 0, quizVariants: [], calendarYear: new Date().getFullYear(), calendarMonth: new Date().getMonth(), reminder: { enabled: false, time: '20:00' } }; function $(id) { return document.getElementById(id); } // ---------- Вкладки ---------- function switchTab(tabName) { ['today', 'settings', 'history'].forEach(function (name) { var active = name === tabName; $('view-' + name).hidden = !active; var btn = $('tab-' + name); btn.classList.toggle('bg-indigo-600', active); btn.classList.toggle('text-white', active); btn.classList.toggle('shadow-md', active); btn.classList.toggle('shadow-indigo-200', active); btn.classList.toggle('text-gray-500', !active); }); if (tabName === 'today') { renderTodayIntro(); setTodayView('intro'); } if (tabName === 'history') renderCalendar(); } // ---------- Сегодняшний опрос (интро → опрос по одному вопросу → результат) ---------- function setTodayView(view) { $('today-intro').hidden = view !== 'intro'; $('today-quiz').hidden = view !== 'quiz'; $('today-result-view').hidden = view !== 'result'; } function renderTodayIntro() { $('today-date').textContent = Logic.formatDateDisplay(new Date()); var entry = Storage.getHistoryEntry(state.todayISO); var summary = $('today-summary'); if (entry) { var isYellow = entry.zone === 'yellow'; summary.hidden = false; summary.textContent = (isYellow ? '⚠️ ' : '✅ ') + 'Сегодня пройдено: ' + entry.percentage + '%'; summary.className = 'inline-flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-semibold mb-4 ' + (isYellow ? 'bg-amber-100 text-amber-700' : 'bg-emerald-100 text-emerald-700'); $('intro-title').textContent = 'Готово на сегодня'; $('intro-subtitle').textContent = 'Можешь пройти чек-лист ещё раз, если что-то изменилось'; $('start-quiz-btn').textContent = 'Пройти заново'; } else { summary.hidden = true; $('intro-title').textContent = 'Как ты сегодня?'; $('intro-subtitle').textContent = 'Пройди короткий опрос, чтобы отследить своё состояние'; $('start-quiz-btn').textContent = 'Пройти чек-лист'; } } function startQuiz() { if (state.questions.length === 0) { alert('Добавьте хотя бы один вопрос в настройках, прежде чем проходить чек-лист.'); return; } state.quizIndex = 0; state.todayAnswers = {}; // Формулировка (позитивная/негативная) фиксируется один раз на весь опрос, // чтобы её нельзя было предсказать по прошлым разам ответа "на автомате". state.quizVariants = state.questions.map(function (q) { return Logic.pickVariant(q, Math.random()); }); setTodayView('quiz'); renderQuizQuestion(); } function renderQuizQuestion() { var total = state.questions.length; var question = state.questions[state.quizIndex]; var variant = state.quizVariants[state.quizIndex]; $('quiz-counter').textContent = 'Вопрос ' + (state.quizIndex + 1) + ' из ' + total; $('quiz-question-text').textContent = Logic.getQuestionPromptText(question, variant); var progress = $('quiz-progress'); progress.innerHTML = ''; for (var i = 0; i < total; i++) { var dot = document.createElement('span'); dot.className = 'h-1.5 rounded-full transition-all duration-300 ' + (i < state.quizIndex ? 'w-4 bg-indigo-300' : i === state.quizIndex ? 'w-6 bg-indigo-600' : 'w-1.5 bg-gray-200'); progress.appendChild(dot); } var card = $('quiz-card'); card.classList.remove('anim-pop'); void card.offsetWidth; card.classList.add('anim-pop'); } function answerQuestion(answeredYes) { var question = state.questions[state.quizIndex]; var variant = state.quizVariants[state.quizIndex]; state.todayAnswers[question.id] = Logic.isSymptomPresent(variant, answeredYes); state.quizIndex++; if (state.quizIndex >= state.questions.length) { finishQuiz(); } else { renderQuizQuestion(); } } function finishQuiz() { var entry = Logic.buildHistoryEntry(state.todayISO, state.questions, state.todayAnswers, state.threshold); Storage.saveHistoryEntry(entry); renderResultView(entry); setTodayView('result'); } function catImageForPercentage(percentage) { var index = Math.ceil(percentage / 10); if (index < 1) index = 1; if (index > 10) index = 10; return 'icons/cats/cat-' + index + '.jpg'; } function renderResultView(entry) { var isYellow = entry.zone === 'yellow'; $('result-cat-image').src = catImageForPercentage(entry.percentage); var badge = $('result-badge'); badge.className = 'w-20 h-20 rounded-full flex items-center justify-center mx-auto mb-4 text-2xl font-extrabold ' + (isYellow ? 'bg-amber-100 text-amber-600' : 'bg-emerald-100 text-emerald-600'); badge.textContent = entry.percentage + '%'; $('result-message').textContent = isYellow ? 'Жёлтая зона' : 'Всё спокойно'; $('result-detail').textContent = entry.checkedCount + ' из ' + entry.totalCount + ' отмечено · порог ' + entry.threshold + '%'; $('warning-banner').hidden = !isYellow; var list = $('result-checked-list'); list.innerHTML = ''; state.questions .filter(function (q) { return entry.answers[q.id]; }) .forEach(function (q) { var li = document.createElement('li'); li.className = 'text-xs bg-gray-100 text-gray-600 px-3 py-1 rounded-full'; li.textContent = q.negativeText; list.appendChild(li); }); } // ---------- Настройки ---------- function buildQuestionTextBlock(question) { var wrap = document.createElement('div'); wrap.className = 'flex flex-col gap-1 pr-3 min-w-0'; var negRow = document.createElement('p'); negRow.className = 'text-sm text-gray-700'; negRow.innerHTML = '<span class="text-[10px] font-bold uppercase tracking-wide text-rose-400 mr-1.5">Негатив</span>'; negRow.appendChild(document.createTextNode(question.negativeText)); wrap.appendChild(negRow); if (question.positiveText) { var posRow = document.createElement('p'); posRow.className = 'text-sm text-gray-500'; posRow.innerHTML = '<span class="text-[10px] font-bold uppercase tracking-wide text-emerald-400 mr-1.5">Позитив</span>'; posRow.appendChild(document.createTextNode(question.positiveText)); wrap.appendChild(posRow); } return wrap; } function renderSettings() { $('threshold-slider').value = state.threshold; $('threshold-value').textContent = state.threshold + '%'; var poolList = $('pool-questions'); poolList.innerHTML = ''; Logic.DEFAULT_QUESTIONS.forEach(function (poolQ) { var active = Logic.isQuestionActive(state.questions, poolQ.id); var li = document.createElement('li'); li.className = 'flex items-center justify-between gap-3 bg-white rounded-2xl px-4 py-3.5 shadow-sm ring-1 ring-black/5'; li.appendChild(buildQuestionTextBlock(poolQ)); var label = document.createElement('label'); label.className = 'relative inline-flex items-center shrink-0 cursor-pointer'; label.innerHTML = '<input type="checkbox" class="sr-only peer">' + '<span class="w-11 h-6 bg-gray-200 rounded-full peer-checked:bg-indigo-600 transition-colors"></span>' + '<span class="absolute left-1 top-1 w-4 h-4 bg-white rounded-full shadow transition-transform peer-checked:translate-x-5"></span>'; var checkbox = label.querySelector('input'); checkbox.checked = active; checkbox.addEventListener('change', function () { state.questions = Logic.toggleDefaultQuestion(state.questions, poolQ, checkbox.checked); Storage.saveQuestions(state.questions); renderSettings(); renderTodayIntro(); }); li.appendChild(label); poolList.appendChild(li); }); var customList = $('custom-questions'); customList.innerHTML = ''; var customQuestions = state.questions.filter(function (q) { return q.custom; }); if (customQuestions.length === 0) { var empty = document.createElement('li'); empty.className = 'text-sm text-gray-400 italic px-1'; empty.textContent = 'Своих вопросов пока нет'; customList.appendChild(empty); } customQuestions.forEach(function (q) { var li = document.createElement('li'); li.className = 'flex items-center justify-between gap-3 bg-white rounded-2xl px-4 py-3.5 shadow-sm ring-1 ring-black/5'; li.appendChild(buildQuestionTextBlock(q)); var removeBtn = document.createElement('button'); removeBtn.type = 'button'; removeBtn.className = 'btn-press shrink-0 w-7 h-7 rounded-full flex items-center justify-center text-rose-500 bg-rose-50 hover:bg-rose-100 text-sm font-bold transition'; removeBtn.textContent = '✕'; removeBtn.addEventListener('click', function () { state.questions = Logic.removeQuestion(state.questions, q.id); Storage.saveQuestions(state.questions); renderSettings(); renderTodayIntro(); }); li.appendChild(removeBtn); customList.appendChild(li); }); } function handleThresholdInput() { var val = Logic.clampThreshold($('threshold-slider').value); $('threshold-value').textContent = val + '%'; } function handleThresholdChange() { var val = Logic.clampThreshold($('threshold-slider').value); state.threshold = Storage.saveThreshold(val); } function handleAddQuestion(e) { e.preventDefault(); var negativeInput = $('new-question-negative'); var positiveInput = $('new-question-positive'); try { state.questions = Logic.addQuestion(state.questions, negativeInput.value, positiveInput.value); Storage.saveQuestions(state.questions); negativeInput.value = ''; positiveInput.value = ''; renderSettings(); renderTodayIntro(); } catch (err) { alert(err.message); } } // ---------- История ---------- var ZONE_CLASSES = { green: 'bg-emerald-400 text-white shadow-sm shadow-emerald-100', yellow: 'bg-amber-400 text-white shadow-sm shadow-amber-100' }; function renderCalendar() { var weeks = Logic.getMonthMatrix(state.calendarYear, state.calendarMonth); var monthLabel = new Date(state.calendarYear, state.calendarMonth, 1); $('calendar-label').textContent = Logic.formatDateDisplay(monthLabel).replace(/^\d+ /, ''); var grid = $('calendar-grid'); grid.innerHTML = ''; ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'].forEach(function (d) { var cell = document.createElement('div'); cell.className = 'text-xs text-gray-400 text-center font-semibold py-1'; cell.textContent = d; grid.appendChild(cell); }); weeks.forEach(function (week) { week.forEach(function (date) { var cell = document.createElement('button'); cell.type = 'button'; cell.className = 'aspect-square rounded-xl text-sm font-medium flex items-center justify-center transition-transform hover:scale-105'; if (!date) { cell.className += ' invisible'; grid.appendChild(cell); return; } var iso = Logic.formatDateISO(date); var entry = Storage.getHistoryEntry(iso); var isToday = iso === state.todayISO; cell.textContent = date.getDate(); cell.className += entry ? ' ' + ZONE_CLASSES[entry.zone] : ' bg-gray-50 text-gray-400 hover:bg-gray-100'; if (isToday) cell.className += ' ring-2 ring-indigo-500 ring-offset-1'; cell.addEventListener('click', function () { showDayDetail(iso, entry); }); grid.appendChild(cell); }); }); } function showDayDetail(iso, entry) { var box = $('day-detail'); box.innerHTML = ''; var card = document.createElement('div'); card.className = 'anim-in bg-white rounded-2xl shadow-sm ring-1 ring-black/5 p-5'; if (!entry) { var emptyMsg = document.createElement('p'); emptyMsg.className = 'text-sm text-gray-400'; emptyMsg.textContent = iso + ': нет данных'; card.appendChild(emptyMsg); box.appendChild(card); return; } var isYellow = entry.zone === 'yellow'; var title = document.createElement('p'); title.className = 'font-bold text-gray-800 text-sm mb-1'; title.textContent = iso + ' — ' + entry.percentage + '% (' + (isYellow ? 'жёлтая зона' : 'норма') + ')'; card.appendChild(title); var checkedQuestions = state.questions.filter(function (q) { return entry.answers[q.id]; }); if (checkedQuestions.length > 0) { var list = document.createElement('ul'); list.className = 'flex flex-wrap gap-1.5 mt-3'; checkedQuestions.forEach(function (q) { var li = document.createElement('li'); li.className = 'text-xs bg-gray-100 text-gray-600 px-3 py-1 rounded-full'; li.textContent = q.negativeText; list.appendChild(li); }); card.appendChild(list); } box.appendChild(card); } function changeMonth(delta) { var d = new Date(state.calendarYear, state.calendarMonth + delta, 1); state.calendarYear = d.getFullYear(); state.calendarMonth = d.getMonth(); renderCalendar(); } // ---------- Напоминание ---------- function notificationsSupported() { return typeof Notification !== 'undefined'; } function renderReminderStatus() { var status = $('reminder-status'); if (!notificationsSupported()) { status.textContent = 'Уведомления не поддерживаются этим браузером.'; return; } if (Notification.permission === 'denied') { status.textContent = 'Уведомления заблокированы в браузере. Разрешите их в настройках сайта.'; return; } if (state.reminder.enabled) { status.textContent = 'Работает, пока приложение открыто (вкладка или установленное на экран приложение). ' + 'Полностью закрытый браузер уведомление не покажет — это ограничение веб-платформы без сервера.'; } else { status.textContent = ''; } } function renderReminderSettings() { $('reminder-enabled').checked = state.reminder.enabled; $('reminder-time').value = state.reminder.time; renderReminderStatus(); } function handleReminderEnabledChange() { var checkbox = $('reminder-enabled'); if (checkbox.checked && (!notificationsSupported() || Notification.permission === 'denied')) { checkbox.checked = false; alert('Не удалось включить напоминания: уведомления недоступны или заблокированы в браузере.'); return; } var applyChange = function () { state.reminder = Storage.saveReminderSettings({ enabled: checkbox.checked, time: state.reminder.time }); renderReminderStatus(); }; if (checkbox.checked && notificationsSupported() && Notification.permission === 'default') { Notification.requestPermission().then(function (permission) { checkbox.checked = permission === 'granted'; applyChange(); }); } else { applyChange(); } } function handleReminderTimeChange() { state.reminder = Storage.saveReminderSettings({ enabled: state.reminder.enabled, time: $('reminder-time').value }); renderReminderStatus(); } function showReminderNotification() { var title = 'Ментальный Чек-Лист'; var options = { body: 'Пора пройти сегодняшний чек-лист', icon: 'icons/icon.svg', tag: 'daily-reminder' }; if (navigator.serviceWorker && navigator.serviceWorker.ready) { navigator.serviceWorker.ready.then(function (reg) { reg.showNotification(title, options); }); } else { new Notification(title, options); } } function checkReminder() { if (!state.reminder.enabled || !notificationsSupported() || Notification.permission !== 'granted') { return; } var todayISO = Logic.formatDateISO(new Date()); var hasCompletedToday = !!Storage.getHistoryEntry(todayISO); var lastNotified = Storage.getLastNotifiedDate(); if (Logic.shouldNotifyNow(new Date(), state.reminder.time, lastNotified, todayISO, hasCompletedToday)) { showReminderNotification(); Storage.setLastNotifiedDate(todayISO); } } // ---------- Инициализация ---------- function init() { state.questions = Storage.getQuestions(); state.threshold = Storage.getThreshold(); state.reminder = Storage.getReminderSettings(); $('tab-today').addEventListener('click', function () { switchTab('today'); }); $('tab-settings').addEventListener('click', function () { switchTab('settings'); }); $('tab-history').addEventListener('click', function () { switchTab('history'); }); $('start-quiz-btn').addEventListener('click', startQuiz); $('quiz-yes-btn').addEventListener('click', function () { answerQuestion(true); }); $('quiz-no-btn').addEventListener('click', function () { answerQuestion(false); }); $('result-redo-btn').addEventListener('click', startQuiz); $('result-done-btn').addEventListener('click', function () { renderTodayIntro(); setTodayView('intro'); }); $('threshold-slider').addEventListener('input', handleThresholdInput); $('threshold-slider').addEventListener('change', handleThresholdChange); $('add-question-form').addEventListener('submit', handleAddQuestion); $('prev-month').addEventListener('click', function () { changeMonth(-1); }); $('next-month').addEventListener('click', function () { changeMonth(1); }); $('reminder-enabled').addEventListener('change', handleReminderEnabledChange); $('reminder-time').addEventListener('change', handleReminderTimeChange); renderTodayIntro(); renderSettings(); renderReminderSettings(); switchTab('today'); checkReminder(); setInterval(checkReminder, REMINDER_CHECK_INTERVAL_MS); if ('serviceWorker' in navigator) { navigator.serviceWorker.register('sw.js').catch(function (err) { console.warn('Service worker registration failed:', err); }); } } document.addEventListener('DOMContentLoaded', init); })();