(и да, он будет реально работать)
Хочешь личный цифровой органайзер, который:
✅ сохраняет задачи,
✅ запоминает заметки,
✅ показывает календарь с событиями,
✅ напоминает о встречах,
✅ работает на телефоне и компьютере —
и при этом не требует подписки, облака или регистрации в десяти сервисах?
Такое возможно. И сделать его может любой — даже если ты впервые слышишь слово «localStorage».
harvi.pro — это не просто чат. Это интеллектуальный соавтор, который пишет готовый, рабочий код по твоему описанию.
Он поддерживает вход через Google, GitHub и VK — быстро, без SMS и паспортов.
Да, сервис платный, но при регистрации тебе дарят 2 000 000 токенов — этого хватит с головой, чтобы создать полноценное приложение, протестировать его и даже доработать.
А работает harvi.pro на Claude Sonnet 4.5 — одной из самых мощных и логически последовательных моделей на сегодня. Она понимает сложные требования, не путает переменные и генерирует чистый, структурированный код — даже с календарём, модальными окнами и логикой напоминаний.
Идеально для вайб-кодера, который хочет результат — без боли.
Просто вставь в harvi.pro вот такой промпт:
Создай **однофайловое HTML-приложение** — «Личный Органайзер-планировщик» — с имитацией входа и сохранением данных в `localStorage`.
**Требования:**
- Сначала экран входа: поле ввода «Твоё имя» и кнопка «Войти». После ввода — переход на главную.
- Главная страница содержит:
• Список дел (можно добавлять, отмечать как выполненное, удалять),
• Текстовую заметку (одно поле, сохраняется автоматически),
• Счётчик «Дней в потоке» (увеличивается, если приложение открывали вчера и сегодня),
- Все данные **привязаны к имени** и сохраняются между перезагрузками.
- Дизайн: профессиональный, корпоративный
- Полностью **адаптивно под мобильные** (используй `viewport`, touch-friendly кнопки).
- Всё в **одном .html-файле** (встроенные `<style>` и `<script>`).
- Код с **комментариями на русском** для новичков.
- Отображение календаря с кликабельными датами, при клике на дату открывается модальное окно, которое позволяет запланировать мероприятие: дата - время.
- Настройка, позволяющая установить время оповещения до мероприятия.
Название приложения: **«Органайзер»**
Через несколько минут ИИ выдаст полный HTML-файл — с календарём, модальными окнами, логикой напоминаний и даже счётчиком «дней в потоке».
organizer.htmlЭто твоё личное цифровое пространство. И ты создал его словами, а не строчками кода.
Не учить синтаксис.
Не бояться ошибок.
Просто описать идею — и получить рабочий инструмент.
Попробуй. У тебя получится.
Если не хочешь ждать, пока harvi напишет тебе код, просто скопируй код, вставь его в блокнот и сохрани как, например, органайзер.html Потом открой его в любом браузере и пользуйся.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<title>Органайзер — Личный планировщик</title>
<style>
/* ============================================
ГЛОБАЛЬНЫЕ СТИЛИ И ПЕРЕМЕННЫЕ
============================================ */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* CSS переменные для цветовой схемы */
:root {
--primary: #2563eb;
--primary-hover: #1d4ed8;
--primary-light: #dbeafe;
--success: #10b981;
--danger: #ef4444;
--warning: #f59e0b;
--bg-main: #ffffff;
--bg-secondary: #f8fafc;
--bg-tertiary: #f1f5f9;
--text-primary: #0f172a;
--text-secondary: #64748b;
--text-tertiary: #94a3b8;
--border: #e2e8f0;
--border-hover: #cbd5e1;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.08);
--radius: 12px;
--radius-sm: 8px;
--radius-lg: 16px;
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: var(--text-primary);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
}
/* ============================================
ЭКРАН ВХОДА
============================================ */
#loginScreen {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 20px;
}
.login-card {
background: var(--bg-main);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
padding: 48px 40px;
width: 100%;
max-width: 420px;
text-align: center;
}
.login-logo {
width: 64px;
height: 64px;
background: linear-gradient(135deg, var(--primary) 0%, #7c3aed 100%);
border-radius: var(--radius);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 24px;
font-size: 32px;
color: white;
}
.login-title {
font-size: 28px;
font-weight: 600;
margin-bottom: 8px;
color: var(--text-primary);
}
.login-subtitle {
font-size: 15px;
color: var(--text-secondary);
margin-bottom: 32px;
}
.input-group {
margin-bottom: 24px;
text-align: left;
}
.input-label {
display: block;
font-size: 14px;
font-weight: 500;
color: var(--text-secondary);
margin-bottom: 8px;
}
.input-field {
width: 100%;
padding: 14px 16px;
border: 2px solid var(--border);
border-radius: var(--radius);
font-size: 16px;
transition: var(--transition);
font-family: inherit;
background: var(--bg-main);
}
.input-field:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px var(--primary-light);
}
.btn-primary {
width: 100%;
padding: 14px 24px;
background: var(--primary);
color: white;
border: none;
border-radius: var(--radius);
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
font-family: inherit;
box-shadow: var(--shadow-sm);
}
.btn-primary:hover {
background: var(--primary-hover);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.btn-primary:active {
transform: translateY(0);
}
/* ============================================
ОСНОВНОЕ ПРИЛОЖЕНИЕ И СТРУКТУРА
============================================ */
#appScreen {
display: none;
min-height: 100vh;
padding: 20px;
}
.app-container {
max-width: 1280px;
margin: 0 auto;
}
/* Шапка приложения */
.app-header {
background: var(--bg-main);
border-radius: var(--radius-lg);
padding: 24px 28px;
margin-bottom: 20px;
box-shadow: var(--shadow);
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 16px;
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
}
.header-avatar {
width: 48px;
height: 48px;
background: linear-gradient(135deg, var(--primary) 0%, #7c3aed 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 20px;
font-weight: 600;
}
.header-info h1 {
font-size: 20px;
font-weight: 600;
margin-bottom: 2px;
}
.header-info p {
font-size: 14px;
color: var(--text-secondary);
}
.header-right {
display: flex;
align-items: center;
gap: 12px;
}
.streak-badge {
display: flex;
align-items: center;
gap: 8px;
background: var(--bg-secondary);
padding: 10px 16px;
border-radius: var(--radius);
font-size: 14px;
font-weight: 600;
color: var(--warning);
}
.btn-logout {
padding: 10px 20px;
background: var(--bg-secondary);
color: var(--text-primary);
border: none;
border-radius: var(--radius);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: var(--transition);
font-family: inherit;
}
.btn-logout:hover {
background: var(--bg-tertiary);
}
/* Сетка контента */
.content-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
/* Карточка (универсальный контейнер) */
.card {
background: var(--bg-main);
border-radius: var(--radius-lg);
padding: 24px;
box-shadow: var(--shadow);
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.card-title {
font-size: 18px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
/* ============================================
СПИСОК ДЕЛ
============================================ */
.todo-input-group {
display: flex;
gap: 8px;
margin-bottom: 20px;
}
.todo-input {
flex: 1;
padding: 12px 16px;
border: 2px solid var(--border);
border-radius: var(--radius);
font-size: 15px;
transition: var(--transition);
font-family: inherit;
}
.todo-input:focus {
outline: none;
border-color: var(--primary);
}
.btn-add {
padding: 12px 24px;
background: var(--primary);
color: white;
border: none;
border-radius: var(--radius);
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
font-family: inherit;
}
.btn-add:hover {
background: var(--primary-hover);
}
.todo-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.todo-item {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
background: var(--bg-secondary);
border-radius: var(--radius);
transition: var(--transition);
}
.todo-item:hover {
background: var(--bg-tertiary);
}
.todo-checkbox {
width: 20px;
height: 20px;
cursor: pointer;
accent-color: var(--success);
}
.todo-text {
flex: 1;
font-size: 15px;
}
.todo-text.completed {
text-decoration: line-through;
color: var(--text-tertiary);
}
.btn-delete {
padding: 6px 12px;
background: transparent;
color: var(--danger);
border: none;
border-radius: var(--radius-sm);
font-size: 14px;
cursor: pointer;
transition: var(--transition);
font-weight: 500;
}
.btn-delete:hover {
background: #fee;
}
.empty-state {
text-align: center;
padding: 32px;
color: var(--text-tertiary);
font-size: 14px;
}
/* ============================================
ЗАМЕТКА
============================================ */
.note-textarea {
width: 100%;
min-height: 200px;
padding: 16px;
border: 2px solid var(--border);
border-radius: var(--radius);
font-size: 15px;
font-family: inherit;
resize: vertical;
transition: var(--transition);
}
.note-textarea:focus {
outline: none;
border-color: var(--primary);
}
.note-status {
display: flex;
align-items: center;
gap: 6px;
margin-top: 8px;
font-size: 13px;
color: var(--text-tertiary);
}
/* ============================================
КАЛЕНДАРЬ
============================================ */
.calendar-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.calendar-month {
font-size: 16px;
font-weight: 600;
}
.calendar-nav {
display: flex;
gap: 8px;
}
.btn-nav {
width: 32px;
height: 32px;
background: var(--bg-secondary);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-primary);
}
.btn-nav:hover {
background: var(--bg-tertiary);
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px;
}
.calendar-day-header {
text-align: center;
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
padding: 8px 0;
}
.calendar-day {
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
font-size: 14px;
cursor: pointer;
transition: var(--transition);
position: relative;
background: var(--bg-secondary);
}
.calendar-day:hover {
background: var(--bg-tertiary);
}
.calendar-day.today {
background: var(--primary);
color: white;
font-weight: 600;
}
.calendar-day.other-month {
color: var(--text-tertiary);
}
.calendar-day.has-event::after {
content: '';
position: absolute;
bottom: 4px;
left: 50%;
transform: translateX(-50%);
width: 4px;
height: 4px;
background: var(--warning);
border-radius: 50%;
}
.calendar-day.today.has-event::after {
background: white;
}
/* ============================================
СПИСОК СОБЫТИЙ
============================================ */
.events-list {
display: flex;
flex-direction: column;
gap: 12px;
max-height: 300px;
overflow-y: auto;
}
.event-item {
padding: 14px 16px;
background: var(--bg-secondary);
border-radius: var(--radius);
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
}
.event-content {
flex: 1;
}
.event-title {
font-size: 15px;
font-weight: 600;
margin-bottom: 4px;
}
.event-date {
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 2px;
}
.event-desc {
font-size: 13px;
color: var(--text-tertiary);
}
.btn-delete-event {
padding: 6px 10px;
background: transparent;
color: var(--danger);
border: none;
border-radius: var(--radius-sm);
font-size: 12px;
cursor: pointer;
transition: var(--transition);
}
.btn-delete-event:hover {
background: #fee;
}
/* ============================================
НАСТРОЙКИ
============================================ */
.settings-group {
margin-bottom: 20px;
}
.settings-label {
display: block;
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
}
.settings-select {
width: 100%;
padding: 12px 16px;
border: 2px solid var(--border);
border-radius: var(--radius);
font-size: 15px;
font-family: inherit;
background: var(--bg-main);
cursor: pointer;
transition: var(--transition);
}
.settings-select:focus {
outline: none;
border-color: var(--primary);
}
.settings-info {
font-size: 13px;
color: var(--text-tertiary);
margin-top: 6px;
}
/* ============================================
МОДАЛЬНОЕ ОКНО
============================================ */
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
align-items: center;
justify-content: center;
padding: 20px;
backdrop-filter: blur(4px);
}
.modal-overlay.active {
display: flex;
}
.modal {
background: var(--bg-main);
border-radius: var(--radius-lg);
padding: 28px;
width: 100%;
max-width: 500px;
box-shadow: var(--shadow-lg);
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24px;
}
.modal-title {
font-size: 20px;
font-weight: 600;
}
.btn-close {
width: 32px;
height: 32px;
background: var(--bg-secondary);
border: none;
border-radius: 50%;
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: var(--text-secondary);
}
.btn-close:hover {
background: var(--bg-tertiary);
}
.modal-body {
display: flex;
flex-direction: column;
gap: 20px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-label {
font-size: 14px;
font-weight: 500;
}
.form-input {
padding: 12px 16px;
border: 2px solid var(--border);
border-radius: var(--radius);
font-size: 15px;
font-family: inherit;
transition: var(--transition);
}
.form-input:focus {
outline: none;
border-color: var(--primary);
}
.form-textarea {
padding: 12px 16px;
border: 2px solid var(--border);
border-radius: var(--radius);
font-size: 15px;
font-family: inherit;
resize: vertical;
min-height: 100px;
transition: var(--transition);
}
.form-textarea:focus {
outline: none;
border-color: var(--primary);
}
.modal-footer {
display: flex;
gap: 12px;
margin-top: 24px;
}
.btn-secondary {
flex: 1;
padding: 12px 24px;
background: var(--bg-secondary);
color: var(--text-primary);
border: none;
border-radius: var(--radius);
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
font-family: inherit;
}
.btn-secondary:hover {
background: var(--bg-tertiary);
}
.btn-submit {
flex: 1;
padding: 12px 24px;
background: var(--primary);
color: white;
border: none;
border-radius: var(--radius);
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: var(--transition);
font-family: inherit;
}
.btn-submit:hover {
background: var(--primary-hover);
}
/* ============================================
АДАПТИВНОСТЬ
============================================ */
@media (max-width: 768px) {
.content-grid {
grid-template-columns: 1fr;
}
.app-header {
padding: 20px;
}
.header-left {
width: 100%;
}
.header-right {
width: 100%;
justify-content: space-between;
}
.login-card {
padding: 36px 28px;
}
.card {
padding: 20px;
}
.calendar-day {
font-size: 13px;
}
.calendar-day-header {
font-size: 11px;
}
}
@media (max-width: 480px) {
body {
font-size: 15px;
}
.app-header {
padding: 16px;
}
.header-info h1 {
font-size: 18px;
}
.card {
padding: 16px;
}
.calendar-grid {
gap: 4px;
}
.modal {
padding: 24px;
}
}
/* Скролл */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: var(--border-hover);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-tertiary);
}
</style>
</head>
<body>
<!-- ============================================
ЭКРАН ВХОДА
============================================ -->
<div id="loginScreen">
<div class="login-card">
<div class="login-logo">📋</div>
<h1 class="login-title">Органайзер</h1>
<p class="login-subtitle">Ваш личный планировщик задач и событий</p>
<div class="input-group">
<label class="input-label" for="nameInput">Твоё имя</label>
<input
type="text"
id="nameInput"
class="input-field"
placeholder="Введите ваше имя"
autocomplete="off"
>
</div>
<button class="btn-primary" onclick="login()">Войти</button>
</div>
</div>
<!-- ============================================
ОСНОВНОЕ ПРИЛОЖЕНИЕ
============================================ -->
<div id="appScreen">
<div class="app-container">
<!-- Шапка -->
<header class="app-header">
<div class="header-left">
<div class="header-avatar" id="userAvatar"></div>
<div class="header-info">
<h1>Добро пожаловать, <span id="userName"></span>!</h1>
<p id="currentDate"></p>
</div>
</div>
<div class="header-right">
<div class="streak-badge">
<span>🔥</span>
<span id="streakCount">0</span>
<span>дней</span>
</div>
<button class="btn-logout" onclick="logout()">Выйти</button>
</div>
</header>
<!-- Основной контент -->
<div class="content-grid">
<!-- Список дел -->
<div class="card">
<div class="card-header">
<h2 class="card-title">
<span>✓</span>
Список дел
</h2>
</div>
<div class="todo-input-group">
<input
type="text"
id="todoInput"
class="todo-input"
placeholder="Новая задача..."
onkeypress="if(event.key === 'Enter') addTodo()"
>
<button class="btn-add" onclick="addTodo()">Добавить</button>
</div>
<div class="todo-list" id="todoList">
<div class="empty-state">Пока нет задач. Добавьте первую!</div>
</div>
</div>
<!-- Заметка -->
<div class="card">
<div class="card-header">
<h2 class="card-title">
<span>📝</span>
Заметка
</h2>
</div>
<textarea
id="noteText"
class="note-textarea"
placeholder="Начните писать..."
oninput="saveNote()"
></textarea>
<div class="note-status">
<span>💾</span>
<span>Автосохранение</span>
</div>
</div>
<!-- Календарь -->
<div class="card">
<div class="card-header">
<h2 class="card-title">
<span>📅</span>
Календарь
</h2>
</div>
<div class="calendar-header">
<div class="calendar-month" id="calendarMonth"></div>
<div class="calendar-nav">
<button class="btn-nav" onclick="prevMonth()">‹</button>
<button class="btn-nav" onclick="nextMonth()">›</button>
</div>
</div>
<div class="calendar-grid" id="calendarGrid"></div>
</div>
<!-- Ближайшие события -->
<div class="card">
<div class="card-header">
<h2 class="card-title">
<span>⏰</span>
Ближайшие события
</h2>
</div>
<div class="events-list" id="eventsList">
<div class="empty-state">Нет запланированных событий</div>
</div>
</div>
<!-- Настройки уведомлений -->
<div class="card" style="grid-column: 1 / -1;">
<div class="card-header">
<h2 class="card-title">
<span>⚙️</span>
Настройки уведомлений
</h2>
</div>
<div class="settings-group">
<label class="settings-label" for="notificationTime">
Напомнить за:
</label>
<select id="notificationTime" class="settings-select" onchange="saveNotificationSettings()">
<option value="0">В момент события</option>
<option value="5">5 минут до</option>
<option value="10">10 минут до</option>
<option value="15" selected>15 минут до</option>
<option value="30">30 минут до</option>
<option value="60">1 час до</option>
</select>
<p class="settings-info">
ℹ️ Уведомления работают только при открытом приложении
</p>
</div>
</div>
</div>
</div>
</div>
<!-- ============================================
МОДАЛЬНОЕ ОКНО ДЛЯ СОБЫТИЙ
============================================ -->
<div class="modal-overlay" id="eventModal">
<div class="modal">
<div class="modal-header">
<h2 class="modal-title">Новое событие</h2>
<button class="btn-close" onclick="closeEventModal()">×</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label" for="eventTitle">Название события</label>
<input
type="text"
id="eventTitle"
class="form-input"
placeholder="Встреча, день рождения..."
>
</div>
<div class="form-group">
<label class="form-label" for="eventDate">Дата</label>
<input
type="date"
id="eventDate"
class="form-input"
>
</div>
<div class="form-group">
<label class="form-label" for="eventTime">Время</label>
<input
type="time"
id="eventTime"
class="form-input"
>
</div>
<div class="form-group">
<label class="form-label" for="eventDesc">Описание (опционально)</label>
<textarea
id="eventDesc"
class="form-textarea"
placeholder="Добавьте детали..."
></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeEventModal()">Отмена</button>
<button class="btn-submit" onclick="saveEvent()">Сохранить</button>
</div>
</div>
</div>
<!-- ============================================
JAVASCRIPT ЛОГИКА ПРИЛОЖЕНИЯ
============================================ -->
<script>
// ========================================
// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ И КОНСТАНТЫ
// ========================================
let currentUser = null; // Текущий пользователь
let currentDate = new Date(); // Дата для календаря
let selectedDate = null; // Выбранная дата для события
// ========================================
// ИНИЦИАЛИЗАЦИЯ ПРИЛОЖЕНИЯ
// ========================================
// Запускается при загрузке страницы
window.addEventListener('DOMContentLoaded', () => {
// Проверяем, есть ли сохранённый пользователь
const savedUser = localStorage.getItem('currentUser');
if (savedUser) {
currentUser = savedUser;
showApp();
}
// Запрашиваем разрешение на уведомления
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission();
}
// Запускаем проверку событий каждую минуту
setInterval(checkUpcomingEvents, 60000);
});
// ========================================
// ВХОД И ВЫХОД
// ========================================
// Функция входа в систему
function login() {
const nameInput = document.getElementById('nameInput');
const name = nameInput.value.trim();
if (!name) {
alert('Пожалуйста, введите ваше имя');
return;
}
currentUser = name;
localStorage.setItem('currentUser', name);
// Обновляем счётчик посещений
updateStreak();
showApp();
}
// Функция выхода из системы
function logout() {
if (confirm('Вы уверены, что хотите выйти?')) {
currentUser = null;
localStorage.removeItem('currentUser');
document.getElementById('appScreen').style.display = 'none';
document.getElementById('loginScreen').style.display = 'flex';
document.getElementById('nameInput').value = '';
}
}
// Показать главный экран приложения
function showApp() {
document.getElementById('loginScreen').style.display = 'none';
document.getElementById('appScreen').style.display = 'block';
// Инициализируем интерфейс
updateUserInfo();
updateCurrentDate();
loadTodos();
loadNote();
loadNotificationSettings();
renderCalendar();
loadEvents();
checkUpcomingEvents();
}
// ========================================
// СЧЁТЧИК "ДНЕЙ В ПОТОКЕ"
// ========================================
function updateStreak() {
const key = `streak_${currentUser}`;
const today = new Date().toDateString();
const streakData = JSON.parse(localStorage.getItem(key) || '{"count": 0, "lastVisit": null}');
if (streakData.lastVisit === today) {
// Уже посещали сегодня
return;
}
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
if (streakData.lastVisit === yesterday.toDateString()) {
// Посещали вчера — увеличиваем счётчик
streakData.count++;
} else if (streakData.lastVisit !== null) {
// Был пропуск — сбрасываем
streakData.count = 1;
} else {
// Первое посещение
streakData.count = 1;
}
streakData.lastVisit = today;
localStorage.setItem(key, JSON.stringify(streakData));
// Обновляем UI
document.getElementById('streakCount').textContent = streakData.count;
}
// ========================================
// ОБНОВЛЕНИЕ ИНФОРМАЦИИ О ПОЛЬЗОВАТЕЛЕ
// ========================================
function updateUserInfo() {
const userName = document.getElementById('userName');
const userAvatar = document.getElementById('userAvatar');
userName.textContent = currentUser;
userAvatar.textContent = currentUser.charAt(0).toUpperCase();
// Загружаем счётчик
const key = `streak_${currentUser}`;
const streakData = JSON.parse(localStorage.getItem(key) || '{"count": 0}');
document.getElementById('streakCount').textContent = streakData.count;
}
function updateCurrentDate() {
const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
const dateString = new Date().toLocaleDateString('ru-RU', options);
document.getElementById('currentDate').textContent = dateString;
}
// ========================================
// СПИСОК ДЕЛ
// ========================================
function getTodosKey() {
return `todos_${currentUser}`;
}
function loadTodos() {
const todos = JSON.parse(localStorage.getItem(getTodosKey()) || '[]');
renderTodos(todos);
}
function renderTodos(todos) {
const todoList = document.getElementById('todoList');
if (todos.length === 0) {
todoList.innerHTML = '<div class="empty-state">Пока нет задач. Добавьте первую!</div>';
return;
}
todoList.innerHTML = todos.map((todo, index) => `
<div class="todo-item">
<input
type="checkbox"
class="todo-checkbox"
${todo.completed ? 'checked' : ''}
onchange="toggleTodo(${index})"
>
<span class="todo-text ${todo.completed ? 'completed' : ''}">${escapeHtml(todo.text)}</span>
<button class="btn-delete" onclick="deleteTodo(${index})">Удалить</button>
</div>
`).join('');
}
function addTodo() {
const input = document.getElementById('todoInput');
const text = input.value.trim();
if (!text) return;
const todos = JSON.parse(localStorage.getItem(getTodosKey()) || '[]');
todos.push({ text, completed: false });
localStorage.setItem(getTodosKey(), JSON.stringify(todos));
input.value = '';
renderTodos(todos);
}
function toggleTodo(index) {
const todos = JSON.parse(localStorage.getItem(getTodosKey()) || '[]');
todos[index].completed = !todos[index].completed;
localStorage.setItem(getTodosKey(), JSON.stringify(todos));
renderTodos(todos);
}
function deleteTodo(index) {
const todos = JSON.parse(localStorage.getItem(getTodosKey()) || '[]');
todos.splice(index, 1);
localStorage.setItem(getTodosKey(), JSON.stringify(todos));
renderTodos(todos);
}
// ========================================
// ЗАМЕТКА
// ========================================
function getNoteKey() {
return `note_${currentUser}`;
}
function loadNote() {
const note = localStorage.getItem(getNoteKey()) || '';
document.getElementById('noteText').value = note;
}
function saveNote() {
const text = document.getElementById('noteText').value;
localStorage.setItem(getNoteKey(), text);
}
// ========================================
// КАЛЕНДАРЬ
// ========================================
function renderCalendar() {
const grid = document.getElementById('calendarGrid');
const monthTitle = document.getElementById('calendarMonth');
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
// Название месяца
const monthName = currentDate.toLocaleDateString('ru-RU', { month: 'long', year: 'numeric' });
monthTitle.textContent = monthName.charAt(0).toUpperCase() + monthName.slice(1);
// Первый день месяца
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
// Смещение (день недели первого числа: 0 = вс, 1 = пн, ...)
let startDay = firstDay.getDay();
startDay = startDay === 0 ? 6 : startDay - 1; // Преобразуем: пн = 0
// Дни предыдущего месяца
const prevMonthDays = new Date(year, month, 0).getDate();
// Получаем события для отображения индикатора
const events = getEvents();
// Формируем HTML
let html = '';
// Заголовки дней недели
const dayNames = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
dayNames.forEach(name => {
html += `<div class="calendar-day-header">${name}</div>`;
});
// Дни предыдущего месяца
for (let i = startDay - 1; i >= 0; i--) {
const day = prevMonthDays - i;
html += `<div class="calendar-day other-month">${day}</div>`;
}
// Дни текущего месяца
const today = new Date();
for (let day = 1; day <= lastDay.getDate(); day++) {
const date = new Date(year, month, day);
const dateStr = date.toISOString().split('T')[0];
const isToday = date.toDateString() === today.toDateString();
const hasEvent = events.some(e => e.date === dateStr);
let classes = 'calendar-day';
if (isToday) classes += ' today';
if (hasEvent) classes += ' has-event';
html += `<div class="${classes}" onclick="openEventModal('${dateStr}')">${day}</div>`;
}
// Дни следующего месяца
const remainingCells = 42 - (startDay + lastDay.getDate());
for (let day = 1; day <= remainingCells; day++) {
html += `<div class="calendar-day other-month">${day}</div>`;
}
grid.innerHTML = html;
}
function prevMonth() {
currentDate.setMonth(currentDate.getMonth() - 1);
renderCalendar();
}
function nextMonth() {
currentDate.setMonth(currentDate.getMonth() + 1);
renderCalendar();
}
// ========================================
// СОБЫТИЯ
// ========================================
function getEventsKey() {
return `events_${currentUser}`;
}
function getEvents() {
return JSON.parse(localStorage.getItem(getEventsKey()) || '[]');
}
function openEventModal(dateStr) {
selectedDate = dateStr;
document.getElementById('eventDate').value = dateStr;
document.getElementById('eventModal').classList.add('active');
}
function closeEventModal() {
document.getElementById('eventModal').classList.remove('active');
document.getElementById('eventTitle').value = '';
document.getElementById('eventDate').value = '';
document.getElementById('eventTime').value = '';
document.getElementById('eventDesc').value = '';
selectedDate = null;
}
function saveEvent() {
const title = document.getElementById('eventTitle').value.trim();
const date = document.getElementById('eventDate').value;
const time = document.getElementById('eventTime').value;
const desc = document.getElementById('eventDesc').value.trim();
if (!title || !date || !time) {
alert('Пожалуйста, заполните название, дату и время события');
return;
}
const events = getEvents();
events.push({
id: Date.now(),
title,
date,
time,
desc
});
localStorage.setItem(getEventsKey(), JSON.stringify(events));
closeEventModal();
renderCalendar();
loadEvents();
}
function loadEvents() {
const events = getEvents();
const eventsList = document.getElementById('eventsList');
// Сортируем по дате и времени
events.sort((a, b) => {
const dateA = new Date(`${a.date}T${a.time}`);
const dateB = new Date(`${b.date}T${b.time}`);
return dateA - dateB;
});
// Показываем только будущие события
const now = new Date();
const upcoming = events.filter(e => {
const eventDate = new Date(`${e.date}T${e.time}`);
return eventDate >= now;
});
if (upcoming.length === 0) {
eventsList.innerHTML = '<div class="empty-state">Нет запланированных событий</div>';
return;
}
eventsList.innerHTML = upcoming.map(event => {
const eventDate = new Date(`${event.date}T${event.time}`);
const dateStr = eventDate.toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'long',
hour: '2-digit',
minute: '2-digit'
});
return `
<div class="event-item">
<div class="event-content">
<div class="event-title">${escapeHtml(event.title)}</div>
<div class="event-date">${dateStr}</div>
${event.desc ? `<div class="event-desc">${escapeHtml(event.desc)}</div>` : ''}
</div>
<button class="btn-delete-event" onclick="deleteEvent(${event.id})">×</button>
</div>
`;
}).join('');
}
function deleteEvent(id) {
if (!confirm('Удалить это событие?')) return;
const events = getEvents();
const filtered = events.filter(e => e.id !== id);
localStorage.setItem(getEventsKey(), JSON.stringify(filtered));
renderCalendar();
loadEvents();
}
// ========================================
// УВЕДОМЛЕНИЯ
// ========================================
function saveNotificationSettings() {
const minutes = document.getElementById('notificationTime').value;
localStorage.setItem(`notifications_${currentUser}`, minutes);
}
function loadNotificationSettings() {
const minutes = localStorage.getItem(`notifications_${currentUser}`) || '15';
document.getElementById('notificationTime').value = minutes;
}
function checkUpcomingEvents() {
if ('Notification' in window && Notification.permission !== 'granted') {
return;
}
const events = getEvents();
const notificationMinutes = parseInt(localStorage.getItem(`notifications_${currentUser}`) || '15');
const now = new Date();
events.forEach(event => {
const eventDate = new Date(`${event.date}T${event.time}`);
const diff = eventDate - now;
const diffMinutes = Math.floor(diff / 60000);
// Проверяем, нужно ли показать уведомление
if (diffMinutes === notificationMinutes && diffMinutes > 0) {
showNotification(event, notificationMinutes);
}
});
}
function showNotification(event, minutes) {
if ('Notification' in window && Notification.permission === 'granted') {
const title = `Напоминание: ${event.title}`;
const body = `Начало через ${minutes} минут`;
new Notification(title, {
body: body,
icon: '📅',
requireInteraction: true
});
}
}
// ========================================
// УТИЛИТЫ
// ========================================
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
</body>
</html>