promt-dlya-vibecoding-treker/index.html

981 lines
26 KiB
HTML
Raw 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.

<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ты хозяин своих привычек</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;1,400&family=Manrope:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--bg-main: #2C1810;
--bg-card: #3E2418;
--bg-hover: #4A2C1A;
--bg-border: #5C3A28;
--text-main: #F5E6D3;
--text-secondary: #D9C4A8;
--text-muted: #A68B6F;
--accent1: #E8B598;
--accent2: #D4A574;
--accent3: #C08B65;
--accent4: #A67B5B;
--accent5: #8B6F4E;
--accent6: #F7EDE2;
--success: #B5A47A;
--shadow: 0 4px 20px rgba(44, 24, 16, 0.5);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Manrope', system-ui, sans-serif;
background: var(--bg-main);
color: var(--text-main);
min-height: 100vh;
padding: 24px;
line-height: 1.5;
}
h1, h2, h3 {
font-family: 'Cormorant Garamond', Georgia, serif;
font-weight: 600;
}
.container {
max-width: 1400px;
margin: 0 auto;
display: grid;
grid-template-columns: 320px 1fr 280px;
gap: 24px;
}
/* Header */
.header {
grid-column: 1 / -1;
display: flex;
justify-content: space-between;
align-items: flex-end;
padding-bottom: 20px;
border-bottom: 1px solid var(--bg-border);
margin-bottom: 24px;
}
.header h1 {
font-size: 42px;
color: var(--accent1);
font-style: italic;
}
.header-date {
color: var(--text-secondary);
font-size: 16px;
margin-top: 4px;
cursor: pointer;
}
.header-date:hover { color: var(--accent1); }
.month-selector {
display: flex;
gap: 4px;
flex-wrap: wrap;
justify-content: flex-end;
}
.month-btn {
background: transparent;
border: 1px solid var(--bg-border);
color: var(--text-secondary);
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s ease-out;
}
.month-btn:hover {
background: var(--bg-hover);
color: var(--text-main);
}
.month-btn.active {
background: var(--accent1);
border-color: var(--accent1);
color: var(--bg-main);
font-weight: 600;
}
/* Cards */
.card {
background: var(--bg-card);
border-radius: 12px;
padding: 20px;
box-shadow: var(--shadow);
border: 1px solid var(--bg-border);
}
.card-title {
font-size: 18px;
color: var(--text-secondary);
margin-bottom: 16px;
text-transform: uppercase;
letter-spacing: 1px;
font-size: 13px;
}
/* Schedule Section */
.schedule-section {
margin-bottom: 20px;
}
.schedule-title {
font-size: 16px;
color: var(--accent1);
margin-bottom: 12px;
font-style: italic;
}
.task-list {
list-style: none;
}
.task-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
border-radius: 6px;
margin-bottom: 6px;
transition: background 0.2s;
}
.task-item:hover {
background: var(--bg-hover);
}
.task-text {
flex: 1;
font-size: 14px;
border: none;
background: transparent;
color: var(--text-main);
padding: 4px;
border-radius: 4px;
}
.task-text:focus {
outline: none;
background: rgba(255,255,255,0.05);
}
.task-delete {
opacity: 0;
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
font-size: 18px;
padding: 0 4px;
transition: all 0.2s;
}
.task-item:hover .task-delete { opacity: 1; }
.task-delete:hover { color: var(--accent3); }
.add-task-btn {
width: 100%;
padding: 8px;
background: transparent;
border: 1px dashed var(--bg-border);
color: var(--text-secondary);
border-radius: 6px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.add-task-btn:hover {
border-color: var(--accent1);
color: var(--accent1);
}
/* Weeks Grid */
.weeks-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.week-block {
border-radius: 10px;
overflow: hidden;
border: 1px solid var(--bg-border);
}
.week-header {
padding: 12px 16px;
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1.5px;
}
.week-1 .week-header { background: var(--accent1); color: var(--bg-main); }
.week-2 .week-header { background: var(--accent3); color: var(--bg-main); }
.week-3 .week-header { background: var(--accent5); color: var(--text-main); }
.week-4 .week-header { background: var(--accent2); color: var(--bg-main); }
.week-days {
display: grid;
grid-template-columns: 1.5fr repeat(7, 1fr);
background: var(--bg-card);
border-bottom: 1px solid var(--bg-border);
}
.week-days div {
padding: 10px 6px;
text-align: center;
font-size: 12px;
color: var(--text-secondary);
border-right: 1px solid var(--bg-border);
}
.week-days div:first-child {
text-align: left;
padding-left: 12px;
border-left: none;
}
.checkbox-grid {
background: var(--bg-card);
}
.checkbox-row {
display: grid;
grid-template-columns: 1.5fr repeat(7, 1fr);
align-items: center;
}
.checkbox-row:not(:last-child) {
border-bottom: 1px solid var(--bg-border);
}
.checkbox-row .task-name {
padding: 12px;
font-size: 13px;
color: var(--text-main);
border-right: 1px solid var(--bg-border);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.checkbox-cell {
display: flex;
justify-content: center;
align-items: center;
padding: 8px 0;
border-right: 1px solid var(--bg-border);
}
.custom-checkbox {
width: 22px;
height: 22px;
border: 2px solid var(--bg-border);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease-out;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
}
.custom-checkbox:hover {
transform: scale(1.1);
border-color: var(--accent1);
}
.custom-checkbox.checked {
background: var(--accent1);
border-color: var(--accent1);
}
.custom-checkbox.checked::after {
content: '✓';
color: var(--bg-main);
font-weight: bold;
font-size: 14px;
}
.week-2 .custom-checkbox.checked { background: var(--accent3); border-color: var(--accent3); }
.week-3 .custom-checkbox.checked { background: var(--accent5); border-color: var(--accent5); }
.week-4 .custom-checkbox.checked { background: var(--accent2); border-color: var(--accent2); }
/* Bar Chart */
.chart-block {
margin-top: 16px;
padding: 16px;
background: rgba(0,0,0,0.2);
border-radius: 8px;
}
.bar-chart {
display: flex;
justify-content: space-between;
align-items: flex-end;
height: 100px;
gap: 8px;
padding: 0 8px;
}
.bar-container {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.bar {
width: 100%;
max-width: 40px;
border-radius: 4px 4px 0 0;
transition: height 0.3s ease-out;
min-height: 4px;
}
.bar-value {
font-size: 11px;
color: var(--text-secondary);
font-weight: 600;
}
.bar-label {
font-size: 10px;
color: var(--text-muted);
text-transform: uppercase;
}
.week-progress {
margin-top: 12px;
display: flex;
align-items: center;
gap: 12px;
}
.progress-bar-bg {
flex: 1;
height: 8px;
background: var(--bg-border);
border-radius: 4px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
border-radius: 4px;
transition: width 0.3s ease-out;
}
.week-1 .progress-bar-fill { background: var(--accent1); }
.week-2 .progress-bar-fill { background: var(--accent3); }
.week-3 .progress-bar-fill { background: var(--accent5); }
.week-4 .progress-bar-fill { background: var(--accent2); }
.progress-text {
font-size: 12px;
color: var(--text-secondary);
min-width: 60px;
text-align: right;
}
/* Progress Section */
.progress-title {
font-size: 16px;
color: var(--accent1);
margin-bottom: 16px;
font-style: italic;
}
.progress-count {
font-size: 28px;
color: var(--text-main);
font-weight: 600;
margin-bottom: 16px;
}
.task-progress-list {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 20px;
}
.task-progress-item {
display: flex;
align-items: center;
gap: 10px;
}
.task-progress-name {
width: 140px;
font-size: 12px;
color: var(--text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.task-progress-bar-bg {
flex: 1;
height: 6px;
background: var(--bg-border);
border-radius: 3px;
overflow: hidden;
}
.task-progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--accent3), var(--accent1));
border-radius: 3px;
transition: width 0.3s ease-out;
}
.task-progress-percent {
width: 35px;
font-size: 11px;
color: var(--text-muted);
text-align: right;
}
.donut-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 16px 0;
}
.donut-chart {
width: 120px;
height: 120px;
border-radius: 50%;
background: conic-gradient(var(--accent1) 0deg 0deg, var(--bg-border) 0deg 360deg);
position: relative;
transition: background 0.3s ease-out;
}
.donut-inner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80px;
height: 80px;
background: var(--bg-card);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: 600;
color: var(--accent1);
}
.donut-legend {
display: flex;
gap: 16px;
font-size: 11px;
color: var(--text-secondary);
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
}
.legend-color {
width: 10px;
height: 10px;
border-radius: 2px;
}
/* Overall Progress */
.overall-progress {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid var(--bg-border);
}
.overall-title {
font-size: 14px;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 12px;
}
.overall-count {
font-size: 36px;
color: var(--accent1);
font-weight: 600;
margin-bottom: 8px;
}
.overall-percent {
font-size: 18px;
color: var(--text-muted);
margin-bottom: 12px;
}
.overall-bar-bg {
height: 10px;
background: var(--bg-border);
border-radius: 5px;
overflow: hidden;
}
.overall-bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--accent5), var(--accent1));
border-radius: 5px;
transition: width 0.3s ease-out;
}
/* Responsive */
@media (max-width: 1200px) {
.container {
grid-template-columns: 280px 1fr;
}
.progress-section {
grid-column: 1 / -1;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
}
@media (max-width: 900px) {
.container {
grid-template-columns: 1fr;
}
.schedule-section, .progress-section {
grid-column: 1;
}
.weeks-container {
overflow-x: auto;
}
.week-days, .checkbox-row {
min-width: 600px;
}
.progress-section {
grid-template-columns: 1fr;
}
}
@media (max-width: 600px) {
body { padding: 12px; }
.header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.header h1 { font-size: 28px; }
.month-selector {
justify-content: flex-start;
}
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<header class="header">
<div>
<h1>Планер</h1>
<div class="header-date" id="currentDate" title="Нажмите для редактирования">15 декабря 2025</div>
</div>
<div class="month-selector" id="monthSelector"></div>
</header>
<!-- Left: Schedule -->
<div class="schedule-section card">
<h2 class="card-title">Расписание дня</h2>
<div class="schedule-block" data-period="morning">
<h3 class="schedule-title">☀️ Утро</h3>
<ul class="task-list" id="morningTasks"></ul>
<button class="add-task-btn" onclick="addTask('morning')">+ Добавить задачу</button>
</div>
<div class="schedule-block" data-period="day" style="margin-top: 20px;">
<h3 class="schedule-title">🌤 День</h3>
<ul class="task-list" id="dayTasks"></ul>
<button class="add-task-btn" onclick="addTask('day')">+ Добавить задачу</button>
</div>
<div class="schedule-block" data-period="evening" style="margin-top: 20px;">
<h3 class="schedule-title">🌙 Вечер</h3>
<ul class="task-list" id="eveningTasks"></ul>
<button class="add-task-btn" onclick="addTask('evening')">+ Добавить задачу</button>
</div>
</div>
<!-- Center: Weeks Grid -->
<div class="weeks-container card" id="weeksContainer"></div>
<!-- Right: Progress -->
<div class="progress-section">
<div class="card">
<h2 class="progress-title">Прогресс за месяц</h2>
<div class="progress-count" id="monthCount">0 / 0</div>
<div class="task-progress-list" id="taskProgressList"></div>
<div class="donut-container">
<div class="donut-chart" id="donutChart">
<div class="donut-inner" id="donutInner">0%</div>
</div>
<div class="donut-legend">
<div class="legend-item">
<div class="legend-color" style="background: var(--accent1)"></div>
<span>Выполнено</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: var(--bg-border)"></div>
<span>Осталось</span>
</div>
</div>
</div>
<div class="overall-progress">
<h3 class="overall-title">Общий прогресс</h3>
<div class="overall-count" id="overallCount">0 / 0</div>
<div class="overall-percent" id="overallPercent">0%</div>
<div class="overall-bar-bg">
<div class="overall-bar-fill" id="overallBar" style="width: 0%"></div>
</div>
</div>
</div>
</div>
</div>
<script>
const MONTHS = [
'Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь',
'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'
];
const WEEK_COLORS = ['var(--accent1)', 'var(--accent3)', 'var(--accent5)', 'var(--accent2)'];
let currentMonth = new Date().getMonth();
let currentYear = new Date().getFullYear();
let currentDate = new Date();
let data = {
morning: [],
day: [],
evening: []
};
let checkboxes = {};
function getStorageKey(month, year) {
return `habit-tracker-${year}-${String(month).padStart(2, '0')}`;
}
function loadAllData() {
const today = new Date();
currentDate = today;
for (let m = 0; m < 12; m++) {
const key = getStorageKey(m, currentYear);
const saved = localStorage.getItem(key);
if (saved) {
const parsed = JSON.parse(saved);
if (parsed.data) data = parsed.data;
if (parsed.checkboxes) checkboxes = parsed.checkboxes;
}
}
if (Object.keys(data).length === 0 || (!data.morning.length && !data.day.length && !data.evening.length)) {
data = {
morning: [],
day: [],
evening: []
};
}
}
function saveCurrentMonth() {
const key = getStorageKey(currentMonth, currentYear);
localStorage.setItem(key, JSON.stringify({ data, checkboxes }));
}
function getDaysInMonth(month, year) {
return new Date(year, month + 1, 0).getDate();
}
function formatDate(date) {
const options = { day: 'numeric', month: 'long', year: 'numeric' };
return date.toLocaleDateString('ru-RU', options);
}
function updateDateDisplay() {
document.getElementById('currentDate').textContent = formatDate(currentDate);
}
function renderMonthSelector() {
const container = document.getElementById('monthSelector');
container.innerHTML = MONTHS.map((month, i) => `
<button class="month-btn ${i === currentMonth ? 'active' : ''}" onclick="switchMonth(${i})">
${month.slice(0, 3)}
</button>
`).join('');
}
function switchMonth(month) {
currentMonth = month;
loadAllData();
renderAll();
}
function renderSchedule() {
['morning', 'day', 'evening'].forEach(period => {
const container = document.getElementById(`${period}Tasks`);
container.innerHTML = data[period].map((task, i) => `
<li class="task-item">
<input type="text" class="task-text" value="${task}"
onchange="updateTask('${period}', ${i}, this.value)">
<button class="task-delete" onclick="deleteTask('${period}', ${i})">×</button>
</li>
`).join('');
});
}
function updateTask(period, index, value) {
data[period][index] = value;
saveCurrentMonth();
renderWeeks();
renderProgress();
}
function deleteTask(period, index) {
if (confirm('Удалить эту задачу?')) {
data[period].splice(index, 1);
saveCurrentMonth();
renderSchedule();
renderWeeks();
renderProgress();
}
}
function addTask(period) {
const taskName = prompt('Название задачи:');
if (taskName && taskName.trim()) {
data[period].push(taskName.trim());
saveCurrentMonth();
renderSchedule();
renderWeeks();
renderProgress();
}
}
function getAllTasks() {
return [...data.morning, ...data.day, ...data.evening];
}
function renderWeeks() {
const container = document.getElementById('weeksContainer');
const daysInMonth = getDaysInMonth(currentMonth, currentYear);
const tasks = getAllTasks();
let html = '';
const weeksCount = Math.ceil(daysInMonth / 7);
for (let w = 0; w < Math.min(weeksCount, 4); w++) {
const weekStart = w * 7 + 1;
const weekEnd = Math.min((w + 1) * 7, daysInMonth);
html += `<div class="week-block week-${w + 1}">`;
html += `<div class="week-header">Неделя ${w + 1}</div>`;
html += '<div class="week-days">';
html += '<div>Задача</div>';
const dayNames = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
for (let d = 0; d < 7; d++) {
const dayNum = weekStart + d;
if (dayNum <= daysInMonth) {
html += `<div>${dayNames[d]} ${dayNum}</div>`;
} else {
html += '<div></div>';
}
}
html += '</div>';
html += '<div class="checkbox-grid">';
tasks.forEach((task, taskIdx) => {
html += '<div class="checkbox-row">';
html += `<div class="task-name">${task}</div>`;
for (let d = 0; d < 7; d++) {
const dayNum = weekStart + d;
if (dayNum <= daysInMonth) {
const dayIndex = (w * 7 + d);
const checkboxKey = `${taskIdx}-${dayIndex}`;
const isChecked = checkboxes[checkboxKey] || false;
html += `
<div class="checkbox-cell">
<div class="custom-checkbox ${isChecked ? 'checked' : ''}"
onclick="toggleCheckbox('${checkboxKey}')"></div>
</div>
`;
} else {
html += '<div class="checkbox-cell"></div>';
}
}
html += '</div>';
});
html += '</div>';
html += renderChart(w, tasks.length, daysInMonth - weekStart + 1);
html += '</div>';
}
container.innerHTML = html;
}
function renderChart(weekIndex, tasksPerDay, daysToShow) {
const weekStart = weekIndex * 7;
const chartData = [];
const dayNames = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
for (let d = 0; d < 7; d++) {
const dayIndex = weekStart + d;
let count = 0;
for (let t = 0; t < tasksPerDay; t++) {
if (checkboxes[`${t}-${dayIndex}`]) count++;
}
chartData.push(count);
}
const maxVal = Math.max(...chartData, 1);
const total = chartData.reduce((a, b) => a + b, 0);
const maxPossible = tasksPerDay * 7;
const percent = maxPossible > 0 ? Math.round((total / maxPossible) * 100) : 0;
let html = '<div class="chart-block">';
html += '<div class="bar-chart">';
chartData.forEach((val, i) => {
const height = (val / maxVal) * 80 + 4;
html += `
<div class="bar-container">
<div class="bar-value">${val}</div>
<div class="bar" style="height: ${height}px; background: ${WEEK_COLORS[weekIndex]}"></div>
<div class="bar-label">${dayNames[i]}</div>
</div>
`;
});
html += '</div>';
html += `
<div class="week-progress">
<div class="progress-bar-bg">
<div class="progress-bar-fill" style="width: ${percent}%"></div>
</div>
<div class="progress-text">${total} / ${maxPossible} • ${percent}%</div>
</div>
`;
html += '</div>';
return html;
}
function toggleCheckbox(key) {
checkboxes[key] = !checkboxes[key];
saveCurrentMonth();
renderWeeks();
renderProgress();
}
function renderProgress() {
const tasks = getAllTasks();
const totalSlots = tasks.length * getDaysInMonth(currentMonth, currentYear);
let totalChecked = 0;
const taskProgressHTML = tasks.map((task, taskIdx) => {
let checked = 0;
const daysInMonth = getDaysInMonth(currentMonth, currentYear);
for (let d = 0; d < daysInMonth; d++) {
if (checkboxes[`${taskIdx}-${d}`]) checked++;
}
totalChecked += checked;
const percent = daysInMonth > 0 ? Math.round((checked / daysInMonth) * 100) : 0;
return `
<div class="task-progress-item">
<div class="task-progress-name">${task}</div>
<div class="task-progress-bar-bg">
<div class="task-progress-bar-fill" style="width: ${percent}%"></div>
</div>
<div class="task-progress-percent">${percent}%</div>
</div>
`;
}).join('');
document.getElementById('taskProgressList').innerHTML = taskProgressHTML;
document.getElementById('monthCount').textContent = `${totalChecked} / ${totalSlots}`;
const monthPercent = totalSlots > 0 ? Math.round((totalChecked / totalSlots) * 100) : 0;
const donut = document.getElementById('donutChart');
const donutInner = document.getElementById('donutInner');
const degrees = (monthPercent / 100) * 360;
donut.style.background = `conic-gradient(var(--accent1) 0deg ${degrees}deg, var(--bg-border) ${degrees}deg 360deg)`;
donutInner.textContent = `${monthPercent}%`;
document.getElementById('overallCount').textContent = `${totalChecked} / ${totalSlots}`;
document.getElementById('overallPercent').textContent = `${monthPercent}%`;
document.getElementById('overallBar').style.width = `${monthPercent}%`;
}
function renderAll() {
updateDateDisplay();
renderMonthSelector();
renderSchedule();
renderWeeks();
renderProgress();
}
document.getElementById('currentDate').addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'date';
input.value = currentDate.toISOString().split('T')[0];
input.style.background = 'var(--bg-hover)';
input.style.border = '1px solid var(--bg-border)';
input.style.color = 'var(--text-main)';
input.style.padding = '4px 8px';
input.style.borderRadius = '4px';
input.style.fontFamily = 'inherit';
input.onchange = () => {
currentDate = new Date(input.value + 'T12:00:00');
saveCurrentMonth();
updateDateDisplay();
};
const dateEl = document.getElementById('currentDate');
dateEl.innerHTML = '';
dateEl.appendChild(input);
input.focus();
});
loadAllData();
renderAll();
</script>
</body>
</html>