<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Координаты дня</title>
<style>
:root {
--axis-color: #333;
--grid-color: rgba(0, 0, 0, 0.08);
--text-color: #222;
--bg-color: #fff;
--btn-bg: #f5f5f5;
}
@media (prefers-color-scheme: dark) {
:root {
--axis-color: #ddd;
--grid-color: rgba(255, 255, 255, 0.1);
--text-color: #eee;
--bg-color: #121212;
--btn-bg: #1e1e1e;
}
}
body, html { margin: 0; padding: 0; height: 100%; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg-color); color: var(--text-color); display: flex; flex-direction: column; overflow: hidden; touch-action: pan-y; }
.header { text-align: center; padding: 15px 10px 5px; position: relative; z-index: 10; background: var(--bg-color); border-bottom: 1px solid var(--grid-color); }
.month-nav { display: flex; justify-content: space-between; align-items: center; max-width: 300px; margin: 0 auto; gap: 10px; }
.month-label { font-size: 18px; font-weight: 600; min-width: 120px; text-align: center; }
.nav-btn { width: 36px; height: 36px; border: 1px solid var(--grid-color); border-radius: 8px; background: var(--btn-bg); display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 20px; line-height: 1; }
.nav-btn:active { background: var(--grid-color); }
.chart-container { flex-grow: 1; position: relative; padding: 60px 40px 60px 50px; overflow: hidden; }
svg { width: 100%; height: 100%; display: block; }
.y-axis line, .x-axis line { stroke: var(--axis-color); stroke-width: 1; shape-rendering: crispEdges; }
.grid line { stroke: var(--grid-color); stroke-width: 1; }
.tick-text { fill: var(--text-color); font-size: 12px; dominant-baseline: middle; pointer-events: none; user-select: none; }
.y-tick { text-anchor: end; dx: -8px; }
.x-tick { text-anchor: middle; dy: 20px; opacity: 0.7; }
.point { cursor: pointer; transition: r 0.1s ease; }
.point:hover { r: 10 !important; }
/* Цвета точек */
.p-0 { fill: #D32F2F; } /* Красный */
.p-6 { fill: #9E9E9E; } /* Серый */
.p-7 { fill: #FFEB3B; } /* Желтый */
.p-8 { fill: #FFA000; } /* Оранжевый */
.p-9 { fill: #64DD17; } /* Салатовый */
.p-10 { fill: #00C853; } /* Изумрудный */
.empty-day { fill: transparent; stroke: var(--grid-color); stroke-width: 1; rx: 5; cursor: pointer; }
.empty-day:hover { fill: rgba(0,0,0,0.02); }
.modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; visibility: hidden; opacity: 0; transition: opacity 0.2s ease; z-index: 100; }
.modal.active { visibility: visible; opacity: 1; }
.modal-content { background: var(--bg-color); padding: 25px; border-radius: 16px; width: 90%; max-width: 400px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); transform: scale(0.9); transition: transform 0.2s ease; }
.modal.active .modal-content { transform: scale(1); }
.modal-header { font-size: 18px; font-weight: 600; margin-bottom: 10px; }
.score-display { font-size: 48px; font-weight: bold; text-align: center; margin: 15px 0; line-height: 1; }
.note-input { width: 100%; padding: 12px; border: 1px solid var(--grid-color); border-radius: 8px; font-size: 16px; margin-top: 15px; box-sizing: border-box; background: inherit; color: inherit; }
.modal-actions { display: flex; gap: 10px; margin-top: 20px; }
.btn { flex: 1; padding: 12px; border: none; border-radius: 8px; font-size: 16px; cursor: pointer; text-align: center; }
.btn-cancel { background: var(--btn-bg); color: var(--text-color); }
.btn-save { background: #007AFF; color: white; }
.toast { position: fixed; top: 100px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.8); color: white; padding: 12px 20px; border-radius: 25px; font-size: 14px; opacity: 0; pointer-events: none; transition: opacity 0.3s; z-index: 200; }
.toast.show { opacity: 1; }
.legend { position: absolute; top: 15px; right: 15px; font-size: 12px; background: rgba(255,255,255,0.9); backdrop-filter: blur(10px); padding: 8px 12px; border-radius: 20px; border: 1px solid var(--grid-color); display: none; }
@media (min-width: 500px) { .legend { display: block; } }
.legend-item { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
.legend-dot { width: 10px; height: 10px; border-radius: 50%; border: 1px solid var(--grid-color); }
</style>
</head>
<body>
<div class="header">
<div class="month-nav">
<div class="nav-btn" id="prevMonth">‹</div>
<div class="month-label" id="monthLabel"></div>
<div class="nav-btn" id="nextMonth">›</div>
</div>
</div>
<div class="chart-container" id="chartArea">
<!-- SVG График будет здесь -->
</div>
<!-- Модальное окно подтверждения -->
<div class="modal" id="confirmModal">
<div class="modal-content">
<div class="modal-header">Оценка за день</div>
<div class="day-indicator" id="modalDay"></div>
<div class="score-display" id="modalScore"></div>
<input type="text" class="note-input" id="noteInput" placeholder="Заметка (необязательно)">
<div class="modal-actions">
<button class="btn btn-cancel" id="cancelBtn">Отмена</button>
<button class="btn btn-save" id="saveBtn">Сохранить</button>
</div>
</div>
</div>
<div class="toast" id="toast">Записано</div>
<script>
// --- ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ---
let currentDate = new Date();
let dataStore = {};
const efficiencyMap = [
{val: 0, label: 'Нулевая активность', colorClass: 'p-0'},
{val: 6, label: 'Еле встал / Апатия', colorClass: 'p-6'},
{val: 7, label: 'Базовое выживание', colorClass: 'p-7'},
{val: 8, label: 'Главное дело сделано', colorClass: 'p-8'},
{val: 9, label: 'Продуктивный день', colorClass: 'p-9'},
{val: 10, label: 'Прорыв', colorClass: 'p-10'}
];
// --- ЭЛЕМЕНТЫ DOM ---
const chartArea = document.getElementById('chartArea');
const monthLabel = document.getElementById('monthLabel');
const prevMonthBtn = document.getElementById('prevMonth');
const nextMonthBtn = document.getElementById('nextMonth');
const confirmModal = document.getElementById('confirmModal');
const modalDay = document.getElementById('modalDay');
const modalScore = document.getElementById('modalScore');
const noteInput = document.getElementById('noteInput');
const saveBtn = document.getElementById('saveBtn');
const cancelBtn = document.getElementById('cancelBtn');
const toast = document.getElementById('toast');
// --- УТИЛИТЫ ---
function formatKey(date) {
return date.toISOString().slice(0, 7);
}
function getDaysInMonth(year, month) {
return new Date(year, month + 1, 0).getDate();
}
function loadData() {
const raw = localStorage.getItem('day_coordinates_data');
if (raw) {
try {
dataStore = JSON.parse(raw);
} catch(e) {
console.error("Ошибка парсинга данных", e);
dataStore = {};
}
}
}
function saveData() {
localStorage.setItem('day_coordinates_data', JSON.stringify(dataStore));
}
// --- ЛОГИКА РИСОВАНИЯ ---
function renderChart() {
const key = formatKey(currentDate);
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
const options = { month: 'long' };
monthLabel.textContent = currentDate.toLocaleDateString('ru-RU', options) + ' ' + year;
const daysInCurrentMonth = getDaysInMonth(year, month);
const monthData = dataStore[key] || [];
const containerRect = chartArea.getBoundingClientRect();
const w = containerRect.width;
const h = containerRect.height;
const paddingLeft = 50;
const paddingBottom = 60;
const plotW = w - paddingLeft;
const plotH = h - paddingBottom;
const xScale = (i) => paddingLeft + (i * plotW / 31);
const yScale = (score) => {
const levels = [0, 6, 7, 8, 9, 10];
const step = plotH / (levels.length - 1);
const index = levels.indexOf(score);
return h - paddingBottom - (index * step);
};
chartArea.innerHTML = '';
const svgNS = "http://www.w3.org/2000/svg";
const svg = document.createElementNS(svgNS, 'svg');
svg.setAttribute('width', w);
svg.setAttribute('height', h);
chartArea.appendChild(svg);
const gridGroup = document.createElementNS(svgNS, 'g');
const axisGroup = document.createElementNS(svgNS, 'g');
svg.appendChild(gridGroup);
svg.appendChild(axisGroup);
const yLevels = [0, 6, 7, 8, 9, 10];
yLevels.forEach((lvl, idx) => {
const y = yScale(lvl);
const line = document.createElementNS(svgNS, 'line');
line.setAttribute('x1', paddingLeft);
line.setAttribute('x2', w);
line.setAttribute('y1', y);
line.setAttribute('y2', y);
line.classList.add('grid');
gridGroup.appendChild(line);
const text = document.createElementNS(svgNS, 'text');
text.setAttribute('x', paddingLeft - 15);
text.setAttribute('y', y + 4);
text.classList.add('tick-text', 'y-tick');
text.textContent = lvl === 0 ? '0' : lvl.toString();
axisGroup.appendChild(text);
});
for(let i = 1; i <= daysInCurrentMonth; i++) {
const x = xScale(i - 1);
const vLine = document.createElementNS(svgNS, 'line');
vLine.setAttribute('x1', x);
vLine.setAttribute('x2', x);
vLine.setAttribute('y1', 0);
vLine.setAttribute('y2', h - paddingBottom);
vLine.classList.add('grid');
gridGroup.appendChild(vLine);
const xText = document.createElementNS(svgNS, 'text');
xText.setAttribute('x', x);
xText.setAttribute('y', h - paddingBottom + 20);
xText.classList.add('tick-text', 'x-tick');
xText.textContent = i;
axisGroup.appendChild(xText);
}
const pointsGroup = document.createElementNS(svgNS, 'g');
svg.appendChild(pointsGroup);
monthData.forEach(item => {
const day = item.dayOfMonth;
const score = item.efficiencyScore;
if(day > daysInCurrentMonth) return;
const cx = xScale(day - 1);
const cy = yScale(score);
const circle = document.createElementNS(svgNS, 'circle');
circle.setAttribute('cx', cx);
circle.setAttribute('cy', cy);
circle.setAttribute('r', 8);
circle.classList.add('point', item.colorClass);
circle.dataset.day = day;
circle.dataset.score = score;
circle.dataset.note = item.note || '';
pointsGroup.appendChild(circle);
});
for(let d = 1; d <= daysInCurrentMonth; d++) {
const exists = monthData.find(it => it.dayOfMonth === d);
if(exists) continue;
const rect = document.createElementNS(svgNS, 'rect');
const size = 20;
const rx = xScale(d - 1) - size/2;
const ry = yScale(6) - size/2;
rect.setAttribute('x', rx);
rect.setAttribute('y', ry);
rect.setAttribute('width', size);
rect.setAttribute('height', size);
rect.classList.add('empty-day');
rect.dataset.day = d;
pointsGroup.appendChild(rect);
}
pointsGroup.addEventListener('click', function(e) {
let target = e.target;
if(target.tagName !== 'circle' && target.tagName !== 'rect') return;
const day = parseInt(target.dataset.day);
const today = new Date();
const isPastOrToday = (
currentDate.getMonth() === today.getMonth() &&
currentDate.getFullYear() === today.getFullYear() &&
day <= today.getDate()
) || currentDate < today;
if(!isPastOrToday) return;
const existingScore = parseInt(target.dataset.score);
openModal(day, existingScore);
});
}
// --- МОДАЛЬНОЕ ОКНО ---
function openModal(day, presetScore = null) {
modalDay.textContent = `День ${day}`;
if(presetScore !== null){
modalScore.textContent = presetScore;
modalScore.className = `score-display ${findColorClass(presetScore)}`;
noteInput.value = findNoteFor(day, presetScore);
} else {
modalScore.textContent = '?';
modalScore.className = 'score-display';
noteInput.value = '';
}
confirmModal.classList.add('active');
const allPoints = document.querySelectorAll('.point');
allPoints.forEach(pt => pt.style.outline = '');
const handler = function(e) {
const clickedScore = parseInt(e.target.dataset.score);
modalScore.textContent = clickedScore;
modalScore.className = `score-display ${e.target.classList.contains('point') ? e.target.classList[1] : ''}`;
allPoints.forEach(pt => pt.removeEventListener('click', handler));
};
if(presetScore === null){
setTimeout(() => {
allPoints.forEach(pt => pt.addEventListener('click', handler));
}, 300);
}
}
function closeModal(save = false) {
confirmModal.classList.remove('active');
if(save){
const day = parseInt(modalDay.textContent.replace('День ', ''));
const score = parseInt(modalScore.textContent);
const note = noteInput.value.trim();
upsertPoint({date: currentDate, dayOfMonth: day, efficiencyScore: score, note: note});
showToast('Записано');
renderChart();
}
}
// --- РАБОТА С ДАННЫМИ ---
function upsertPoint(pointObj) {
const key = formatKey(pointObj.date);
if (!dataStore[key]) dataStore[key] = [];
dataStore[key] = dataStore[key].filter(item => item.dayOfMonth !== pointObj.dayOfMonth);
const colorCls = findColorClass(pointObj.efficiencyScore);
dataStore[key].push({
...pointObj,
colorClass: colorCls
});
dataStore[key].sort((a,b) => a.dayOfMonth - b.dayOfMonth);
saveData();
}
function findColorClass(score) {
const found = efficiencyMap.find(el => el.val === score);
return found ? found.colorClass : 'p-6';
}
function findNoteFor(day, score) {
const key = formatKey(currentDate);
const item = (dataStore[key] || []).find(it => it.dayOfMonth === day && it.efficiencyScore === score);
return item ? item.note : '';
}
function showToast(msg) {
toast.textContent = msg;
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 1500);
}
// --- НАВИГАЦИЯ ---
prevMonthBtn.addEventListener('click', () => {
currentDate.setMonth(currentDate.getMonth() - 1);
renderChart();
});
nextMonthBtn.addEventListener('click', () => {
currentDate.setMonth(currentDate.getMonth() + 1);
renderChart();
});
saveBtn.addEventListener('click', () => closeModal(true));
cancelBtn.addEventListener('click', () => closeModal(false));
window.addEventListener('keydown', (e) => {
if(e.key === 'Escape' && confirmModal.classList.contains('active')) closeModal(false);
});
// --- ЗАПУСК ---
loadData();
renderChart();
</script>
</body>
</html>