Compare commits
6 Commits
346e6d0172
...
09064cc028
| Author | SHA1 | Date | |
|---|---|---|---|
| 09064cc028 | |||
| 9764976d7b | |||
| 3560b4d461 | |||
| 6f79afeb7e | |||
| 810e309db8 | |||
| e4b2be3052 |
247
MOBILE_SIDEBAR_FEATURE.md
Normal file
247
MOBILE_SIDEBAR_FEATURE.md
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
# Мобильный боковой слайдер - Документация функции
|
||||||
|
|
||||||
|
## Обзор
|
||||||
|
|
||||||
|
На мобильных устройствах добавлена новая функция **мобильного бокового слайдера**, который предоставляет доступ к календарю, поиску и тегам. Эта функция значительно улучшает пользовательский опыт на мобильных устройствах, предоставляя полный функционал ПК версии в удобной форме.
|
||||||
|
|
||||||
|
## Технические характеристики
|
||||||
|
|
||||||
|
### Точка срабатывания адаптивности
|
||||||
|
- **Мобильная версия**: ширина экрана ≤ 768px
|
||||||
|
- **ПК версия**: ширина экрана > 768px
|
||||||
|
|
||||||
|
### Компоненты слайдера
|
||||||
|
|
||||||
|
1. **Кнопка открытия** (☰)
|
||||||
|
- Позиция: фиксированная, левый верхний угол
|
||||||
|
- Z-index: 999
|
||||||
|
- Стиль: кнопка с выпуклым эффектом (box-shadow)
|
||||||
|
|
||||||
|
2. **Основной слайдер** (.mobile-sidebar)
|
||||||
|
- Позиция: фиксированная, слева
|
||||||
|
- Ширина: 280px
|
||||||
|
- Высота: 100vh (во всю высоту экрана)
|
||||||
|
- Анимация: плавное выдвижение (0.3s ease)
|
||||||
|
- Z-index: 1000
|
||||||
|
|
||||||
|
3. **Оверлей** (.mobile-sidebar-overlay)
|
||||||
|
- Позиция: фиксированная, полный экран
|
||||||
|
- Фон: полупрозрачный чёрный (rgba(0, 0, 0, 0.5))
|
||||||
|
- Z-index: 999
|
||||||
|
- Функция: закрытие слайдера при клике
|
||||||
|
|
||||||
|
4. **Кнопка закрытия**
|
||||||
|
- Позиция: верхний правый угол слайдера
|
||||||
|
- Размер: 40x40px
|
||||||
|
- Иконка: mdi:close
|
||||||
|
|
||||||
|
### Содержимое слайдера
|
||||||
|
|
||||||
|
#### 1. Календарь (renderCalendarMobile)
|
||||||
|
```javascript
|
||||||
|
- 42 дня (6 недель x 7 дней)
|
||||||
|
- Полная навигация по месяцам
|
||||||
|
- Выделение сегодняшнего дня (синий цвет)
|
||||||
|
- Выделение выбранного дня (зелёный цвет)
|
||||||
|
- Индикатор дней с заметками (зелёная точка)
|
||||||
|
- Дни соседних месяцев (серый цвет, меньший размер)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Поле поиска
|
||||||
|
```javascript
|
||||||
|
- Синхронизировано с основным поиском
|
||||||
|
- Задержка 300ms перед поиском (оптимизация)
|
||||||
|
- Кнопка очистки (✕)
|
||||||
|
- Подержка Escape для сброса
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Теги (renderTagsMobile)
|
||||||
|
```javascript
|
||||||
|
- Все уникальные теги из заметок
|
||||||
|
- Показывает количество заметок для каждого тега
|
||||||
|
- Визуальное выделение активного тега
|
||||||
|
- Полная функциональность фильтрации
|
||||||
|
```
|
||||||
|
|
||||||
|
## Функциональность
|
||||||
|
|
||||||
|
### Управление слайдером
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Открытие слайдера
|
||||||
|
mobileMenuBtn.addEventListener("click", () => {
|
||||||
|
mobileSidebar.classList.add("open");
|
||||||
|
sidebarOverlay.classList.add("open");
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Закрытие слайдера
|
||||||
|
sidebarCloseBtn.addEventListener("click", () => {
|
||||||
|
mobileSidebar.classList.remove("open");
|
||||||
|
sidebarOverlay.classList.remove("open");
|
||||||
|
document.body.style.overflow = "auto";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Закрытие при клике на оверлей
|
||||||
|
sidebarOverlay.addEventListener("click", () => {
|
||||||
|
mobileSidebar.classList.remove("open");
|
||||||
|
sidebarOverlay.classList.remove("open");
|
||||||
|
document.body.style.overflow = "auto";
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Синхронизация
|
||||||
|
|
||||||
|
Все действия в мобильном слайдере автоматически синхронизируются с ПК версией:
|
||||||
|
|
||||||
|
1. **Выбор даты в календаре**
|
||||||
|
- Срабатывает `handleDayClickMobile()`
|
||||||
|
- Перерисовываются оба календаря
|
||||||
|
- Обновляются теги и основной календарь
|
||||||
|
|
||||||
|
2. **Ввод текста в поиск**
|
||||||
|
- Синхронизируется с основным полем поиска (#searchInput)
|
||||||
|
- Вызывает `searchNotes(query)`
|
||||||
|
- Обновляет индикатор фильтра
|
||||||
|
|
||||||
|
3. **Клик на тег**
|
||||||
|
- Срабатывает `handleTagClickMobile()`
|
||||||
|
- Синхронизируется с основными тегами
|
||||||
|
- Обновляет оба календаря
|
||||||
|
|
||||||
|
## Новые функции в app.js
|
||||||
|
|
||||||
|
### renderCalendarMobile()
|
||||||
|
Отображает календарь в мобильном слайдере. Использует тот же алгоритм, что и основной календарь.
|
||||||
|
|
||||||
|
### handleDayClickMobile()
|
||||||
|
Обработчик клика на день в мобильном календаре. Обновляет фильтр и перерисовывает все компоненты.
|
||||||
|
|
||||||
|
### renderTagsMobile()
|
||||||
|
Отображает теги в мобильном слайдере. Синхронизируется с основными тегами.
|
||||||
|
|
||||||
|
### handleTagClickMobile()
|
||||||
|
Обработчик клика на тег в мобильном слайдере. Обновляет фильтр и перерисовывает компоненты.
|
||||||
|
|
||||||
|
### initSearchMobile()
|
||||||
|
Инициализирует поле поиска в мобильном слайдере. Синхронизируется с основным поиском.
|
||||||
|
|
||||||
|
## Стили в style.css
|
||||||
|
|
||||||
|
### Ключевые CSS классы
|
||||||
|
|
||||||
|
```css
|
||||||
|
.mobile-menu-btn /* Кнопка открытия меню */
|
||||||
|
.mobile-sidebar /* Основной контейнер слайдера */
|
||||||
|
.mobile-sidebar.open /* Состояние открытого слайдера */
|
||||||
|
.sidebar-close-btn /* Кнопка закрытия */
|
||||||
|
.sidebar-content /* Контейнер содержимого */
|
||||||
|
.mobile-sidebar-overlay /* Оверлей */
|
||||||
|
.mobile-sidebar-overlay.open /* Состояние открытого оверлея */
|
||||||
|
```
|
||||||
|
|
||||||
|
### Медиа-запрос
|
||||||
|
|
||||||
|
```css
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.mobile-menu-btn {
|
||||||
|
display: flex; /* Показываем кнопку меню */
|
||||||
|
}
|
||||||
|
.container-leftside {
|
||||||
|
display: none !important; /* Скрываем ПК версию панели */
|
||||||
|
}
|
||||||
|
/* Дополнительные адаптации */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Преимущества реализации
|
||||||
|
|
||||||
|
✅ **Полная функциональность** - все функции как в ПК версии
|
||||||
|
✅ **Синхронизация** - изменения в слайдере сразу отражаются везде
|
||||||
|
✅ **Удобство** - легко открыть/закрыть
|
||||||
|
✅ **Экономия места** - не занимает постоянное место на экране
|
||||||
|
✅ **Хороший UX** - оверлей и плавные анимации
|
||||||
|
✅ **Производительность** - минимальные затраты на отрисовку
|
||||||
|
✅ **Доступность** - полная поддержка клавиатуры (Escape)
|
||||||
|
|
||||||
|
## Тестирование
|
||||||
|
|
||||||
|
### Чек-лист тестирования
|
||||||
|
|
||||||
|
- [x] Кнопка меню появляется на экранах ≤ 768px
|
||||||
|
- [x] Кнопка меню исчезает на экранах > 768px
|
||||||
|
- [x] Слайдер открывается при нажатии кнопки
|
||||||
|
- [x] Слайдер закрывается кнопкой ✕
|
||||||
|
- [x] Слайдер закрывается кликом на оверлей
|
||||||
|
- [x] Календарь отображается правильно
|
||||||
|
- [x] Навигация по месяцам работает в обе стороны
|
||||||
|
- [x] День выбирается и фильтрует заметки
|
||||||
|
- [x] Выбранный день синхронизируется между слайдером и основным календарем
|
||||||
|
- [x] Поле поиска синхронизируется
|
||||||
|
- [x] Теги отображаются правильно
|
||||||
|
- [x] Клик на тег применяет фильтр
|
||||||
|
- [x] Все фильтры работают одновременно
|
||||||
|
|
||||||
|
## Браузеры и поддержка
|
||||||
|
|
||||||
|
- ✅ Chrome / Chromium
|
||||||
|
- ✅ Firefox
|
||||||
|
- ✅ Safari
|
||||||
|
- ✅ Edge
|
||||||
|
- ✅ Все современные мобильные браузеры
|
||||||
|
|
||||||
|
## Примеры кода
|
||||||
|
|
||||||
|
### Пример 1: Открытие/закрытие слайдера
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// HTML
|
||||||
|
<div class="mobile-menu-btn" id="mobileMenuBtn">
|
||||||
|
<span class="iconify" data-icon="mdi:menu"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// JavaScript
|
||||||
|
document.getElementById("mobileMenuBtn").addEventListener("click", function () {
|
||||||
|
document.getElementById("mobileSidebar").classList.add("open");
|
||||||
|
document.getElementById("sidebarOverlay").classList.add("open");
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Пример 2: Синхронизация поиска
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// При вводе в мобильное поле
|
||||||
|
searchInputMobile.addEventListener("input", function () {
|
||||||
|
const query = this.value;
|
||||||
|
searchNotes(query);
|
||||||
|
|
||||||
|
// Синхронизируем с основным полем
|
||||||
|
document.getElementById("searchInput").value = query;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Пример 3: Синхронизация календарей
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// При нажатии на месяц в мобильном календаре
|
||||||
|
prevMonthBtnMobile.addEventListener("click", function () {
|
||||||
|
currentDate.setMonth(currentDate.getMonth() - 1);
|
||||||
|
renderCalendar(); // Обновляем основной календарь
|
||||||
|
renderCalendarMobile(); // Обновляем мобильный
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Возможные улучшения в будущем
|
||||||
|
|
||||||
|
1. 🎯 Анимация свайпа (жесты) для открытия/закрытия
|
||||||
|
2. 🎨 Темная тема (dark mode) для слайдера
|
||||||
|
3. 📊 Статистика по тегам в слайдере
|
||||||
|
4. 🔔 Уведомления о днях с заметками
|
||||||
|
5. ⌨️ Горячие клавиши для быстрого открытия слайдера
|
||||||
|
6. 📍 Сохранение позиции прокрутки при открытии/закрытии
|
||||||
|
7. 🎭 Переходы и анимации при перелистывании месяцев
|
||||||
|
|
||||||
|
## Заключение
|
||||||
|
|
||||||
|
Мобильный боковой слайдер - это полнофункциональное решение для мобильной версии приложения NoteJS, которое предоставляет удобный доступ ко всем инструментам фильтрации и поиска, сохраняя при этом максимум пространства на экране для просмотра заметок.
|
||||||
28
README.md
28
README.md
@ -15,6 +15,8 @@
|
|||||||
- 🏷️ **Система тегов с автоматическим извлечением из заметок** (NEW!)
|
- 🏷️ **Система тегов с автоматическим извлечением из заметок** (NEW!)
|
||||||
- 🔍 **Поиск по заметкам с подсветкой результатов** (NEW!)
|
- 🔍 **Поиск по заметкам с подсветкой результатов** (NEW!)
|
||||||
- 📅 **Мини-календарь для навигации по датам заметок** (NEW!)
|
- 📅 **Мини-календарь для навигации по датам заметок** (NEW!)
|
||||||
|
- 📱 **Адаптивный дизайн с боковым слайдером на мобильных** (NEW!)
|
||||||
|
- ☰ **Мобильный боковой слайдер с календарём, поиском и тегами** (NEW!)
|
||||||
- 🎨 Простой и интуитивный интерфейс
|
- 🎨 Простой и интуитивный интерфейс
|
||||||
- 📱 Адаптивный дизайн
|
- 📱 Адаптивный дизайн
|
||||||
|
|
||||||
@ -128,6 +130,32 @@ npm start
|
|||||||
|
|
||||||
Нажмите кнопку "🚪 Выйти" в верхней части страницы заметок
|
Нажмите кнопку "🚪 Выйти" в верхней части страницы заметок
|
||||||
|
|
||||||
|
## Мобильная версия
|
||||||
|
|
||||||
|
### Боковой слайдер на мобильных устройствах
|
||||||
|
|
||||||
|
На мобильных устройствах (ширина экрана до 768 пикселей) вместо постоянной левой панели появляется удобный **боковой слайдер** с кнопкой "☰" в левом верхнем углу.
|
||||||
|
|
||||||
|
**Функциональность мобильного слайдера:**
|
||||||
|
|
||||||
|
1. **Кнопка открытия** - нажмите на кнопку "☰" в левом верхнем углу экрана
|
||||||
|
2. **Содержимое слайдера** включает все основные инструменты:
|
||||||
|
- 📅 **Календарь** - полностью функциональный календарь месяца с навигацией
|
||||||
|
- 🔍 **Поле поиска** - синхронизировано с основным полем поиска
|
||||||
|
- 🏷️ **Теги** - все теги с количеством заметок
|
||||||
|
3. **Закрытие слайдера**:
|
||||||
|
- Нажмите кнопку ✕ в верхнем правом углу слайдера
|
||||||
|
- Нажмите на серую область (оверлей) справа
|
||||||
|
4. **Синхронизация** - изменения в слайдере (выбор даты, ввод текста, клик на тег) автоматически синхронизируются с основной ПК версией
|
||||||
|
|
||||||
|
**Преимущества мобильного слайдера:**
|
||||||
|
|
||||||
|
- ✅ Полная функциональность как в ПК версии
|
||||||
|
- ✅ Экономия места на экране мобильного устройства
|
||||||
|
- ✅ Легкое открытие/закрытие
|
||||||
|
- ✅ Удобная навигация по заметкам
|
||||||
|
- ✅ Не блокирует контент при открытии (использует оверлей)
|
||||||
|
|
||||||
## Структура проекта
|
## Структура проекта
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
66
SESSION_PERSISTENCE_UPDATE.md
Normal file
66
SESSION_PERSISTENCE_UPDATE.md
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
# Обновление системы аутентификации для сохранения сессии при перезагрузке сервера
|
||||||
|
|
||||||
|
## Проблема
|
||||||
|
При перезагрузке сервера пользователи разлогинивались, так как сессии хранились только в памяти.
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
Реализована двухуровневая система аутентификации:
|
||||||
|
|
||||||
|
### 1. Клиентская часть (localStorage)
|
||||||
|
- **Файлы изменены:**
|
||||||
|
- `public/login.js` - сохранение состояния аутентификации при входе
|
||||||
|
- `public/register.js` - сохранение состояния аутентификации при регистрации
|
||||||
|
- `public/app.js` - проверка аутентификации при загрузке страницы заметок
|
||||||
|
- `public/profile.js` - проверка аутентификации при загрузке профиля
|
||||||
|
|
||||||
|
- **Функциональность:**
|
||||||
|
- Сохранение флага `isAuthenticated` и `username` в localStorage
|
||||||
|
- Проверка аутентификации при загрузке защищенных страниц
|
||||||
|
- Очистка localStorage при выходе
|
||||||
|
- Автоматическое перенаправление неавторизованных пользователей
|
||||||
|
|
||||||
|
### 2. Серверная часть (SQLite)
|
||||||
|
- **Файлы изменены:**
|
||||||
|
- `server.js` - добавлено хранение сессий в SQLite
|
||||||
|
|
||||||
|
- **Новые зависимости:**
|
||||||
|
- `connect-sqlite3` - для хранения сессий в базе данных
|
||||||
|
|
||||||
|
- **Функциональность:**
|
||||||
|
- Сессии теперь сохраняются в файле `sessions.db`
|
||||||
|
- Время жизни сессии: 7 дней
|
||||||
|
- Новый API endpoint `/api/auth/status` для проверки статуса аутентификации
|
||||||
|
|
||||||
|
## Как это работает
|
||||||
|
|
||||||
|
1. **При входе/регистрации:**
|
||||||
|
- Сервер создает сессию и сохраняет её в SQLite
|
||||||
|
- Клиент сохраняет флаг аутентификации в localStorage
|
||||||
|
|
||||||
|
2. **При загрузке страницы:**
|
||||||
|
- Клиент проверяет localStorage
|
||||||
|
- Если пользователь "авторизован", проверяется серверная сессия
|
||||||
|
- Если серверная сессия действительна, пользователь остается авторизованным
|
||||||
|
|
||||||
|
3. **При перезагрузке сервера:**
|
||||||
|
- Сессии восстанавливаются из SQLite
|
||||||
|
- Пользователи остаются авторизованными
|
||||||
|
|
||||||
|
4. **При выходе:**
|
||||||
|
- Серверная сессия удаляется
|
||||||
|
- localStorage очищается
|
||||||
|
|
||||||
|
## Безопасность
|
||||||
|
- Сессии имеют ограниченное время жизни (7 дней)
|
||||||
|
- Проверка аутентификации происходит как на клиенте, так и на сервере
|
||||||
|
- При недействительной серверной сессии пользователь автоматически разлогинивается
|
||||||
|
|
||||||
|
## Тестирование
|
||||||
|
1. Войдите в систему через браузер
|
||||||
|
2. Перезагрузите сервер (Ctrl+C и снова `node server.js`)
|
||||||
|
3. Обновите страницу в браузере
|
||||||
|
4. Пользователь должен остаться авторизованным
|
||||||
|
|
||||||
|
## Файлы базы данных
|
||||||
|
- `notes.db` - основная база данных приложения
|
||||||
|
- `sessions.db` - база данных сессий (создается автоматически)
|
||||||
98
package-lock.json
generated
98
package-lock.json
generated
@ -18,6 +18,7 @@
|
|||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"body-parser": "^2.2.0",
|
"body-parser": "^2.2.0",
|
||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
|
"connect-sqlite3": "^0.9.16",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
@ -26,6 +27,7 @@
|
|||||||
"helmet": "^8.1.0",
|
"helmet": "^8.1.0",
|
||||||
"marked": "^16.4.0",
|
"marked": "^16.4.0",
|
||||||
"multer": "^2.0.0-rc.4",
|
"multer": "^2.0.0-rc.4",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
"sqlite3": "^5.1.7"
|
"sqlite3": "^5.1.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -1168,6 +1170,17 @@
|
|||||||
"typedarray": "^0.0.6"
|
"typedarray": "^0.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/connect-sqlite3": {
|
||||||
|
"version": "0.9.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/connect-sqlite3/-/connect-sqlite3-0.9.16.tgz",
|
||||||
|
"integrity": "sha512-2gqo0QmcBBL8p8+eqpBETn7RgM/PaoKvpQGl8PfjEgwlr0VuMYNMxRJRrRCo3KR3fxMYeSsCw2tGNG0JKN9Nvg==",
|
||||||
|
"dependencies": {
|
||||||
|
"sqlite3": "^5.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/console-control-strings": {
|
"node_modules/console-control-strings": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
|
||||||
@ -1226,6 +1239,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="
|
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="
|
||||||
},
|
},
|
||||||
|
"node_modules/data-uri-to-buffer": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
@ -1503,6 +1524,28 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||||
},
|
},
|
||||||
|
"node_modules/fetch-blob": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/jimmywarting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paypal",
|
||||||
|
"url": "https://paypal.me/jimmywarting"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"node-domexception": "^1.0.0",
|
||||||
|
"web-streams-polyfill": "^3.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20 || >= 14.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/file-uri-to-path": {
|
"node_modules/file-uri-to-path": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||||
@ -1536,6 +1579,17 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/formdata-polyfill": {
|
||||||
|
"version": "4.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||||
|
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||||
|
"dependencies": {
|
||||||
|
"fetch-blob": "^3.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/forwarded": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@ -2322,6 +2376,42 @@
|
|||||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="
|
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/node-domexception": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||||
|
"deprecated": "Use your platform's native DOMException instead",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/jimmywarting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://paypal.me/jimmywarting"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-fetch": {
|
||||||
|
"version": "3.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
||||||
|
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
||||||
|
"dependencies": {
|
||||||
|
"data-uri-to-buffer": "^4.0.0",
|
||||||
|
"fetch-blob": "^3.1.4",
|
||||||
|
"formdata-polyfill": "^4.0.10"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/node-fetch"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-gyp": {
|
"node_modules/node-gyp": {
|
||||||
"version": "8.4.1",
|
"version": "8.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz",
|
||||||
@ -3254,6 +3344,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
|
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/web-streams-polyfill": {
|
||||||
|
"version": "3.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||||
|
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|||||||
@ -24,6 +24,7 @@
|
|||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"body-parser": "^2.2.0",
|
"body-parser": "^2.2.0",
|
||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
|
"connect-sqlite3": "^0.9.16",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
@ -32,6 +33,7 @@
|
|||||||
"helmet": "^8.1.0",
|
"helmet": "^8.1.0",
|
||||||
"marked": "^16.4.0",
|
"marked": "^16.4.0",
|
||||||
"multer": "^2.0.0-rc.4",
|
"multer": "^2.0.0-rc.4",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
"sqlite3": "^5.1.7"
|
"sqlite3": "^5.1.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
420
public/app.js
420
public/app.js
@ -320,6 +320,8 @@ async function loadNotes() {
|
|||||||
renderNotes(notes);
|
renderNotes(notes);
|
||||||
renderCalendar(); // Обновляем календарь после загрузки заметок
|
renderCalendar(); // Обновляем календарь после загрузки заметок
|
||||||
renderTags(); // Обновляем теги после загрузки заметок
|
renderTags(); // Обновляем теги после загрузки заметок
|
||||||
|
renderCalendarMobile(); // Обновляем мобильный календарь после загрузки заметок
|
||||||
|
renderTagsMobile(); // Обновляем мобильные теги после загрузки заметок
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Ошибка:", error);
|
console.error("Ошибка:", error);
|
||||||
notesList.innerHTML = "<p>Ошибка загрузки заметок</p>";
|
notesList.innerHTML = "<p>Ошибка загрузки заметок</p>";
|
||||||
@ -718,11 +720,65 @@ noteInput.addEventListener("keydown", function (event) {
|
|||||||
|
|
||||||
// Загружаем заметки при загрузке страницы
|
// Загружаем заметки при загрузке страницы
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
// Проверяем аутентификацию при загрузке страницы
|
||||||
|
checkAuthentication();
|
||||||
loadUserInfo();
|
loadUserInfo();
|
||||||
loadNotes();
|
loadNotes();
|
||||||
updateFilterIndicator();
|
updateFilterIndicator();
|
||||||
|
|
||||||
|
// Добавляем обработчик для кнопки выхода
|
||||||
|
setupLogoutHandler();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Функция для настройки обработчика выхода
|
||||||
|
function setupLogoutHandler() {
|
||||||
|
const logoutForms = document.querySelectorAll('form[action="/logout"]');
|
||||||
|
logoutForms.forEach(form => {
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
// Очищаем localStorage перед выходом
|
||||||
|
localStorage.removeItem('isAuthenticated');
|
||||||
|
localStorage.removeItem('username');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для проверки аутентификации
|
||||||
|
async function checkAuthentication() {
|
||||||
|
const isAuthenticated = localStorage.getItem('isAuthenticated');
|
||||||
|
|
||||||
|
if (isAuthenticated !== 'true') {
|
||||||
|
// Если пользователь не аутентифицирован, перенаправляем на страницу входа
|
||||||
|
window.location.href = "/";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что сессия на сервере еще действительна
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/auth/status");
|
||||||
|
if (!response.ok) {
|
||||||
|
// Если сессия недействительна, очищаем localStorage и перенаправляем
|
||||||
|
localStorage.removeItem('isAuthenticated');
|
||||||
|
localStorage.removeItem('username');
|
||||||
|
window.location.href = "/";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authData = await response.json();
|
||||||
|
if (!authData.authenticated) {
|
||||||
|
// Если сервер говорит, что пользователь не аутентифицирован
|
||||||
|
localStorage.removeItem('isAuthenticated');
|
||||||
|
localStorage.removeItem('username');
|
||||||
|
window.location.href = "/";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Ошибка проверки аутентификации:", error);
|
||||||
|
// В случае ошибки сети, оставляем пользователя на странице
|
||||||
|
// но показываем предупреждение
|
||||||
|
console.warn("Не удалось проверить статус аутентификации");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Функция для загрузки информации о пользователе
|
// Функция для загрузки информации о пользователе
|
||||||
async function loadUserInfo() {
|
async function loadUserInfo() {
|
||||||
try {
|
try {
|
||||||
@ -1062,3 +1118,367 @@ function initSearch() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== МОБИЛЬНЫЙ СЛАЙДЕР ====================
|
||||||
|
|
||||||
|
// Функция для отображения календаря в мобильном слайдере
|
||||||
|
function renderCalendarMobile() {
|
||||||
|
const calendarDays = document.getElementById("calendarDaysMobile");
|
||||||
|
const monthYear = document.getElementById("monthYearMobile");
|
||||||
|
|
||||||
|
// Проверяем, существуют ли элементы календаря для мобильной версии
|
||||||
|
if (!calendarDays || !monthYear) return;
|
||||||
|
|
||||||
|
const year = currentDate.getFullYear();
|
||||||
|
const month = currentDate.getMonth();
|
||||||
|
|
||||||
|
// Массив названий месяцев
|
||||||
|
const monthNames = [
|
||||||
|
"Январь",
|
||||||
|
"Февраль",
|
||||||
|
"Март",
|
||||||
|
"Апрель",
|
||||||
|
"Май",
|
||||||
|
"Июнь",
|
||||||
|
"Июль",
|
||||||
|
"Август",
|
||||||
|
"Сентябрь",
|
||||||
|
"Октябрь",
|
||||||
|
"Ноябрь",
|
||||||
|
"Декабрь",
|
||||||
|
];
|
||||||
|
|
||||||
|
// Устанавливаем заголовок месяца и года
|
||||||
|
monthYear.textContent = `${monthNames[month]} ${year}`;
|
||||||
|
|
||||||
|
// Получаем первый день месяца
|
||||||
|
const firstDay = new Date(year, month, 1);
|
||||||
|
// Получаем последний день месяца
|
||||||
|
const lastDay = new Date(year, month + 1, 0);
|
||||||
|
// Получаем день недели первого дня (0 - воскресенье, 1 - понедельник и т.д.)
|
||||||
|
let firstDayOfWeek = firstDay.getDay();
|
||||||
|
// Преобразуем так, чтобы понедельник был первым днем (0)
|
||||||
|
firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1;
|
||||||
|
|
||||||
|
// Очищаем календарь
|
||||||
|
calendarDays.innerHTML = "";
|
||||||
|
|
||||||
|
// Создаём Set дат, когда были созданы заметки
|
||||||
|
const noteDates = new Set();
|
||||||
|
allNotes.forEach((note) => {
|
||||||
|
noteDates.add(note.date);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Получаем последний день предыдущего месяца
|
||||||
|
const prevMonthLastDay = new Date(year, month, 0).getDate();
|
||||||
|
const prevMonth = month === 0 ? 11 : month - 1;
|
||||||
|
const prevYear = month === 0 ? year - 1 : year;
|
||||||
|
|
||||||
|
// Добавляем дни предыдущего месяца
|
||||||
|
for (let i = firstDayOfWeek - 1; i >= 0; i--) {
|
||||||
|
const day = prevMonthLastDay - i;
|
||||||
|
const dateStr = `${String(day).padStart(2, "0")}.${String(
|
||||||
|
prevMonth + 1
|
||||||
|
).padStart(2, "0")}.${prevYear}`;
|
||||||
|
|
||||||
|
const dayDiv = document.createElement("div");
|
||||||
|
dayDiv.classList.add("calendar-day", "other-month");
|
||||||
|
dayDiv.textContent = day;
|
||||||
|
dayDiv.dataset.date = dateStr;
|
||||||
|
|
||||||
|
// Проверяем, есть ли заметки на этот день
|
||||||
|
if (noteDates.has(dateStr)) {
|
||||||
|
dayDiv.classList.add("has-notes");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, выбран ли этот день
|
||||||
|
if (selectedDateFilter === dateStr) {
|
||||||
|
dayDiv.classList.add("selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем обработчик клика
|
||||||
|
dayDiv.addEventListener("click", handleDayClickMobile);
|
||||||
|
|
||||||
|
calendarDays.appendChild(dayDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем дни текущего месяца
|
||||||
|
const today = new Date();
|
||||||
|
for (let day = 1; day <= lastDay.getDate(); day++) {
|
||||||
|
const dateStr = `${String(day).padStart(2, "0")}.${String(
|
||||||
|
month + 1
|
||||||
|
).padStart(2, "0")}.${year}`;
|
||||||
|
|
||||||
|
const dayDiv = document.createElement("div");
|
||||||
|
dayDiv.classList.add("calendar-day");
|
||||||
|
dayDiv.textContent = day;
|
||||||
|
dayDiv.dataset.date = dateStr;
|
||||||
|
|
||||||
|
// Проверяем, является ли день сегодняшним
|
||||||
|
if (
|
||||||
|
day === today.getDate() &&
|
||||||
|
month === today.getMonth() &&
|
||||||
|
year === today.getFullYear()
|
||||||
|
) {
|
||||||
|
dayDiv.classList.add("today");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, есть ли заметки на этот день
|
||||||
|
if (noteDates.has(dateStr)) {
|
||||||
|
dayDiv.classList.add("has-notes");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, выбран ли этот день
|
||||||
|
if (selectedDateFilter === dateStr) {
|
||||||
|
dayDiv.classList.add("selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем обработчик клика
|
||||||
|
dayDiv.addEventListener("click", handleDayClickMobile);
|
||||||
|
|
||||||
|
calendarDays.appendChild(dayDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем дни следующего месяца
|
||||||
|
const totalCells = calendarDays.children.length;
|
||||||
|
const remainingCells = 42 - totalCells; // 6 недель по 7 дней
|
||||||
|
const nextMonth = month === 11 ? 0 : month + 1;
|
||||||
|
const nextYear = month === 11 ? year + 1 : year;
|
||||||
|
|
||||||
|
for (let day = 1; day <= remainingCells; day++) {
|
||||||
|
const dateStr = `${String(day).padStart(2, "0")}.${String(
|
||||||
|
nextMonth + 1
|
||||||
|
).padStart(2, "0")}.${nextYear}`;
|
||||||
|
|
||||||
|
const dayDiv = document.createElement("div");
|
||||||
|
dayDiv.classList.add("calendar-day", "other-month");
|
||||||
|
dayDiv.textContent = day;
|
||||||
|
dayDiv.dataset.date = dateStr;
|
||||||
|
|
||||||
|
// Проверяем, есть ли заметки на этот день
|
||||||
|
if (noteDates.has(dateStr)) {
|
||||||
|
dayDiv.classList.add("has-notes");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, выбран ли этот день
|
||||||
|
if (selectedDateFilter === dateStr) {
|
||||||
|
dayDiv.classList.add("selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем обработчик клика
|
||||||
|
dayDiv.addEventListener("click", handleDayClickMobile);
|
||||||
|
|
||||||
|
calendarDays.appendChild(dayDiv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработчик клика на день в календаре для мобильной версии
|
||||||
|
function handleDayClickMobile(event) {
|
||||||
|
const clickedDate = event.target.dataset.date;
|
||||||
|
|
||||||
|
// Если кликнули на тот же день, снимаем фильтр
|
||||||
|
if (selectedDateFilter === clickedDate) {
|
||||||
|
selectedDateFilter = null;
|
||||||
|
} else {
|
||||||
|
selectedDateFilter = clickedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Перерисовываем заметки, оба календаря и теги
|
||||||
|
renderNotes(allNotes);
|
||||||
|
renderCalendar();
|
||||||
|
renderCalendarMobile();
|
||||||
|
renderTags();
|
||||||
|
renderTagsMobile();
|
||||||
|
updateFilterIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для отображения тегов в мобильном слайдере
|
||||||
|
function renderTagsMobile() {
|
||||||
|
const tagsContainer = document.getElementById("tagsContainerMobile");
|
||||||
|
if (!tagsContainer) return;
|
||||||
|
|
||||||
|
const tagCounts = getAllTags(allNotes);
|
||||||
|
const sortedTags = Object.keys(tagCounts).sort();
|
||||||
|
|
||||||
|
if (sortedTags.length === 0) {
|
||||||
|
tagsContainer.innerHTML =
|
||||||
|
'<div style="font-size: 10px; color: #999; text-align: center;">Нет тегов</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tagsContainer.innerHTML = sortedTags
|
||||||
|
.map((tag) => {
|
||||||
|
const count = tagCounts[tag];
|
||||||
|
const isActive = selectedTagFilter === tag ? "active" : "";
|
||||||
|
return `<span class="tag ${isActive}" data-tag="${tag}">#${tag}<span class="tag-count">${count}</span></span>`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
// Добавляем обработчики кликов для тегов
|
||||||
|
tagsContainer.querySelectorAll(".tag").forEach((tagElement) => {
|
||||||
|
tagElement.addEventListener("click", handleTagClickMobile);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработчик клика на тег в мобильном слайдере
|
||||||
|
function handleTagClickMobile(event) {
|
||||||
|
const clickedTag = event.target.closest(".tag").dataset.tag;
|
||||||
|
|
||||||
|
// Если кликнули на тот же тег, снимаем фильтр
|
||||||
|
if (selectedTagFilter === clickedTag) {
|
||||||
|
selectedTagFilter = null;
|
||||||
|
} else {
|
||||||
|
selectedTagFilter = clickedTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Перерисовываем заметки, теги и оба календаря
|
||||||
|
renderNotes(allNotes);
|
||||||
|
renderTags();
|
||||||
|
renderTagsMobile();
|
||||||
|
renderCalendar();
|
||||||
|
renderCalendarMobile();
|
||||||
|
updateFilterIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализация мобильного слайдера
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
const mobileMenuBtn = document.getElementById("mobileMenuBtn");
|
||||||
|
const sidebarCloseBtn = document.getElementById("sidebarCloseBtn");
|
||||||
|
const mobileSidebar = document.getElementById("mobileSidebar");
|
||||||
|
const sidebarOverlay = document.getElementById("sidebarOverlay");
|
||||||
|
|
||||||
|
// Открытие слайдера при клике на кнопку меню
|
||||||
|
if (mobileMenuBtn) {
|
||||||
|
mobileMenuBtn.addEventListener("click", function () {
|
||||||
|
mobileSidebar.classList.add("open");
|
||||||
|
sidebarOverlay.classList.add("open");
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Закрытие слайдера при клике на кнопку закрытия
|
||||||
|
if (sidebarCloseBtn) {
|
||||||
|
sidebarCloseBtn.addEventListener("click", function () {
|
||||||
|
mobileSidebar.classList.remove("open");
|
||||||
|
sidebarOverlay.classList.remove("open");
|
||||||
|
document.body.style.overflow = "auto";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Закрытие слайдера при клике на оверлей
|
||||||
|
if (sidebarOverlay) {
|
||||||
|
sidebarOverlay.addEventListener("click", function () {
|
||||||
|
mobileSidebar.classList.remove("open");
|
||||||
|
sidebarOverlay.classList.remove("open");
|
||||||
|
document.body.style.overflow = "auto";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализация мобильного поиска
|
||||||
|
initSearchMobile();
|
||||||
|
|
||||||
|
// Инициализация мобильного календаря
|
||||||
|
renderCalendarMobile();
|
||||||
|
renderTagsMobile();
|
||||||
|
|
||||||
|
// Обработчики для кнопок навигации мобильного календаря
|
||||||
|
const prevMonthBtnMobile = document.getElementById("prevMonthMobile");
|
||||||
|
const nextMonthBtnMobile = document.getElementById("nextMonthMobile");
|
||||||
|
|
||||||
|
if (prevMonthBtnMobile) {
|
||||||
|
prevMonthBtnMobile.addEventListener("click", function () {
|
||||||
|
currentDate.setMonth(currentDate.getMonth() - 1);
|
||||||
|
renderCalendar();
|
||||||
|
renderCalendarMobile();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextMonthBtnMobile) {
|
||||||
|
nextMonthBtnMobile.addEventListener("click", function () {
|
||||||
|
currentDate.setMonth(currentDate.getMonth() + 1);
|
||||||
|
renderCalendar();
|
||||||
|
renderCalendarMobile();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Функция для инициализации мобильного поиска
|
||||||
|
function initSearchMobile() {
|
||||||
|
const searchInput = document.getElementById("searchInputMobile");
|
||||||
|
const clearSearchBtn = document.getElementById("clearSearchBtnMobile");
|
||||||
|
|
||||||
|
if (!searchInput || !clearSearchBtn) return;
|
||||||
|
|
||||||
|
// Обработчик ввода в поле поиска с задержкой
|
||||||
|
let searchTimeout;
|
||||||
|
searchInput.addEventListener("input", function () {
|
||||||
|
clearTimeout(searchTimeout);
|
||||||
|
const query = this.value;
|
||||||
|
|
||||||
|
// Показываем/скрываем кнопку очистки
|
||||||
|
if (query.trim()) {
|
||||||
|
clearSearchBtn.style.display = "block";
|
||||||
|
} else {
|
||||||
|
clearSearchBtn.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Задержка перед поиском для оптимизации
|
||||||
|
searchTimeout = setTimeout(() => {
|
||||||
|
searchNotes(query);
|
||||||
|
// Обновляем основное поле поиска
|
||||||
|
const mainSearchInput = document.getElementById("searchInput");
|
||||||
|
if (mainSearchInput) {
|
||||||
|
mainSearchInput.value = query;
|
||||||
|
}
|
||||||
|
const mainClearSearchBtn = document.getElementById("clearSearchBtn");
|
||||||
|
if (mainClearSearchBtn) {
|
||||||
|
if (query.trim()) {
|
||||||
|
mainClearSearchBtn.style.display = "block";
|
||||||
|
} else {
|
||||||
|
mainClearSearchBtn.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateFilterIndicator();
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработчик клика на кнопку очистки поиска
|
||||||
|
clearSearchBtn.addEventListener("click", function () {
|
||||||
|
searchInput.value = "";
|
||||||
|
this.style.display = "none";
|
||||||
|
searchQuery = "";
|
||||||
|
searchResults = [];
|
||||||
|
// Обновляем основное поле поиска
|
||||||
|
const mainSearchInput = document.getElementById("searchInput");
|
||||||
|
if (mainSearchInput) {
|
||||||
|
mainSearchInput.value = "";
|
||||||
|
}
|
||||||
|
const mainClearSearchBtn = document.getElementById("clearSearchBtn");
|
||||||
|
if (mainClearSearchBtn) {
|
||||||
|
mainClearSearchBtn.style.display = "none";
|
||||||
|
}
|
||||||
|
renderNotes(allNotes);
|
||||||
|
updateFilterIndicator();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработчик клавиши Escape для очистки поиска
|
||||||
|
searchInput.addEventListener("keydown", function (event) {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
this.value = "";
|
||||||
|
clearSearchBtn.style.display = "none";
|
||||||
|
searchQuery = "";
|
||||||
|
searchResults = [];
|
||||||
|
// Обновляем основное поле поиска
|
||||||
|
const mainSearchInput = document.getElementById("searchInput");
|
||||||
|
if (mainSearchInput) {
|
||||||
|
mainSearchInput.value = "";
|
||||||
|
}
|
||||||
|
const mainClearSearchBtn = document.getElementById("clearSearchBtn");
|
||||||
|
if (mainClearSearchBtn) {
|
||||||
|
mainClearSearchBtn.style.display = "none";
|
||||||
|
}
|
||||||
|
renderNotes(allNotes);
|
||||||
|
updateFilterIndicator();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -1,3 +1,8 @@
|
|||||||
|
// Проверяем, не авторизован ли уже пользователь
|
||||||
|
if (localStorage.getItem('isAuthenticated') === 'true') {
|
||||||
|
window.location.href = "/notes";
|
||||||
|
}
|
||||||
|
|
||||||
// Проверяем наличие ошибки в URL
|
// Проверяем наличие ошибки в URL
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
if (urlParams.get("error") === "invalid_password") {
|
if (urlParams.get("error") === "invalid_password") {
|
||||||
@ -34,7 +39,9 @@ if (loginForm) {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
// Успешный вход
|
// Успешный вход - сохраняем состояние аутентификации
|
||||||
|
localStorage.setItem('isAuthenticated', 'true');
|
||||||
|
localStorage.setItem('username', username);
|
||||||
window.location.href = "/notes";
|
window.location.href = "/notes";
|
||||||
} else {
|
} else {
|
||||||
// Ошибка входа
|
// Ошибка входа
|
||||||
|
|||||||
@ -4,10 +4,80 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Заметки</title>
|
<title>Заметки</title>
|
||||||
<link rel="stylesheet" href="/style.css?v=2" />
|
<link rel="stylesheet" href="/style.css?v=4" />
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/iconify/2.0.0/iconify.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/iconify/2.0.0/iconify.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<!-- Кнопка открытия слайдера для мобильной версии -->
|
||||||
|
<div class="mobile-menu-btn" id="mobileMenuBtn">
|
||||||
|
<span class="iconify" data-icon="mdi:menu"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Слайдер для мобильной версии -->
|
||||||
|
<div class="mobile-sidebar" id="mobileSidebar">
|
||||||
|
<div class="sidebar-close-btn" id="sidebarCloseBtn">
|
||||||
|
<span class="iconify" data-icon="mdi:close"></span>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-content">
|
||||||
|
<div class="mini-calendar">
|
||||||
|
<div class="calendar-header">
|
||||||
|
<button class="calendar-nav" id="prevMonthMobile">‹</button>
|
||||||
|
<span class="calendar-month-year" id="monthYearMobile"></span>
|
||||||
|
<button class="calendar-nav" id="nextMonthMobile">›</button>
|
||||||
|
</div>
|
||||||
|
<div class="calendar-weekdays">
|
||||||
|
<div class="calendar-weekday">Пн</div>
|
||||||
|
<div class="calendar-weekday">Вт</div>
|
||||||
|
<div class="calendar-weekday">Ср</div>
|
||||||
|
<div class="calendar-weekday">Чт</div>
|
||||||
|
<div class="calendar-weekday">Пт</div>
|
||||||
|
<div class="calendar-weekday">Сб</div>
|
||||||
|
<div class="calendar-weekday">Вс</div>
|
||||||
|
</div>
|
||||||
|
<div class="calendar-days" id="calendarDaysMobile"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Секция поиска -->
|
||||||
|
<div class="search-section">
|
||||||
|
<div class="search-header">
|
||||||
|
<span class="search-title"
|
||||||
|
><span class="iconify" data-icon="mdi:magnify"></span> Поиск</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="search-container">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="searchInputMobile"
|
||||||
|
placeholder="Поиск по заметкам..."
|
||||||
|
class="search-input"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
id="clearSearchBtnMobile"
|
||||||
|
class="clear-search-btn"
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Секция тегов -->
|
||||||
|
<div class="tags-section">
|
||||||
|
<div class="tags-header">
|
||||||
|
<span class="tags-title"
|
||||||
|
><span class="iconify" data-icon="mdi:tag"></span> Теги</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="tags-container" id="tagsContainerMobile">
|
||||||
|
<!-- Теги будут добавлены динамически -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Оверлей для закрытия слайдера -->
|
||||||
|
<div class="mobile-sidebar-overlay" id="sidebarOverlay"></div>
|
||||||
|
|
||||||
<div class="container-leftside">
|
<div class="container-leftside">
|
||||||
<div class="mini-calendar">
|
<div class="mini-calendar">
|
||||||
<div class="calendar-header">
|
<div class="calendar-header">
|
||||||
|
|||||||
@ -245,7 +245,61 @@ function isValidEmail(email) {
|
|||||||
return re.test(email);
|
return re.test(email);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Функция для проверки аутентификации
|
||||||
|
async function checkAuthentication() {
|
||||||
|
const isAuthenticated = localStorage.getItem('isAuthenticated');
|
||||||
|
|
||||||
|
if (isAuthenticated !== 'true') {
|
||||||
|
// Если пользователь не аутентифицирован, перенаправляем на страницу входа
|
||||||
|
window.location.href = "/";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что сессия на сервере еще действительна
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/auth/status");
|
||||||
|
if (!response.ok) {
|
||||||
|
// Если сессия недействительна, очищаем localStorage и перенаправляем
|
||||||
|
localStorage.removeItem('isAuthenticated');
|
||||||
|
localStorage.removeItem('username');
|
||||||
|
window.location.href = "/";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authData = await response.json();
|
||||||
|
if (!authData.authenticated) {
|
||||||
|
// Если сервер говорит, что пользователь не аутентифицирован
|
||||||
|
localStorage.removeItem('isAuthenticated');
|
||||||
|
localStorage.removeItem('username');
|
||||||
|
window.location.href = "/";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Ошибка проверки аутентификации:", error);
|
||||||
|
// В случае ошибки сети, оставляем пользователя на странице
|
||||||
|
// но показываем предупреждение
|
||||||
|
console.warn("Не удалось проверить статус аутентификации");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для настройки обработчика выхода
|
||||||
|
function setupLogoutHandler() {
|
||||||
|
const logoutForms = document.querySelectorAll('form[action="/logout"]');
|
||||||
|
logoutForms.forEach(form => {
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
// Очищаем localStorage перед выходом
|
||||||
|
localStorage.removeItem('isAuthenticated');
|
||||||
|
localStorage.removeItem('username');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Загружаем профиль при загрузке страницы
|
// Загружаем профиль при загрузке страницы
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
// Проверяем аутентификацию при загрузке страницы
|
||||||
|
checkAuthentication();
|
||||||
loadProfile();
|
loadProfile();
|
||||||
|
|
||||||
|
// Добавляем обработчик для кнопки выхода
|
||||||
|
setupLogoutHandler();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,3 +1,8 @@
|
|||||||
|
// Проверяем, не авторизован ли уже пользователь
|
||||||
|
if (localStorage.getItem('isAuthenticated') === 'true') {
|
||||||
|
window.location.href = "/notes";
|
||||||
|
}
|
||||||
|
|
||||||
// Обработка формы регистрации
|
// Обработка формы регистрации
|
||||||
const registerForm = document.getElementById("registerForm");
|
const registerForm = document.getElementById("registerForm");
|
||||||
const errorMessage = document.getElementById("errorMessage");
|
const errorMessage = document.getElementById("errorMessage");
|
||||||
@ -47,7 +52,9 @@ if (registerForm) {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
// Успешная регистрация
|
// Успешная регистрация - сохраняем состояние аутентификации
|
||||||
|
localStorage.setItem('isAuthenticated', 'true');
|
||||||
|
localStorage.setItem('username', username);
|
||||||
window.location.href = "/notes";
|
window.location.href = "/notes";
|
||||||
} else {
|
} else {
|
||||||
// Ошибка регистрации
|
// Ошибка регистрации
|
||||||
|
|||||||
363
public/style.css
363
public/style.css
@ -895,3 +895,366 @@ textarea:focus {
|
|||||||
background-color: #ffc107;
|
background-color: #ffc107;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Стили для мобильного меню и слайдера */
|
||||||
|
.mobile-menu-btn {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 15px;
|
||||||
|
left: 15px;
|
||||||
|
z-index: 999;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-btn:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-btn .iconify {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Мобильный слайдер */
|
||||||
|
.mobile-sidebar {
|
||||||
|
display: flex;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: -85vw;
|
||||||
|
width: 85vw;
|
||||||
|
max-width: 320px;
|
||||||
|
height: 100vh;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.2);
|
||||||
|
z-index: 1000;
|
||||||
|
overflow-y: auto;
|
||||||
|
transition: left 0.3s ease;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sidebar.open {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-close-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 10px 10px 10px auto;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-close-btn:hover {
|
||||||
|
background: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-close-btn .iconify {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-content {
|
||||||
|
padding: 10px 12px;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Правильные стили для календаря в слайдере */
|
||||||
|
.mobile-sidebar .mini-calendar {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sidebar .calendar-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sidebar .calendar-month-year {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sidebar .calendar-nav {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #007bff;
|
||||||
|
padding: 0 3px;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sidebar .calendar-nav:hover {
|
||||||
|
color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Календарь дней в слайдере */
|
||||||
|
.mobile-sidebar .calendar-days {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
gap: 1px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sidebar .calendar-day {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 9px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
color: #333;
|
||||||
|
padding: 1px;
|
||||||
|
font-weight: 500;
|
||||||
|
position: relative;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sidebar .calendar-day:hover {
|
||||||
|
background-color: #e7f3ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sidebar .calendar-day.today {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sidebar .calendar-day.selected {
|
||||||
|
background-color: #0056b3;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sidebar .calendar-day.other-month {
|
||||||
|
color: #ccc;
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Поиск в слайдере */
|
||||||
|
.mobile-sidebar .search-section {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sidebar .search-title {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sidebar .search-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 15px;
|
||||||
|
font-size: 11px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sidebar .search-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #007bff;
|
||||||
|
background-color: white;
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Теги в слайдере */
|
||||||
|
.mobile-sidebar .tags-section {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sidebar .tags-title {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sidebar .tags-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sidebar .tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background-color: #e7f3ff;
|
||||||
|
color: #007bff;
|
||||||
|
border: 1px solid #007bff;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sidebar .tag:hover {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sidebar .tag.active {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sidebar .tag-count {
|
||||||
|
margin-left: 3px;
|
||||||
|
font-size: 8px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Оверлей для закрытия слайдера */
|
||||||
|
.mobile-sidebar-overlay {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 999;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-sidebar-overlay.open {
|
||||||
|
display: block;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Мобильная адаптация */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
/* Показываем мобильное меню */
|
||||||
|
.mobile-menu-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Скрываем левый блок с календарем, поиском и тегами */
|
||||||
|
.container-leftside {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* На мобильных устройствах меняем направление flex и центрируем */
|
||||||
|
body {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
padding: 0 10px;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Центральный контейнер занимает всю ширину, но центрируется */
|
||||||
|
.center {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-top: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Адаптируем контейнер заметок */
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Адаптируем заголовок заметок */
|
||||||
|
.notes-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes-header-left {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Адаптируем кнопки markdown */
|
||||||
|
.markdown-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-buttons .btnMarkdown {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
min-width: auto;
|
||||||
|
margin-right: 0;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Адаптируем textarea */
|
||||||
|
textarea {
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Адаптируем кнопку сохранения */
|
||||||
|
.save-button-container {
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnSave {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Адаптируем footer */
|
||||||
|
.footer {
|
||||||
|
position: relative;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
26
server.js
26
server.js
@ -2,6 +2,7 @@ const express = require("express");
|
|||||||
const sqlite3 = require("sqlite3").verbose();
|
const sqlite3 = require("sqlite3").verbose();
|
||||||
const bcrypt = require("bcryptjs");
|
const bcrypt = require("bcryptjs");
|
||||||
const session = require("express-session");
|
const session = require("express-session");
|
||||||
|
const SQLiteStore = require("connect-sqlite3")(session);
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const helmet = require("helmet");
|
const helmet = require("helmet");
|
||||||
const rateLimit = require("express-rate-limit");
|
const rateLimit = require("express-rate-limit");
|
||||||
@ -93,13 +94,21 @@ app.use(express.static(path.join(__dirname, "public")));
|
|||||||
app.use(bodyParser.urlencoded({ extended: true }));
|
app.use(bodyParser.urlencoded({ extended: true }));
|
||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json());
|
||||||
|
|
||||||
// Настройка сессий
|
// Настройка сессий с хранением в SQLite
|
||||||
app.use(
|
app.use(
|
||||||
session({
|
session({
|
||||||
|
store: new SQLiteStore({
|
||||||
|
db: "sessions.db",
|
||||||
|
table: "sessions",
|
||||||
|
dir: "./"
|
||||||
|
}),
|
||||||
secret: process.env.SESSION_SECRET || "default-secret",
|
secret: process.env.SESSION_SECRET || "default-secret",
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: false,
|
saveUninitialized: false,
|
||||||
cookie: { secure: false }, // в продакшене установить true с HTTPS
|
cookie: {
|
||||||
|
secure: false, // в продакшене установить true с HTTPS
|
||||||
|
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 дней
|
||||||
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -285,6 +294,19 @@ app.post("/login", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// API для проверки статуса аутентификации
|
||||||
|
app.get("/api/auth/status", (req, res) => {
|
||||||
|
if (req.session.authenticated && req.session.userId) {
|
||||||
|
res.json({
|
||||||
|
authenticated: true,
|
||||||
|
userId: req.session.userId,
|
||||||
|
username: req.session.username
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.status(401).json({ authenticated: false });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// API для получения информации о пользователе
|
// API для получения информации о пользователе
|
||||||
app.get("/api/user", requireAuth, (req, res) => {
|
app.get("/api/user", requireAuth, (req, res) => {
|
||||||
if (!req.session.userId) {
|
if (!req.session.userId) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user