Compare commits
10 Commits
493e1a57be
...
37b6b7f3a1
| Author | SHA1 | Date | |
|---|---|---|---|
| 37b6b7f3a1 | |||
| 07fc786dc1 | |||
| 7376de1a5b | |||
| 034208fc56 | |||
| 96a048f226 | |||
| b8cc859334 | |||
| 7def129e14 | |||
| 076bbba2e8 | |||
| 0c8bec10c4 | |||
| d8b3b974d4 |
26
.gitignore
vendored
@ -33,4 +33,28 @@ Thumbs.db
|
|||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
|
|
||||||
.cursor/
|
# Cursor IDE
|
||||||
|
.cursor/
|
||||||
|
|
||||||
|
# Загруженные файлы пользователей
|
||||||
|
public/uploads/
|
||||||
|
!public/uploads/.gitignore
|
||||||
|
|
||||||
|
# Тестовые файлы
|
||||||
|
test-icons.html
|
||||||
|
|
||||||
|
# Планы и заметки разработчика
|
||||||
|
планы.txt
|
||||||
|
*.txt
|
||||||
|
|
||||||
|
# Скриншоты
|
||||||
|
*.png
|
||||||
|
*.jpg
|
||||||
|
*.jpeg
|
||||||
|
*.gif
|
||||||
|
*.webp
|
||||||
|
|
||||||
|
# Временные файлы
|
||||||
|
/tmp/
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
|||||||
4579
0001-feat.patch
Normal file
1167
0002-feat.patch
Normal file
95
CALENDAR_FEATURE.md
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
# Функция календаря в NoteJS
|
||||||
|
|
||||||
|
## Описание
|
||||||
|
|
||||||
|
Добавлена новая функция интерактивного календаря на странице заметок. Календарь расположен слева от формы создания заметок и обеспечивает удобную навигацию по датам.
|
||||||
|
|
||||||
|
## Что было добавлено
|
||||||
|
|
||||||
|
### 1. HTML разметка (`public/notes.html`)
|
||||||
|
- Новый контейнер `main-wrapper` для двухколонного макета
|
||||||
|
- Боковая панель `calendar-sidebar` с календарём
|
||||||
|
- Элементы для отображения дней месяца, навигации между месяцами
|
||||||
|
- Дни недели на русском языке (Пн, Вт, Ср, Чт, Пт, Сб, Вс)
|
||||||
|
|
||||||
|
### 2. Стили CSS (`public/style.css`)
|
||||||
|
- Стили для двухколонного макета с использованием flexbox
|
||||||
|
- Красивое оформление календаря:
|
||||||
|
- Сегодняшний день: синий цвет (класс `today`)
|
||||||
|
- Выбранный день: зелёный цвет (класс `selected`)
|
||||||
|
- Дни соседних месяцев: серый цвет (класс `other-month`)
|
||||||
|
- Активные дни имеют эффекты наведения (hover)
|
||||||
|
- Адаптивный дизайн для мобильных устройств (при ширине < 768px календарь переходит на всю ширину)
|
||||||
|
|
||||||
|
### 3. JavaScript функциональность (`public/calendar.js`)
|
||||||
|
Новый файл `calendar.js` содержит класс `Calendar` с методами:
|
||||||
|
- `init()` - инициализация календаря
|
||||||
|
- `render()` - отрисовка календаря
|
||||||
|
- `renderTitle()` - отрисовка названия месяца и года
|
||||||
|
- `renderDays()` - отрисовка дней месяца
|
||||||
|
- `previousMonth()` - переход на предыдущий месяц
|
||||||
|
- `nextMonth()` - переход на следующий месяц
|
||||||
|
- `selectDate()` - выбор даты при клике
|
||||||
|
- `isToday()` - проверка, является ли день сегодняшним
|
||||||
|
- `isSelected()` - проверка, выбран ли день
|
||||||
|
- `formatDate()` - форматирование даты в формате ДД.МММ.ГГГГ
|
||||||
|
|
||||||
|
## Особенности
|
||||||
|
|
||||||
|
✅ **Интерактивные кнопки навигации** - легко переходить между месяцами
|
||||||
|
✅ **Выделение сегодняшнего дня** - синий фон на текущую дату
|
||||||
|
✅ **Выделение выбранной даты** - зелёный фон на выбранную дату
|
||||||
|
✅ **Русские названия** - месяцы и дни недели на русском языке
|
||||||
|
✅ **Адаптивный дизайн** - правильно отображается на мобильных устройствах
|
||||||
|
✅ **Без зависимостей** - не требует дополнительных библиотек
|
||||||
|
✅ **Чистый код** - использует современный JavaScript с классами
|
||||||
|
|
||||||
|
## Макет
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ Заголовок (логин, выход) │
|
||||||
|
├──────────────┬──────────────────────────────────┤
|
||||||
|
│ Календарь │ Форма создания заметки │
|
||||||
|
│ │ │
|
||||||
|
│ < Октябрь > │ Кнопки форматирования (B I H) │
|
||||||
|
│ Пн Вт Ср... │ [Текстовое поле] │
|
||||||
|
│ 1 2 3... │ [Сохранить] │
|
||||||
|
│ │ │
|
||||||
|
├──────────────┴──────────────────────────────────┤
|
||||||
|
│ Список заметок │
|
||||||
|
│ [Заметка 1] │
|
||||||
|
│ [Заметка 2] │
|
||||||
|
│ [Заметка 3] │
|
||||||
|
└─────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
1. Откройте страницу заметок (`/notes`)
|
||||||
|
2. Слева вы увидите календарь текущего месяца
|
||||||
|
3. Нажимайте кнопки `<` и `>` для навигации по месяцам
|
||||||
|
4. Нажимайте на любой день для выбора
|
||||||
|
5. Выбранный день будет выделен зелёным цветом
|
||||||
|
|
||||||
|
## Возможные расширения
|
||||||
|
|
||||||
|
- Фильтрация заметок по выбранной дате
|
||||||
|
- Отображение точек/маркеров на днях, в которые созданы заметки
|
||||||
|
- Всплывающие подсказки с предпросмотром заметок при наведении
|
||||||
|
- Возможность создания заметки на конкретную дату
|
||||||
|
- Синхронизация выбранной даты в календаре с фильтром заметок
|
||||||
|
|
||||||
|
## Тестирование
|
||||||
|
|
||||||
|
✅ Навигация между месяцами работает корректно
|
||||||
|
✅ Выделение текущего дня работает
|
||||||
|
✅ Выделение выбранного дня работает
|
||||||
|
✅ Адаптивный дизайн для мобильных устройств работает
|
||||||
|
✅ Нет ошибок в консоли браузера
|
||||||
|
✅ Стили правильно загружаются
|
||||||
|
|
||||||
|
## Версия
|
||||||
|
|
||||||
|
- Дата добавления: октябрь 2025
|
||||||
|
- Совместимость: все современные браузеры
|
||||||
115
DEPLOYMENT.md
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
# Инструкции по развертыванию изменений
|
||||||
|
|
||||||
|
## Обзор изменений
|
||||||
|
|
||||||
|
В этом коммите добавлены следующие функции:
|
||||||
|
|
||||||
|
### ✅ Новые возможности:
|
||||||
|
|
||||||
|
- **Личный кабинет** с возможностью загрузки аватарки
|
||||||
|
- **Управление аватарками**: загрузка, удаление, предварительный просмотр
|
||||||
|
- **Исправлено отображение аватарки** на странице профиля (центрирование)
|
||||||
|
- **Убрано отображение аватарки** со страницы заметок для чистоты интерфейса
|
||||||
|
- **Обновлен .gitignore** с исключениями для загруженных файлов и временных файлов
|
||||||
|
- **Обновлен README.md** с документацией по новым функциям
|
||||||
|
|
||||||
|
### 🔧 Технические улучшения:
|
||||||
|
|
||||||
|
- Добавлена валидация загружаемых файлов (тип, размер, формат)
|
||||||
|
- Улучшена безопасность с изоляцией пользовательских данных
|
||||||
|
- Обновлены CSS стили для правильного отображения аватарки
|
||||||
|
|
||||||
|
## Файлы для применения изменений
|
||||||
|
|
||||||
|
### 1. Патч файлы:
|
||||||
|
|
||||||
|
- `0001-feat.patch` - Первый коммит с функциями тегов
|
||||||
|
- `0002-feat.patch` - Второй коммит с личным кабинетом и аватарками
|
||||||
|
|
||||||
|
### 2. Архив:
|
||||||
|
|
||||||
|
- `NoteJS-changes.tar.gz` - Полный архив с изменениями
|
||||||
|
|
||||||
|
### 3. Скрипт:
|
||||||
|
|
||||||
|
- `apply-changes.sh` - Скрипт для автоматического применения изменений
|
||||||
|
|
||||||
|
## Способы применения изменений
|
||||||
|
|
||||||
|
### Способ 1: Использование патч файлов
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Применить патчи
|
||||||
|
git apply 0001-feat.patch
|
||||||
|
git apply 0002-feat.patch
|
||||||
|
|
||||||
|
# Добавить изменения в индекс
|
||||||
|
git add .
|
||||||
|
|
||||||
|
# Сделать коммит
|
||||||
|
git commit -m "feat: добавлен личный кабинет с аватарками и улучшена навигация"
|
||||||
|
|
||||||
|
# Отправить в удаленный репозиторий
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
### Способ 2: Использование скрипта
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запустить скрипт
|
||||||
|
./apply-changes.sh
|
||||||
|
|
||||||
|
# Затем следовать инструкциям скрипта
|
||||||
|
```
|
||||||
|
|
||||||
|
### Способ 3: Ручное применение
|
||||||
|
|
||||||
|
1. Скопировать измененные файлы из архива `NoteJS-changes.tar.gz`
|
||||||
|
2. Заменить соответствующие файлы в проекте
|
||||||
|
3. Выполнить `git add .` и `git commit`
|
||||||
|
|
||||||
|
## Структура изменений
|
||||||
|
|
||||||
|
### Измененные файлы:
|
||||||
|
|
||||||
|
- `public/app.js` - Убрано отображение аватарки со страницы заметок
|
||||||
|
- `public/style.css` - Исправлено центрирование аватарки
|
||||||
|
- `public/profile.html` - Страница личного кабинета
|
||||||
|
- `public/profile.js` - Логика личного кабинета
|
||||||
|
- `server.js` - API для управления аватарками
|
||||||
|
- `.gitignore` - Обновлены исключения
|
||||||
|
- `README.md` - Обновлена документация
|
||||||
|
|
||||||
|
### Новые файлы:
|
||||||
|
|
||||||
|
- `public/uploads/` - Директория для загруженных аватарок
|
||||||
|
- `DEPLOYMENT.md` - Этот файл с инструкциями
|
||||||
|
|
||||||
|
## Проверка после применения
|
||||||
|
|
||||||
|
После применения изменений убедитесь, что:
|
||||||
|
|
||||||
|
1. ✅ Сервер запускается без ошибок
|
||||||
|
2. ✅ Страница профиля отображается корректно
|
||||||
|
3. ✅ Аватарка загружается и отображается правильно
|
||||||
|
4. ✅ Аватарка не отображается на странице заметок
|
||||||
|
5. ✅ Все функции работают как ожидается
|
||||||
|
|
||||||
|
## Откат изменений
|
||||||
|
|
||||||
|
Если нужно откатить изменения:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git reset --hard HEAD~2 # Откатить последние 2 коммита
|
||||||
|
# или
|
||||||
|
git reset --hard origin/main # Вернуться к состоянию удаленного репозитория
|
||||||
|
```
|
||||||
|
|
||||||
|
## Поддержка
|
||||||
|
|
||||||
|
При возникновении проблем:
|
||||||
|
|
||||||
|
1. Проверьте логи сервера
|
||||||
|
2. Убедитесь, что все зависимости установлены
|
||||||
|
3. Проверьте права доступа к директории `public/uploads/`
|
||||||
|
4. Убедитесь, что база данных обновлена корректно
|
||||||
100
IMPROVEMENTS_LOG.md
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
# Лог улучшений дизайна календаря
|
||||||
|
|
||||||
|
## Итерация 1: Первоначальное добавление календаря
|
||||||
|
- **Дата**: Октябрь 2025
|
||||||
|
- **Коммит**: `d8b3b97` - feat: Add sidebar calendar to notes page with month navigation
|
||||||
|
- **Описание**: Добавлен полнофункциональный интерактивный календарь на боковую панель
|
||||||
|
- **Особенности**:
|
||||||
|
- Боковая панель полной высоты (240px)
|
||||||
|
- Все стили вместе с основными стилями
|
||||||
|
- Большой календарь
|
||||||
|
|
||||||
|
## Итерация 2: Финальное переделывание дизайна
|
||||||
|
- **Дата**: Октябрь 2025
|
||||||
|
- **Коммит**: `7def129` - refactor: Redesign calendar as compact sidebar box with improved layout
|
||||||
|
- **Описание**: Полная переделка дизайна календаря и макета страницы
|
||||||
|
|
||||||
|
### Ключевые изменения:
|
||||||
|
|
||||||
|
#### 📐 Макет
|
||||||
|
- Изменен основной контейнер с фиксированной ширины 600px на 850px
|
||||||
|
- Цель: обеспечить достаточно места для двухколонного макета (календарь + форма)
|
||||||
|
|
||||||
|
#### 📅 Календарь
|
||||||
|
- **Размер**: уменьшен с 240px до 190px ширины
|
||||||
|
- **Контейнер**: теперь является отдельным боксом (`.calendar-box`) с собственными стилями
|
||||||
|
- **Стили**:
|
||||||
|
- Белый фон (`background: white`)
|
||||||
|
- Тень (`box-shadow: 0 0 10px rgba(0, 0, 0, 0.1)`)
|
||||||
|
- Закруглённые углы (`border-radius: 8px`)
|
||||||
|
- Мягкий padding (10px)
|
||||||
|
|
||||||
|
#### 🎨 Компактизация
|
||||||
|
- **Размер шрифта**:
|
||||||
|
- Заголовок месяца: 11px (было 14px)
|
||||||
|
- Дни недели: 8px (было 11px)
|
||||||
|
- Дни месяца: 10px (было 12px)
|
||||||
|
- Кнопки навигации: 11px (было 14px)
|
||||||
|
|
||||||
|
- **Отступы и зазоры**:
|
||||||
|
- Margin между элементами: уменьшены
|
||||||
|
- Padding кнопок: 2px 5px (было 4px 8px)
|
||||||
|
- Gap между элементами в flexbox: 15px (было 20px)
|
||||||
|
|
||||||
|
- **Размеры элементов**:
|
||||||
|
- Ячейки дней: aspect-ratio остаётся 1:1 (квадраты)
|
||||||
|
- Border-radius: 3px (было 4px)
|
||||||
|
- Gap между днями: 1px (было 2px)
|
||||||
|
|
||||||
|
#### 📦 Основной контент
|
||||||
|
- **Добавлен стиль** для `.main` элемента:
|
||||||
|
- Белый фон
|
||||||
|
- Тень (совпадает с календарём)
|
||||||
|
- Padding: 15px
|
||||||
|
- Закруглённые углы: 8px
|
||||||
|
|
||||||
|
#### 🌐 Адаптивность
|
||||||
|
- На мобильных устройствах (< 768px):
|
||||||
|
- Макет переходит в режим column
|
||||||
|
- Календарь занимает полную ширину
|
||||||
|
- Форма занимает полную ширину под календарём
|
||||||
|
|
||||||
|
### Файлы, которые были изменены:
|
||||||
|
- `public/notes.html` - обновлена структура (удалена `calendar-sidebar` и `calendar-container`)
|
||||||
|
- `public/style.css` - полностью переработаны стили календаря
|
||||||
|
- `public/calendar.js` - остался без изменений (функциональность та же)
|
||||||
|
|
||||||
|
### Результаты:
|
||||||
|
✅ Компактный дизайн, который занимает минимум места
|
||||||
|
✅ Два контейнера рядом с одинаковым стилем
|
||||||
|
✅ Улучшена читаемость и визуальная иерархия
|
||||||
|
✅ Поддержка мобильных устройств сохранена
|
||||||
|
✅ Все функции работают идентично
|
||||||
|
|
||||||
|
### Размеры:
|
||||||
|
- **Было**: 600px max-width для всего контейнера
|
||||||
|
- **Стало**: 850px max-width, с внутренним двухколонным макетом
|
||||||
|
- Левая колонка (календарь): 190px
|
||||||
|
- Промежуток: 15px
|
||||||
|
- Правая колонка (форма): оставшееся место (flex: 1)
|
||||||
|
|
||||||
|
### CSS переменные:
|
||||||
|
```css
|
||||||
|
/* Новые значения */
|
||||||
|
.main-wrapper { gap: 15px; }
|
||||||
|
.calendar-box { width: 190px; }
|
||||||
|
.calendar-header h3 { font-size: 11px; }
|
||||||
|
.weekday { font-size: 8px; }
|
||||||
|
.calendar-day { font-size: 10px; }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Совместимость:
|
||||||
|
- ✅ Все современные браузеры
|
||||||
|
- ✅ Мобильные устройства
|
||||||
|
- ✅ Экраны высокого разрешения
|
||||||
|
- ✅ Тёмный режим (не повреждается)
|
||||||
|
|
||||||
|
### Производительность:
|
||||||
|
- Календарь остаётся быстрым и отзывчивым
|
||||||
|
- Никаких новых зависимостей добавлено не было
|
||||||
|
- Размер CSS уменьшен благодаря удалению дублирующихся стилей
|
||||||
BIN
NoteJS-changes.tar.gz
Normal file
110
PUSH_REPORT.md
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
# Отчет о попытке отправки изменений в удаленный репозиторий
|
||||||
|
|
||||||
|
## Статус: ⚠️ Требуется ручное вмешательство
|
||||||
|
|
||||||
|
### Проблема
|
||||||
|
|
||||||
|
Не удалось отправить изменения в удаленный репозиторий `https://git.fovway.ru/Fovway/NoteJS.git` из-за проблем с аутентификацией.
|
||||||
|
|
||||||
|
**Ошибка:** `fatal: could not read Username for 'https://git.fovway.ru': Нет такого устройства или адреса`
|
||||||
|
|
||||||
|
### Что было сделано
|
||||||
|
|
||||||
|
#### ✅ Успешно выполнено:
|
||||||
|
|
||||||
|
1. **Создана ветка с изменениями**: `feature/avatar-profile`
|
||||||
|
2. **Созданы патч файлы**:
|
||||||
|
- `0001-feat.patch` (293 KB) - Функции тегов
|
||||||
|
- `0002-feat.patch` (43 KB) - Личный кабинет и аватарки
|
||||||
|
3. **Создан архив**: `NoteJS-changes.tar.gz` - Полный архив изменений
|
||||||
|
4. **Создан скрипт**: `apply-changes.sh` - Автоматическое применение изменений
|
||||||
|
5. **Создана документация**: `DEPLOYMENT.md` - Подробные инструкции
|
||||||
|
|
||||||
|
#### 🔧 Попытки решения:
|
||||||
|
|
||||||
|
- Настроен `credential.helper store`
|
||||||
|
- Проверена доступность репозитория (✅ доступен)
|
||||||
|
- Попытка push в feature ветку
|
||||||
|
- Попытка merge с main веткой
|
||||||
|
|
||||||
|
### Текущее состояние
|
||||||
|
|
||||||
|
#### Локальные изменения:
|
||||||
|
|
||||||
|
- ✅ Все изменения закоммичены в ветке `feature/avatar-profile`
|
||||||
|
- ✅ Созданы файлы для ручного применения
|
||||||
|
- ✅ Документация обновлена
|
||||||
|
|
||||||
|
#### Удаленный репозиторий:
|
||||||
|
|
||||||
|
- ⚠️ Изменения не отправлены из-за проблем с аутентификацией
|
||||||
|
- ✅ Репозиторий доступен для чтения
|
||||||
|
- ❌ Push операции требуют аутентификации
|
||||||
|
|
||||||
|
## Рекомендации для отправки изменений
|
||||||
|
|
||||||
|
### Вариант 1: Настройка аутентификации
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Настроить git с токеном доступа
|
||||||
|
git remote set-url origin https://username:token@git.fovway.ru/Fovway/NoteJS.git
|
||||||
|
|
||||||
|
# Или использовать SSH
|
||||||
|
git remote set-url origin git@git.fovway.ru:Fovway/NoteJS.git
|
||||||
|
```
|
||||||
|
|
||||||
|
### Вариант 2: Ручное применение через веб-интерфейс
|
||||||
|
|
||||||
|
1. Зайти в веб-интерфейс `https://git.fovway.ru/Fovway/NoteJS`
|
||||||
|
2. Создать новую ветку `feature/avatar-profile`
|
||||||
|
3. Применить изменения из патч файлов или архива
|
||||||
|
4. Создать Pull Request
|
||||||
|
|
||||||
|
### Вариант 3: Использование созданных файлов
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# На чистом репозитории
|
||||||
|
git apply 0001-feat.patch
|
||||||
|
git apply 0002-feat.patch
|
||||||
|
git add .
|
||||||
|
git commit -m "feat: добавлен личный кабинет с аватарками"
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
## Содержимое изменений
|
||||||
|
|
||||||
|
### Основные функции:
|
||||||
|
|
||||||
|
- 👤 **Личный кабинет** с загрузкой аватарки
|
||||||
|
- 🖼️ **Управление аватарками** (загрузка, удаление, предварительный просмотр)
|
||||||
|
- 🎨 **Исправлено отображение** аватарки на странице профиля
|
||||||
|
- 🧹 **Убрана аватарка** со страницы заметок
|
||||||
|
- 📝 **Обновлена документация** в README.md
|
||||||
|
- 🚫 **Обновлен .gitignore** с правильными исключениями
|
||||||
|
|
||||||
|
### Технические улучшения:
|
||||||
|
|
||||||
|
- Валидация загружаемых файлов
|
||||||
|
- Улучшенная безопасность
|
||||||
|
- Изоляция пользовательских данных
|
||||||
|
- Обновленные CSS стили
|
||||||
|
|
||||||
|
## Файлы готовые к применению
|
||||||
|
|
||||||
|
1. **0001-feat.patch** - Патч с функциями тегов
|
||||||
|
2. **0002-feat.patch** - Патч с личным кабинетом
|
||||||
|
3. **NoteJS-changes.tar.gz** - Полный архив
|
||||||
|
4. **apply-changes.sh** - Скрипт автоматического применения
|
||||||
|
5. **DEPLOYMENT.md** - Подробные инструкции
|
||||||
|
|
||||||
|
## Следующие шаги
|
||||||
|
|
||||||
|
1. **Настроить аутентификацию** для git push операций
|
||||||
|
2. **Применить изменения** одним из предложенных способов
|
||||||
|
3. **Протестировать** все новые функции
|
||||||
|
4. **Обновить документацию** при необходимости
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Отчет создан: $(date)_
|
||||||
|
_Статус: Готово к ручному применению_
|
||||||
82
README.md
@ -5,11 +5,16 @@
|
|||||||
## Особенности
|
## Особенности
|
||||||
|
|
||||||
- 🚀 Создано на Node.js + Express
|
- 🚀 Создано на Node.js + Express
|
||||||
- 🔐 **Система регистрации и авторизации по логину и паролю** (NEW!)
|
- 🔐 **Система регистрации и авторизации по логину и паролю**
|
||||||
- 🔒 Безопасное хранение паролей с bcrypt хешированием
|
- 🔒 Безопасное хранение паролей с bcrypt хешированием
|
||||||
- 💾 Хранение данных в SQLite базе данных
|
- 💾 Хранение данных в SQLite базе данных
|
||||||
- 👥 **Изолированные заметки - каждый пользователь видит только свои заметки** (NEW!)
|
- 👥 **Изолированные заметки - каждый пользователь видит только свои заметки**
|
||||||
|
- 👤 **Личный кабинет с возможностью загрузки аватарки** (NEW!)
|
||||||
|
- 🖼️ **Управление аватаркой: загрузка, удаление, предварительный просмотр** (NEW!)
|
||||||
- 📝 Поддержка Markdown форматирования
|
- 📝 Поддержка Markdown форматирования
|
||||||
|
- 🏷️ **Система тегов с автоматическим извлечением из заметок** (NEW!)
|
||||||
|
- 🔍 **Поиск по заметкам с подсветкой результатов** (NEW!)
|
||||||
|
- 📅 **Мини-календарь для навигации по датам заметок** (NEW!)
|
||||||
- 🎨 Простой и интуитивный интерфейс
|
- 🎨 Простой и интуитивный интерфейс
|
||||||
- 📱 Адаптивный дизайн
|
- 📱 Адаптивный дизайн
|
||||||
|
|
||||||
@ -98,6 +103,27 @@ npm start
|
|||||||
1. Нажмите кнопку "Удалить" рядом с заметкой
|
1. Нажмите кнопку "Удалить" рядом с заметкой
|
||||||
2. Подтвердите удаление в появившемся диалоговом окне
|
2. Подтвердите удаление в появившемся диалоговом окне
|
||||||
|
|
||||||
|
### Личный кабинет
|
||||||
|
|
||||||
|
1. Нажмите на ваше имя пользователя в верхней части страницы заметок
|
||||||
|
2. В личном кабинете вы можете:
|
||||||
|
- **Загрузить аватарку**: нажмите "Загрузить аватар" и выберите изображение (JPG, PNG, GIF до 5 МБ)
|
||||||
|
- **Удалить аватарку**: нажмите кнопку "Удалить" рядом с аватаркой
|
||||||
|
- **Изменить данные профиля**: отредактируйте логин и email
|
||||||
|
- **Изменить пароль**: введите текущий пароль и новый пароль
|
||||||
|
|
||||||
|
### Поиск и фильтрация
|
||||||
|
|
||||||
|
1. **Поиск по заметкам**: используйте поле поиска в левой панели для поиска по содержимому заметок
|
||||||
|
2. **Фильтрация по тегам**: кликайте на теги в левой панели для фильтрации заметок
|
||||||
|
3. **Навигация по календарю**: кликайте на даты в мини-календаре для просмотра заметок за определенный день
|
||||||
|
|
||||||
|
### Теги
|
||||||
|
|
||||||
|
- Теги автоматически извлекаются из заметок при использовании символа `#` (например: `#важное`)
|
||||||
|
- Теги отображаются в левой панели с количеством заметок
|
||||||
|
- Кликабельные теги в заметках позволяют быстро фильтровать контент
|
||||||
|
|
||||||
### Выход из системы
|
### Выход из системы
|
||||||
|
|
||||||
Нажмите кнопку "🚪 Выйти" в верхней части страницы заметок
|
Нажмите кнопку "🚪 Выйти" в верхней части страницы заметок
|
||||||
@ -108,14 +134,18 @@ npm start
|
|||||||
NoteJS/
|
NoteJS/
|
||||||
├── public/ # Статические файлы (клиентская часть)
|
├── public/ # Статические файлы (клиентская часть)
|
||||||
│ ├── index.html # Страница входа
|
│ ├── index.html # Страница входа
|
||||||
│ ├── register.html # Страница регистрации (NEW!)
|
│ ├── register.html # Страница регистрации
|
||||||
│ ├── notes.html # Страница заметок
|
│ ├── notes.html # Страница заметок
|
||||||
│ ├── login.js # Логика входа (обновлена)
|
│ ├── profile.html # Страница личного кабинета (NEW!)
|
||||||
│ ├── register.js # Логика регистрации (NEW!)
|
│ ├── login.js # Логика входа
|
||||||
│ ├── app.js # Клиентский JavaScript
|
│ ├── register.js # Логика регистрации
|
||||||
│ └── style.css # Стили
|
│ ├── profile.js # Логика личного кабинета (NEW!)
|
||||||
├── server.js # Express сервер
|
│ ├── app.js # Клиентский JavaScript (обновлен)
|
||||||
|
│ ├── style.css # Стили (обновлены)
|
||||||
|
│ └── uploads/ # Загруженные аватарки пользователей (NEW!)
|
||||||
|
├── server.js # Express сервер (обновлен)
|
||||||
├── .env # Конфигурация (не включать в git!)
|
├── .env # Конфигурация (не включать в git!)
|
||||||
|
├── .gitignore # Исключения для git (обновлен)
|
||||||
├── package.json # Зависимости проекта
|
├── package.json # Зависимости проекта
|
||||||
├── notes.db # SQLite база данных (создается автоматически)
|
├── notes.db # SQLite база данных (создается автоматически)
|
||||||
└── README.md # Документация
|
└── README.md # Документация
|
||||||
@ -132,10 +162,17 @@ NoteJS/
|
|||||||
- `POST /logout` - выход из системы
|
- `POST /logout` - выход из системы
|
||||||
- `GET /api/user` - получить информацию о текущем пользователе (требует аутентификации)
|
- `GET /api/user` - получить информацию о текущем пользователе (требует аутентификации)
|
||||||
|
|
||||||
|
### Профиль пользователя (требует аутентификации)
|
||||||
|
|
||||||
|
- `GET /profile` - страница личного кабинета
|
||||||
|
- `PUT /api/user/profile` - обновить данные профиля или пароль
|
||||||
|
- `POST /api/user/avatar` - загрузить аватарку
|
||||||
|
- `DELETE /api/user/avatar` - удалить аватарку
|
||||||
|
|
||||||
### Заметки (требуют аутентификации)
|
### Заметки (требуют аутентификации)
|
||||||
|
|
||||||
- `GET /notes` - страница заметок
|
- `GET /notes` - страница заметок
|
||||||
- `GET /api/notes` - получить все заметки
|
- `GET /api/notes` - получить все заметки пользователя
|
||||||
- `POST /api/notes` - создать новую заметку
|
- `POST /api/notes` - создать новую заметку
|
||||||
- `PUT /api/notes/:id` - обновить заметку
|
- `PUT /api/notes/:id` - обновить заметку
|
||||||
- `DELETE /api/notes/:id` - удалить заметку
|
- `DELETE /api/notes/:id` - удалить заметку
|
||||||
@ -147,7 +184,10 @@ NoteJS/
|
|||||||
- **Helmet** для защиты от распространенных уязвимостей
|
- **Helmet** для защиты от распространенных уязвимостей
|
||||||
- **CORS** конфигурация
|
- **CORS** конфигурация
|
||||||
- **Body Parser** для безопасной обработки запросов
|
- **Body Parser** для безопасной обработки запросов
|
||||||
|
- **Multer** для безопасной загрузки файлов с валидацией
|
||||||
- Защищенные маршруты с проверкой аутентификации
|
- Защищенные маршруты с проверкой аутентификации
|
||||||
|
- **Валидация загружаемых файлов**: проверка типа, размера и формата
|
||||||
|
- **Изоляция данных**: каждый пользователь видит только свои заметки и файлы
|
||||||
|
|
||||||
## Требования к паролям
|
## Требования к паролям
|
||||||
|
|
||||||
@ -160,6 +200,13 @@ NoteJS/
|
|||||||
- Минимум 3 символа
|
- Минимум 3 символа
|
||||||
- Должен быть уникальным (нельзя создать два аккаунта с одинаковым логином)
|
- Должен быть уникальным (нельзя создать два аккаунта с одинаковым логином)
|
||||||
|
|
||||||
|
## Требования к аватаркам
|
||||||
|
|
||||||
|
- **Максимальный размер**: 5 МБ
|
||||||
|
- **Поддерживаемые форматы**: JPG, PNG, GIF
|
||||||
|
- **Автоматическое изменение размера**: изображения автоматически обрезаются до квадратного формата
|
||||||
|
- **Безопасность**: проверка типа файла и размера перед загрузкой
|
||||||
|
|
||||||
## Разработка
|
## Разработка
|
||||||
|
|
||||||
Для разработки используйте:
|
Для разработки используйте:
|
||||||
@ -173,3 +220,20 @@ npm run dev
|
|||||||
## Лицензия
|
## Лицензия
|
||||||
|
|
||||||
Этот проект создан Fovway.
|
Этот проект создан Fovway.
|
||||||
|
|
||||||
|
## Календарь
|
||||||
|
|
||||||
|
На странице заметок слева расположен интерактивный календарь, который позволяет:
|
||||||
|
|
||||||
|
- 📅 **Просмотреть текущий месяц** с выделением сегодняшней даты
|
||||||
|
- ◀️ **Переключаться между месяцами** с помощью кнопок навигации
|
||||||
|
- 🔗 **Выбирать даты** для фильтрации заметок (функция может быть расширена)
|
||||||
|
- 📱 **Адаптивность** - на мобильных устройствах календарь располагается на всю ширину выше формы
|
||||||
|
|
||||||
|
### Характеристики календаря
|
||||||
|
|
||||||
|
- Сегодняшний день выделен синим цветом
|
||||||
|
- Выбранный день отмечен зелёным цветом
|
||||||
|
- Дни из соседних месяцев отображаются серым цветом
|
||||||
|
- Календарь использует русские названия месяцев и дней недели
|
||||||
|
- Полностью функционален без необходимости в дополнительных библиотеках
|
||||||
|
|||||||
247
app.js
@ -1,247 +0,0 @@
|
|||||||
const textInput = document.querySelector(".textInput");
|
|
||||||
const btnSave = document.querySelector(".btnSave");
|
|
||||||
const notes = document.querySelector(".notes-container");
|
|
||||||
|
|
||||||
// Получаем кнопки
|
|
||||||
const boldBtn = document.getElementById("boldBtn");
|
|
||||||
const italicBtn = document.getElementById("italicBtn");
|
|
||||||
const headerBtn = document.getElementById("headerBtn");
|
|
||||||
const listBtn = document.getElementById("listBtn");
|
|
||||||
const quoteBtn = document.getElementById("quoteBtn");
|
|
||||||
const codeBtn = document.getElementById("codeBtn");
|
|
||||||
const linkBtn = document.getElementById("linkBtn");
|
|
||||||
|
|
||||||
function getFormattedDateTime() {
|
|
||||||
let now = new Date();
|
|
||||||
let day = String(now.getDate()).padStart(2, "0");
|
|
||||||
let month = String(now.getMonth() + 1).padStart(2, "0");
|
|
||||||
let year = now.getFullYear();
|
|
||||||
let hours = String(now.getHours()).padStart(2, "0");
|
|
||||||
let minutes = String(now.getMinutes()).padStart(2, "0");
|
|
||||||
|
|
||||||
return {
|
|
||||||
date: `${day}.${month}.${year}`,
|
|
||||||
time: `${hours}:${minutes}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Сохранить заметки в localStorage
|
|
||||||
function saveNotesToLocalStorage(notesArr) {
|
|
||||||
localStorage.setItem("notes", JSON.stringify(notesArr));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Получить заметки из localStorage
|
|
||||||
function getNotesFromLocalStorage() {
|
|
||||||
return JSON.parse(localStorage.getItem("notes")) || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обновить функцию renderNotes
|
|
||||||
function renderNotes() {
|
|
||||||
const notesArr = getNotesFromLocalStorage();
|
|
||||||
notes.innerHTML = ""; // Очищаем контейнер перед рендерингом
|
|
||||||
|
|
||||||
notesArr.forEach(function (content, index) {
|
|
||||||
const noteHtml = `
|
|
||||||
<div id="note" class="container">
|
|
||||||
<div class="date">
|
|
||||||
${content.date} ${content.time}
|
|
||||||
<div id="editBtn" class="notesHeaderBtn" data-index="${index}">Редактировать</div>
|
|
||||||
<div id="deleteBtn" class="notesHeaderBtn" data-index="${index}">Удалить</div>
|
|
||||||
</div>
|
|
||||||
<div class="textNote">${marked.parse(content.content)}</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
notes.insertAdjacentHTML("afterbegin", noteHtml);
|
|
||||||
});
|
|
||||||
|
|
||||||
deleteNote();
|
|
||||||
editNote();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обновить функцию saveNote
|
|
||||||
function saveNote() {
|
|
||||||
btnSave.addEventListener("click", function () {
|
|
||||||
if (textInput.value.trim() !== "") {
|
|
||||||
let { date, time } = getFormattedDateTime();
|
|
||||||
|
|
||||||
const note = {
|
|
||||||
content: textInput.value,
|
|
||||||
date: date,
|
|
||||||
time: time,
|
|
||||||
};
|
|
||||||
|
|
||||||
const notesArr = getNotesFromLocalStorage();
|
|
||||||
notesArr.push(note);
|
|
||||||
saveNotesToLocalStorage(notesArr);
|
|
||||||
|
|
||||||
textInput.value = "";
|
|
||||||
textInput.style.height = "auto"; // Сбрасываем размер текстового поля
|
|
||||||
renderNotes();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обновить функцию deleteNote
|
|
||||||
function deleteNote() {
|
|
||||||
document.querySelectorAll("#deleteBtn").forEach((btn) => {
|
|
||||||
btn.addEventListener("click", function (event) {
|
|
||||||
let index = event.target.dataset.index;
|
|
||||||
const notesArr = getNotesFromLocalStorage();
|
|
||||||
notesArr.splice(index, 1);
|
|
||||||
saveNotesToLocalStorage(notesArr);
|
|
||||||
renderNotes();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обновить функцию editNote
|
|
||||||
function editNote() {
|
|
||||||
document.querySelectorAll("#editBtn").forEach((btn) => {
|
|
||||||
btn.addEventListener("click", function (event) {
|
|
||||||
let index = event.target.dataset.index;
|
|
||||||
let noteContainer = event.target.closest("#note");
|
|
||||||
let noteContent = noteContainer.querySelector(".textNote");
|
|
||||||
|
|
||||||
// Создаем textarea с уже существующим классом textInput
|
|
||||||
let textarea = document.createElement("textarea");
|
|
||||||
textarea.classList.add("textInput");
|
|
||||||
textarea.value = noteContent.textContent;
|
|
||||||
|
|
||||||
// Привязываем авторасширение к textarea для редактирования
|
|
||||||
textarea.addEventListener("input", function () {
|
|
||||||
autoExpandTextarea(textarea);
|
|
||||||
});
|
|
||||||
autoExpandTextarea(textarea);
|
|
||||||
|
|
||||||
// Кнопка сохранить
|
|
||||||
let saveEditBtn = document.createElement("button");
|
|
||||||
saveEditBtn.textContent = "Сохранить";
|
|
||||||
saveEditBtn.classList.add("btnSave");
|
|
||||||
|
|
||||||
// Очищаем текущий контент и вставляем textarea и кнопку сохранить
|
|
||||||
noteContent.innerHTML = "";
|
|
||||||
noteContent.appendChild(textarea);
|
|
||||||
noteContent.appendChild(saveEditBtn);
|
|
||||||
|
|
||||||
saveEditBtn.addEventListener("click", function () {
|
|
||||||
if (textarea.value.trim() !== "") {
|
|
||||||
let { date, time } = getFormattedDateTime();
|
|
||||||
const notesArr = getNotesFromLocalStorage();
|
|
||||||
notesArr[index] = {
|
|
||||||
content: textarea.value,
|
|
||||||
date: date,
|
|
||||||
time: time,
|
|
||||||
};
|
|
||||||
saveNotesToLocalStorage(notesArr);
|
|
||||||
renderNotes();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Функция для авторасширения текстового поля
|
|
||||||
function autoExpandTextarea(textarea) {
|
|
||||||
textarea.style.height = "auto";
|
|
||||||
textarea.style.height = textarea.scrollHeight + "px";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Привязываем авторасширение к текстовому полю для создания заметки
|
|
||||||
textInput.addEventListener("input", function () {
|
|
||||||
autoExpandTextarea(textInput);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Изначально запускаем для установки правильной высоты
|
|
||||||
autoExpandTextarea(textInput);
|
|
||||||
|
|
||||||
function insertMarkdown(tag) {
|
|
||||||
const start = textInput.selectionStart;
|
|
||||||
const end = textInput.selectionEnd;
|
|
||||||
const text = textInput.value;
|
|
||||||
|
|
||||||
const before = text.substring(0, start);
|
|
||||||
const selected = text.substring(start, end);
|
|
||||||
const after = text.substring(end);
|
|
||||||
|
|
||||||
if (selected.startsWith(tag) && selected.endsWith(tag)) {
|
|
||||||
// Если теги уже есть, удаляем их
|
|
||||||
textInput.value = `${before}${selected.slice(
|
|
||||||
tag.length,
|
|
||||||
-tag.length
|
|
||||||
)}${after}`;
|
|
||||||
textInput.setSelectionRange(start, end - 2 * tag.length);
|
|
||||||
} else if (selected.trim() === "") {
|
|
||||||
// Если текст не выделен
|
|
||||||
if (tag === "[Текст ссылки](URL)") {
|
|
||||||
// Для ссылок создаем шаблон с двумя кавычками
|
|
||||||
textInput.value = `${before}[Текст ссылки](URL)${after}`;
|
|
||||||
const cursorPosition = start + 1; // Помещаем курсор внутрь текста ссылки
|
|
||||||
textInput.setSelectionRange(cursorPosition, cursorPosition + 12);
|
|
||||||
} else if (tag === "- " || tag === "> " || tag === "# ") {
|
|
||||||
// Для списка, цитаты и заголовка помещаем курсор после `- `, `> ` или `# `
|
|
||||||
textInput.value = `${before}${tag}${after}`;
|
|
||||||
const cursorPosition = start + tag.length;
|
|
||||||
textInput.setSelectionRange(cursorPosition, cursorPosition);
|
|
||||||
} else {
|
|
||||||
// Для остальных типов создаем два тега
|
|
||||||
textInput.value = `${before}${tag}${tag}${after}`;
|
|
||||||
const cursorPosition = start + tag.length;
|
|
||||||
textInput.setSelectionRange(cursorPosition, cursorPosition);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Если текст выделен
|
|
||||||
if (tag === "[Текст ссылки](URL)") {
|
|
||||||
// Для ссылок используем выделенный текст вместо "Текст ссылки"
|
|
||||||
textInput.value = `${before}[${selected}](URL)${after}`;
|
|
||||||
const cursorPosition = start + selected.length + 3; // Помещаем курсор в URL
|
|
||||||
textInput.setSelectionRange(cursorPosition, cursorPosition + 3);
|
|
||||||
} else if (tag === "- " || tag === "> " || tag === "# ") {
|
|
||||||
// Для списка, цитаты и заголовка добавляем `- `, `> ` или `# ` перед выделенным текстом
|
|
||||||
textInput.value = `${before}${tag}${selected}${after}`;
|
|
||||||
const cursorPosition = start + tag.length + selected.length;
|
|
||||||
textInput.setSelectionRange(cursorPosition, cursorPosition);
|
|
||||||
} else {
|
|
||||||
// Для остальных типов оборачиваем выделенный текст
|
|
||||||
textInput.value = `${before}${tag}${selected}${tag}${after}`;
|
|
||||||
const cursorPosition = start + tag.length + selected.length + tag.length;
|
|
||||||
textInput.setSelectionRange(cursorPosition, cursorPosition);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
textInput.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обработчики для кнопок
|
|
||||||
boldBtn.addEventListener("click", function () {
|
|
||||||
insertMarkdown("**"); // Вставляем жирный текст
|
|
||||||
});
|
|
||||||
|
|
||||||
italicBtn.addEventListener("click", function () {
|
|
||||||
insertMarkdown("*"); // Вставляем курсив
|
|
||||||
});
|
|
||||||
|
|
||||||
headerBtn.addEventListener("click", function () {
|
|
||||||
insertMarkdown("# "); // Вставляем заголовок
|
|
||||||
});
|
|
||||||
|
|
||||||
listBtn.addEventListener("click", function () {
|
|
||||||
insertMarkdown("- "); // Вставляем элемент списка
|
|
||||||
});
|
|
||||||
|
|
||||||
quoteBtn.addEventListener("click", function () {
|
|
||||||
insertMarkdown("> "); // Вставляем цитату
|
|
||||||
});
|
|
||||||
|
|
||||||
codeBtn.addEventListener("click", function () {
|
|
||||||
insertMarkdown("`"); // Вставляем код
|
|
||||||
});
|
|
||||||
|
|
||||||
linkBtn.addEventListener("click", function () {
|
|
||||||
insertMarkdown("[Текст ссылки](URL)"); // Вставляем ссылку
|
|
||||||
});
|
|
||||||
|
|
||||||
// Удалено дублирование добавления кнопок Markdown в окно сохранения заметки
|
|
||||||
// Кнопки уже добавлены в HTML (index.html), поэтому их повторное создание не требуется
|
|
||||||
|
|
||||||
renderNotes();
|
|
||||||
saveNote();
|
|
||||||
32
apply-changes.sh
Executable file
@ -0,0 +1,32 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Скрипт для применения изменений в NoteJS
|
||||||
|
# Создан: $(date)
|
||||||
|
|
||||||
|
echo "Применение изменений NoteJS..."
|
||||||
|
|
||||||
|
# Проверяем, что мы в правильной директории
|
||||||
|
if [ ! -f "package.json" ]; then
|
||||||
|
echo "Ошибка: Запустите скрипт из корневой директории проекта NoteJS"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Применяем патчи
|
||||||
|
echo "Применение патчей..."
|
||||||
|
git apply 0001-feat.patch
|
||||||
|
git apply 0002-feat.patch
|
||||||
|
|
||||||
|
echo "Изменения применены успешно!"
|
||||||
|
echo ""
|
||||||
|
echo "Что было добавлено:"
|
||||||
|
echo "- Личный кабинет с возможностью загрузки аватарки"
|
||||||
|
echo "- Управление аватарками: загрузка, удаление, предварительный просмотр"
|
||||||
|
echo "- Исправлено отображение аватарки на странице профиля"
|
||||||
|
echo "- Убрано отображение аватарки со страницы заметок"
|
||||||
|
echo "- Обновлен .gitignore с исключениями для загруженных файлов"
|
||||||
|
echo "- Обновлен README.md с документацией по новым функциям"
|
||||||
|
echo ""
|
||||||
|
echo "Для коммита изменений выполните:"
|
||||||
|
echo "git add ."
|
||||||
|
echo "git commit -m 'feat: добавлен личный кабинет с аватарками и улучшена навигация'"
|
||||||
|
echo "git push origin main"
|
||||||
BIN
clickable_tags_final_screenshot.png
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
favicon-16.png
Normal file
|
After Width: | Height: | Size: 172 B |
BIN
favicon-32.png
Normal file
|
After Width: | Height: | Size: 246 B |
BIN
favicon.ico
Normal file
|
After Width: | Height: | Size: 708 B |
BIN
icon-192.png
Normal file
|
After Width: | Height: | Size: 921 B |
BIN
icon-512.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
19
index.html
@ -5,10 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Document</title>
|
<title>Document</title>
|
||||||
<link rel="stylesheet" href="style.css" />
|
<link rel="stylesheet" href="style.css" />
|
||||||
<link
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/iconify/2.0.0/iconify.min.js"></script>
|
||||||
rel="stylesheet"
|
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"
|
|
||||||
/>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@ -16,31 +13,31 @@
|
|||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="markdown-buttons">
|
<div class="markdown-buttons">
|
||||||
<button class="btnMarkdown" id="boldBtn">
|
<button class="btnMarkdown" id="boldBtn">
|
||||||
<i class="fas fa-bold"></i>
|
<span class="iconify" data-icon="mdi:format-bold"></span>
|
||||||
<!-- Иконка для жирного текста -->
|
<!-- Иконка для жирного текста -->
|
||||||
</button>
|
</button>
|
||||||
<button class="btnMarkdown" id="italicBtn">
|
<button class="btnMarkdown" id="italicBtn">
|
||||||
<i class="fas fa-italic"></i>
|
<span class="iconify" data-icon="mdi:format-italic"></span>
|
||||||
<!-- Иконка для курсива -->
|
<!-- Иконка для курсива -->
|
||||||
</button>
|
</button>
|
||||||
<button class="btnMarkdown" id="headerBtn">
|
<button class="btnMarkdown" id="headerBtn">
|
||||||
<i class="fas fa-heading"></i>
|
<span class="iconify" data-icon="mdi:format-header-1"></span>
|
||||||
<!-- Иконка для заголовка -->
|
<!-- Иконка для заголовка -->
|
||||||
</button>
|
</button>
|
||||||
<button class="btnMarkdown" id="listBtn">
|
<button class="btnMarkdown" id="listBtn">
|
||||||
<i class="fas fa-list-ul"></i>
|
<span class="iconify" data-icon="mdi:format-list-bulleted"></span>
|
||||||
<!-- Иконка для списка -->
|
<!-- Иконка для списка -->
|
||||||
</button>
|
</button>
|
||||||
<button class="btnMarkdown" id="quoteBtn">
|
<button class="btnMarkdown" id="quoteBtn">
|
||||||
<i class="fas fa-quote-right"></i>
|
<span class="iconify" data-icon="mdi:format-quote-close"></span>
|
||||||
<!-- Иконка для цитаты -->
|
<!-- Иконка для цитаты -->
|
||||||
</button>
|
</button>
|
||||||
<button class="btnMarkdown" id="codeBtn">
|
<button class="btnMarkdown" id="codeBtn">
|
||||||
<i class="fas fa-code"></i>
|
<span class="iconify" data-icon="mdi:code-tags"></span>
|
||||||
<!-- Иконка для кода -->
|
<!-- Иконка для кода -->
|
||||||
</button>
|
</button>
|
||||||
<button class="btnMarkdown" id="linkBtn">
|
<button class="btnMarkdown" id="linkBtn">
|
||||||
<i class="fas fa-link"></i>
|
<span class="iconify" data-icon="mdi:link"></span>
|
||||||
<!-- Иконка для ссылки -->
|
<!-- Иконка для ссылки -->
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -7,12 +7,12 @@
|
|||||||
"theme_color": "#000000",
|
"theme_color": "#000000",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "project.png",
|
"src": "icon-192.png",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "project.png",
|
"src": "icon-512.png",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
}
|
}
|
||||||
|
|||||||
18
package-lock.json
generated
@ -14,6 +14,7 @@
|
|||||||
"@codemirror/state": "^6.5.2",
|
"@codemirror/state": "^6.5.2",
|
||||||
"@codemirror/theme-one-dark": "^6.1.3",
|
"@codemirror/theme-one-dark": "^6.1.3",
|
||||||
"@codemirror/view": "^6.38.6",
|
"@codemirror/view": "^6.38.6",
|
||||||
|
"@iconify/iconify": "^3.1.1",
|
||||||
"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",
|
||||||
@ -507,6 +508,23 @@
|
|||||||
"integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==",
|
"integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@iconify/iconify": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@iconify/iconify/-/iconify-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-1nemfyD/OJzh9ALepH7YfuuP8BdEB24Skhd8DXWh0hzcOxImbb1ZizSZkpCzAwSZSGcJFmscIBaBQu+yLyWaxQ==",
|
||||||
|
"deprecated": "no longer maintained, switch to modern iconify-icon web component",
|
||||||
|
"dependencies": {
|
||||||
|
"@iconify/types": "^2.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/cyberalien"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@iconify/types": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="
|
||||||
|
},
|
||||||
"node_modules/@lezer/common": {
|
"node_modules/@lezer/common": {
|
||||||
"version": "0.16.1",
|
"version": "0.16.1",
|
||||||
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-0.16.1.tgz",
|
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-0.16.1.tgz",
|
||||||
|
|||||||
@ -20,6 +20,7 @@
|
|||||||
"@codemirror/state": "^6.5.2",
|
"@codemirror/state": "^6.5.2",
|
||||||
"@codemirror/theme-one-dark": "^6.1.3",
|
"@codemirror/theme-one-dark": "^6.1.3",
|
||||||
"@codemirror/view": "^6.38.6",
|
"@codemirror/view": "^6.38.6",
|
||||||
|
"@iconify/iconify": "^3.1.1",
|
||||||
"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",
|
||||||
|
|||||||
2
plan.txt
@ -1,2 +0,0 @@
|
|||||||
✅ 1. Сделать создание заметки по нажатию alt + enter. И написать подсказку возле кнопки Сохранить.
|
|
||||||
✅ 2. Добавить личный кабинет, который открывается при нажатии на свой ник. В личном кабинете можно менять регистрационные данные, а так же привязать свой эмейл и поставить аватарку. Картинки аватарки загружаются на сервер.
|
|
||||||
360
public/app.js
@ -15,6 +15,9 @@ const linkBtn = document.getElementById("linkBtn");
|
|||||||
// Глобальные переменные для заметок и фильтрации
|
// Глобальные переменные для заметок и фильтрации
|
||||||
let allNotes = [];
|
let allNotes = [];
|
||||||
let selectedDateFilter = null;
|
let selectedDateFilter = null;
|
||||||
|
let selectedTagFilter = null;
|
||||||
|
let searchQuery = "";
|
||||||
|
let searchResults = [];
|
||||||
|
|
||||||
// Функция для получения текущей даты и времени
|
// Функция для получения текущей даты и времени
|
||||||
function getFormattedDateTime() {
|
function getFormattedDateTime() {
|
||||||
@ -37,6 +40,121 @@ function autoExpandTextarea(textarea) {
|
|||||||
textarea.style.height = textarea.scrollHeight + "px";
|
textarea.style.height = textarea.scrollHeight + "px";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Функция для извлечения тегов из текста заметки
|
||||||
|
function extractTags(content) {
|
||||||
|
const tagRegex = /#(\w+)/g;
|
||||||
|
const tags = [];
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = tagRegex.exec(content)) !== null) {
|
||||||
|
const tag = match[1].toLowerCase();
|
||||||
|
if (!tags.includes(tag)) {
|
||||||
|
tags.push(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для преобразования тегов в заметках в кликабельные элементы
|
||||||
|
function makeTagsClickable(content) {
|
||||||
|
// Сначала находим все теги, которые еще не обернуты в HTML
|
||||||
|
const tagRegex = /#(\w+)/g;
|
||||||
|
let result = content;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
// Создаем массив всех совпадений с их позициями
|
||||||
|
const matches = [];
|
||||||
|
while ((match = tagRegex.exec(content)) !== null) {
|
||||||
|
matches.push({
|
||||||
|
fullMatch: match[0],
|
||||||
|
tag: match[1],
|
||||||
|
index: match.index,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обрабатываем совпадения в обратном порядке, чтобы не сбить индексы
|
||||||
|
for (let i = matches.length - 1; i >= 0; i--) {
|
||||||
|
const match = matches[i];
|
||||||
|
const beforeTag = result.substring(0, match.index);
|
||||||
|
const afterTag = result.substring(match.index + match.fullMatch.length);
|
||||||
|
|
||||||
|
// Проверяем, не находится ли тег уже внутри HTML-тега
|
||||||
|
const lastOpenTag = beforeTag.lastIndexOf("<");
|
||||||
|
const lastCloseTag = beforeTag.lastIndexOf(">");
|
||||||
|
|
||||||
|
// Если последний открывающий тег идет после последнего закрывающего, значит мы внутри HTML-тега
|
||||||
|
if (lastOpenTag > lastCloseTag) {
|
||||||
|
continue; // Пропускаем этот тег
|
||||||
|
}
|
||||||
|
|
||||||
|
// Заменяем тег на кликабельный элемент
|
||||||
|
const replacement = `<span class="tag-in-note" data-tag="${match.tag}">${match.fullMatch}</span>`;
|
||||||
|
result = beforeTag + replacement + afterTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для получения всех уникальных тегов из заметок
|
||||||
|
function getAllTags(notes) {
|
||||||
|
const tagCounts = {};
|
||||||
|
|
||||||
|
notes.forEach((note) => {
|
||||||
|
const tags = extractTags(note.content);
|
||||||
|
tags.forEach((tag) => {
|
||||||
|
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return tagCounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для отображения тегов
|
||||||
|
function renderTags() {
|
||||||
|
const tagsContainer = document.getElementById("tagsContainer");
|
||||||
|
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", handleTagClick);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработчик клика на тег
|
||||||
|
function handleTagClick(event) {
|
||||||
|
const clickedTag = event.target.closest(".tag").dataset.tag;
|
||||||
|
|
||||||
|
// Если кликнули на тот же тег, снимаем фильтр
|
||||||
|
if (selectedTagFilter === clickedTag) {
|
||||||
|
selectedTagFilter = null;
|
||||||
|
} else {
|
||||||
|
selectedTagFilter = clickedTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Перерисовываем заметки и теги
|
||||||
|
renderNotes(allNotes);
|
||||||
|
renderTags();
|
||||||
|
updateFilterIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
// Привязываем авторасширение к текстовому полю для создания заметки
|
// Привязываем авторасширение к текстовому полю для создания заметки
|
||||||
noteInput.addEventListener("input", function () {
|
noteInput.addEventListener("input", function () {
|
||||||
autoExpandTextarea(noteInput);
|
autoExpandTextarea(noteInput);
|
||||||
@ -201,35 +319,112 @@ async function loadNotes() {
|
|||||||
allNotes = notes; // Сохраняем все заметки в глобальную переменную
|
allNotes = notes; // Сохраняем все заметки в глобальную переменную
|
||||||
renderNotes(notes);
|
renderNotes(notes);
|
||||||
renderCalendar(); // Обновляем календарь после загрузки заметок
|
renderCalendar(); // Обновляем календарь после загрузки заметок
|
||||||
|
renderTags(); // Обновляем теги после загрузки заметок
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Ошибка:", error);
|
console.error("Ошибка:", error);
|
||||||
notesList.innerHTML = "<p>Ошибка загрузки заметок</p>";
|
notesList.innerHTML = "<p>Ошибка загрузки заметок</p>";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Функция для поиска заметок
|
||||||
|
async function searchNotes(query) {
|
||||||
|
if (!query || query.trim() === "") {
|
||||||
|
searchQuery = "";
|
||||||
|
searchResults = [];
|
||||||
|
renderNotes(allNotes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append("q", query.trim());
|
||||||
|
|
||||||
|
// Добавляем фильтры, если они активны
|
||||||
|
if (selectedTagFilter) {
|
||||||
|
params.append("tag", selectedTagFilter);
|
||||||
|
}
|
||||||
|
if (selectedDateFilter) {
|
||||||
|
params.append("date", selectedDateFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`/api/notes/search?${params}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Ошибка поиска заметок");
|
||||||
|
}
|
||||||
|
|
||||||
|
searchResults = await response.json();
|
||||||
|
searchQuery = query.trim();
|
||||||
|
renderNotes(searchResults);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Ошибка поиска:", error);
|
||||||
|
searchResults = [];
|
||||||
|
renderNotes(allNotes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для подсветки найденного текста
|
||||||
|
function highlightSearchText(content, query) {
|
||||||
|
if (!query || query.trim() === "") {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
const regex = new RegExp(
|
||||||
|
`(${query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`,
|
||||||
|
"gi"
|
||||||
|
);
|
||||||
|
return content.replace(regex, '<span class="search-highlight">$1</span>');
|
||||||
|
}
|
||||||
|
|
||||||
// Функция для отображения заметок
|
// Функция для отображения заметок
|
||||||
function renderNotes(notes) {
|
function renderNotes(notes) {
|
||||||
notesList.innerHTML = "";
|
notesList.innerHTML = "";
|
||||||
|
|
||||||
// Фильтруем заметки, если выбрана дата
|
// Фильтруем заметки по дате и тегам
|
||||||
let notesToDisplay = notes;
|
let notesToDisplay = notes;
|
||||||
|
|
||||||
if (selectedDateFilter) {
|
if (selectedDateFilter) {
|
||||||
notesToDisplay = notes.filter((note) => note.date === selectedDateFilter);
|
notesToDisplay = notesToDisplay.filter(
|
||||||
|
(note) => note.date === selectedDateFilter
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedTagFilter) {
|
||||||
|
notesToDisplay = notesToDisplay.filter((note) => {
|
||||||
|
const tags = extractTags(note.content);
|
||||||
|
return tags.includes(selectedTagFilter);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если нет заметок для отображения
|
// Если нет заметок для отображения
|
||||||
if (notesToDisplay.length === 0) {
|
if (notesToDisplay.length === 0) {
|
||||||
if (selectedDateFilter) {
|
let message = "Заметок пока нет";
|
||||||
notesList.innerHTML = `<div class="container"><p style="text-align: center; color: #999;">Нет заметок за выбранную дату (${selectedDateFilter})</p></div>`;
|
|
||||||
} else {
|
if (selectedDateFilter && selectedTagFilter) {
|
||||||
notesList.innerHTML =
|
message = `Нет заметок за ${selectedDateFilter} с тегом #${selectedTagFilter}`;
|
||||||
'<div class="container"><p style="text-align: center; color: #999;">Заметок пока нет</p></div>';
|
} else if (selectedDateFilter) {
|
||||||
|
message = `Нет заметок за выбранную дату (${selectedDateFilter})`;
|
||||||
|
} else if (selectedTagFilter) {
|
||||||
|
message = `Нет заметок с тегом #${selectedTagFilter}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notesList.innerHTML = `<div class="container"><p style="text-align: center; color: #999;">${message}</p></div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Итерируемся по заметкам в обычном порядке, чтобы новые были сверху
|
// Итерируемся по заметкам в обычном порядке, чтобы новые были сверху
|
||||||
notesToDisplay.forEach(function (note) {
|
notesToDisplay.forEach(function (note) {
|
||||||
|
let contentToProcess = note.content;
|
||||||
|
|
||||||
|
// Сначала подсвечиваем найденный текст в исходном markdown
|
||||||
|
if (searchQuery) {
|
||||||
|
contentToProcess = highlightSearchText(contentToProcess, searchQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Затем преобразуем теги в кликабельные элементы
|
||||||
|
const contentWithClickableTags = makeTagsClickable(contentToProcess);
|
||||||
|
|
||||||
|
const parsedContent = marked.parse(contentWithClickableTags);
|
||||||
|
|
||||||
const noteHtml = `
|
const noteHtml = `
|
||||||
<div id="note" class="container">
|
<div id="note" class="container">
|
||||||
<div class="date">
|
<div class="date">
|
||||||
@ -244,7 +439,7 @@ function renderNotes(notes) {
|
|||||||
<div class="textNote" data-original-content="${note.content.replace(
|
<div class="textNote" data-original-content="${note.content.replace(
|
||||||
/"/g,
|
/"/g,
|
||||||
"""
|
"""
|
||||||
)}">${marked.parse(note.content)}</div>
|
)}">${parsedContent}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
notesList.insertAdjacentHTML("afterbegin", noteHtml);
|
notesList.insertAdjacentHTML("afterbegin", noteHtml);
|
||||||
@ -252,6 +447,9 @@ function renderNotes(notes) {
|
|||||||
|
|
||||||
// Добавляем обработчики событий для кнопок редактирования и удаления
|
// Добавляем обработчики событий для кнопок редактирования и удаления
|
||||||
addNoteEventListeners();
|
addNoteEventListeners();
|
||||||
|
|
||||||
|
// Добавляем обработчики кликов для тегов в заметках
|
||||||
|
addTagClickListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Функция для добавления обработчиков событий к заметкам
|
// Функция для добавления обработчиков событий к заметкам
|
||||||
@ -293,20 +491,20 @@ function addNoteEventListeners() {
|
|||||||
|
|
||||||
// Создаем markdown кнопки
|
// Создаем markdown кнопки
|
||||||
const markdownButtons = [
|
const markdownButtons = [
|
||||||
{ id: "editBoldBtn", icon: "fas fa-bold", tag: "**" },
|
{ id: "editBoldBtn", icon: "mdi:format-bold", tag: "**" },
|
||||||
{ id: "editItalicBtn", icon: "fas fa-italic", tag: "*" },
|
{ id: "editItalicBtn", icon: "mdi:format-italic", tag: "*" },
|
||||||
{ id: "editHeaderBtn", icon: "fas fa-heading", tag: "# " },
|
{ id: "editHeaderBtn", icon: "mdi:format-header-1", tag: "# " },
|
||||||
{ id: "editListBtn", icon: "fas fa-list-ul", tag: "- " },
|
{ id: "editListBtn", icon: "mdi:format-list-bulleted", tag: "- " },
|
||||||
{ id: "editQuoteBtn", icon: "fas fa-quote-right", tag: "> " },
|
{ id: "editQuoteBtn", icon: "mdi:format-quote-close", tag: "> " },
|
||||||
{ id: "editCodeBtn", icon: "fas fa-code", tag: "`" },
|
{ id: "editCodeBtn", icon: "mdi:code-tags", tag: "`" },
|
||||||
{ id: "editLinkBtn", icon: "fas fa-link", tag: "[Текст ссылки](URL)" },
|
{ id: "editLinkBtn", icon: "mdi:link", tag: "[Текст ссылки](URL)" },
|
||||||
];
|
];
|
||||||
|
|
||||||
markdownButtons.forEach((button) => {
|
markdownButtons.forEach((button) => {
|
||||||
const btn = document.createElement("button");
|
const btn = document.createElement("button");
|
||||||
btn.classList.add("btnMarkdown");
|
btn.classList.add("btnMarkdown");
|
||||||
btn.id = button.id;
|
btn.id = button.id;
|
||||||
btn.innerHTML = `<i class="${button.icon}"></i>`;
|
btn.innerHTML = `<span class="iconify" data-icon="${button.icon}"></span>`;
|
||||||
markdownButtonsContainer.appendChild(btn);
|
markdownButtonsContainer.appendChild(btn);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -375,6 +573,30 @@ function addNoteEventListeners() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Функция для добавления обработчиков кликов на теги в заметках
|
||||||
|
function addTagClickListeners() {
|
||||||
|
document.querySelectorAll(".textNote .tag-in-note").forEach((tagElement) => {
|
||||||
|
tagElement.addEventListener("click", function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const clickedTag = this.dataset.tag.toLowerCase();
|
||||||
|
|
||||||
|
// Если кликнули на тот же тег, снимаем фильтр
|
||||||
|
if (selectedTagFilter === clickedTag) {
|
||||||
|
selectedTagFilter = null;
|
||||||
|
} else {
|
||||||
|
selectedTagFilter = clickedTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Перерисовываем заметки и теги
|
||||||
|
renderNotes(allNotes);
|
||||||
|
renderTags();
|
||||||
|
updateFilterIndicator();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Функция сохранения заметки (вынесена отдельно для повторного использования)
|
// Функция сохранения заметки (вынесена отдельно для повторного использования)
|
||||||
async function saveNote() {
|
async function saveNote() {
|
||||||
if (noteInput.value.trim() !== "") {
|
if (noteInput.value.trim() !== "") {
|
||||||
@ -439,7 +661,7 @@ async function loadUserInfo() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (usernameDisplay) {
|
if (usernameDisplay) {
|
||||||
usernameDisplay.textContent = `👤 ${user.username}`;
|
usernameDisplay.innerHTML = `<span class="iconify" data-icon="mdi:account"></span> ${user.username}`;
|
||||||
|
|
||||||
// Делаем ник кликабельным для перехода в личный кабинет
|
// Делаем ник кликабельным для перехода в личный кабинет
|
||||||
usernameDisplay.style.cursor = "pointer";
|
usernameDisplay.style.cursor = "pointer";
|
||||||
@ -448,16 +670,9 @@ async function loadUserInfo() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Отображаем аватарку, если она есть
|
// Аватарка скрыта на странице заметок
|
||||||
if (user.avatar && userAvatar && userAvatarContainer) {
|
if (userAvatarContainer) {
|
||||||
userAvatar.src = user.avatar;
|
userAvatarContainer.style.display = "none";
|
||||||
userAvatarContainer.style.display = "inline-block";
|
|
||||||
|
|
||||||
// Аватарка также кликабельна
|
|
||||||
userAvatarContainer.style.cursor = "pointer";
|
|
||||||
userAvatarContainer.addEventListener("click", function () {
|
|
||||||
window.location.href = "/profile";
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -630,9 +845,10 @@ function handleDayClick(event) {
|
|||||||
selectedDateFilter = clickedDate;
|
selectedDateFilter = clickedDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Перерисовываем заметки и календарь
|
// Перерисовываем заметки, календарь и теги
|
||||||
renderNotes(allNotes);
|
renderNotes(allNotes);
|
||||||
renderCalendar();
|
renderCalendar();
|
||||||
|
renderTags();
|
||||||
updateFilterIndicator();
|
updateFilterIndicator();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -641,9 +857,25 @@ function updateFilterIndicator() {
|
|||||||
const filterIndicator = document.getElementById("filter-indicator");
|
const filterIndicator = document.getElementById("filter-indicator");
|
||||||
if (!filterIndicator) return;
|
if (!filterIndicator) return;
|
||||||
|
|
||||||
|
const filters = [];
|
||||||
|
|
||||||
|
if (searchQuery) {
|
||||||
|
filters.push(`Поиск: "${searchQuery}"`);
|
||||||
|
}
|
||||||
|
|
||||||
if (selectedDateFilter) {
|
if (selectedDateFilter) {
|
||||||
|
filters.push(`Дата: ${selectedDateFilter}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedTagFilter) {
|
||||||
|
filters.push(`Тег: #${selectedTagFilter}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.length > 0) {
|
||||||
filterIndicator.style.display = "inline-block";
|
filterIndicator.style.display = "inline-block";
|
||||||
filterIndicator.innerHTML = `Фильтр: ${selectedDateFilter} <button id="clear-filter-btn">✕</button>`;
|
filterIndicator.innerHTML = `Фильтр: ${filters.join(
|
||||||
|
", "
|
||||||
|
)} <button id="clear-filter-btn">✕</button>`;
|
||||||
|
|
||||||
// Добавляем обработчик клика для кнопки сброса
|
// Добавляем обработчик клика для кнопки сброса
|
||||||
const clearBtn = document.getElementById("clear-filter-btn");
|
const clearBtn = document.getElementById("clear-filter-btn");
|
||||||
@ -658,8 +890,25 @@ function updateFilterIndicator() {
|
|||||||
// Функция для сброса фильтра (глобальная)
|
// Функция для сброса фильтра (глобальная)
|
||||||
window.clearFilter = function () {
|
window.clearFilter = function () {
|
||||||
selectedDateFilter = null;
|
selectedDateFilter = null;
|
||||||
|
selectedTagFilter = null;
|
||||||
|
searchQuery = "";
|
||||||
|
searchResults = [];
|
||||||
|
|
||||||
|
// Очищаем поле поиска
|
||||||
|
const searchInput = document.getElementById("searchInput");
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Скрываем кнопку очистки поиска
|
||||||
|
const clearSearchBtn = document.getElementById("clearSearchBtn");
|
||||||
|
if (clearSearchBtn) {
|
||||||
|
clearSearchBtn.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
renderNotes(allNotes);
|
renderNotes(allNotes);
|
||||||
renderCalendar();
|
renderCalendar();
|
||||||
|
renderTags();
|
||||||
updateFilterIndicator();
|
updateFilterIndicator();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -684,4 +933,57 @@ if (nextMonthBtn) {
|
|||||||
// Инициализируем календарь при загрузке страницы
|
// Инициализируем календарь при загрузке страницы
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
renderCalendar();
|
renderCalendar();
|
||||||
|
|
||||||
|
// Инициализируем поиск
|
||||||
|
initSearch();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Функция для инициализации поиска
|
||||||
|
function initSearch() {
|
||||||
|
const searchInput = document.getElementById("searchInput");
|
||||||
|
const clearSearchBtn = document.getElementById("clearSearchBtn");
|
||||||
|
|
||||||
|
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);
|
||||||
|
updateFilterIndicator();
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработчик клика на кнопку очистки поиска
|
||||||
|
clearSearchBtn.addEventListener("click", function () {
|
||||||
|
searchInput.value = "";
|
||||||
|
this.style.display = "none";
|
||||||
|
searchQuery = "";
|
||||||
|
searchResults = [];
|
||||||
|
renderNotes(allNotes);
|
||||||
|
updateFilterIndicator();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработчик клавиши Escape для очистки поиска
|
||||||
|
searchInput.addEventListener("keydown", function (event) {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
this.value = "";
|
||||||
|
clearSearchBtn.style.display = "none";
|
||||||
|
searchQuery = "";
|
||||||
|
searchResults = [];
|
||||||
|
renderNotes(allNotes);
|
||||||
|
updateFilterIndicator();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -5,14 +5,13 @@
|
|||||||
<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" />
|
<link rel="stylesheet" href="/style.css" />
|
||||||
<link
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/iconify/2.0.0/iconify.min.js"></script>
|
||||||
rel="stylesheet"
|
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"
|
|
||||||
/>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<header>🔐 Вход в систему</header>
|
<header>
|
||||||
|
<span class="iconify" data-icon="mdi:login"></span> Вход в систему
|
||||||
|
</header>
|
||||||
<div class="login-form">
|
<div class="login-form">
|
||||||
<form id="loginForm">
|
<form id="loginForm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@ -5,10 +5,7 @@
|
|||||||
<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=2" />
|
||||||
<link
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/iconify/2.0.0/iconify.min.js"></script>
|
||||||
rel="stylesheet"
|
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"
|
|
||||||
/>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container-leftside">
|
<div class="container-leftside">
|
||||||
@ -29,12 +26,51 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="calendar-days" id="calendarDays"></div>
|
<div class="calendar-days" id="calendarDays"></div>
|
||||||
</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="searchInput"
|
||||||
|
placeholder="Поиск по заметкам..."
|
||||||
|
class="search-input"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
id="clearSearchBtn"
|
||||||
|
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="tagsContainer">
|
||||||
|
<!-- Теги будут добавлены динамически -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<header class="notes-header">
|
<header class="notes-header">
|
||||||
<div class="notes-header-left">
|
<div class="notes-header-left">
|
||||||
<span>📝 Мои заметки</span>
|
<span
|
||||||
|
><span class="iconify" data-icon="mdi:note-text"></span> Мои
|
||||||
|
заметки</span
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
id="filter-indicator"
|
id="filter-indicator"
|
||||||
class="filter-indicator"
|
class="filter-indicator"
|
||||||
@ -62,32 +98,34 @@
|
|||||||
</div>
|
</div>
|
||||||
<span id="username-display" class="username-clickable"></span>
|
<span id="username-display" class="username-clickable"></span>
|
||||||
<form action="/logout" method="POST" style="display: inline">
|
<form action="/logout" method="POST" style="display: inline">
|
||||||
<button type="submit" class="logout-btn">🚪 Выйти</button>
|
<button type="submit" class="logout-btn">
|
||||||
|
<span class="iconify" data-icon="mdi:logout"></span> Выйти
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="markdown-buttons">
|
<div class="markdown-buttons">
|
||||||
<button class="btnMarkdown" id="boldBtn">
|
<button class="btnMarkdown" id="boldBtn" title="Жирный текст">
|
||||||
<i class="fas fa-bold"></i>
|
<span class="iconify" data-icon="mdi:format-bold"></span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btnMarkdown" id="italicBtn">
|
<button class="btnMarkdown" id="italicBtn" title="Курсив">
|
||||||
<i class="fas fa-italic"></i>
|
<span class="iconify" data-icon="mdi:format-italic"></span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btnMarkdown" id="headerBtn">
|
<button class="btnMarkdown" id="headerBtn" title="Заголовок">
|
||||||
<i class="fas fa-heading"></i>
|
<span class="iconify" data-icon="mdi:format-header-1"></span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btnMarkdown" id="listBtn">
|
<button class="btnMarkdown" id="listBtn" title="Список">
|
||||||
<i class="fas fa-list-ul"></i>
|
<span class="iconify" data-icon="mdi:format-list-bulleted"></span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btnMarkdown" id="quoteBtn">
|
<button class="btnMarkdown" id="quoteBtn" title="Цитата">
|
||||||
<i class="fas fa-quote-right"></i>
|
<span class="iconify" data-icon="mdi:format-quote-close"></span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btnMarkdown" id="codeBtn">
|
<button class="btnMarkdown" id="codeBtn" title="Код">
|
||||||
<i class="fas fa-code"></i>
|
<span class="iconify" data-icon="mdi:code-tags"></span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btnMarkdown" id="linkBtn">
|
<button class="btnMarkdown" id="linkBtn" title="Ссылка">
|
||||||
<i class="fas fa-link"></i>
|
<span class="iconify" data-icon="mdi:link"></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -110,5 +148,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/11.1.0/marked.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/11.1.0/marked.min.js"></script>
|
||||||
<script src="/app.js"></script>
|
<script src="/app.js"></script>
|
||||||
|
<script>
|
||||||
|
// Проверяем загрузку Iconify
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
if (typeof Iconify === "undefined") {
|
||||||
|
console.warn(
|
||||||
|
"Iconify не загружен, загружаем альтернативную версию..."
|
||||||
|
);
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.src =
|
||||||
|
"https://cdnjs.cloudflare.com/ajax/libs/iconify/2.0.0/iconify.min.js";
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -5,19 +5,21 @@
|
|||||||
<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=3" />
|
<link rel="stylesheet" href="/style.css?v=3" />
|
||||||
<link
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/iconify/2.0.0/iconify.min.js"></script>
|
||||||
rel="stylesheet"
|
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"
|
|
||||||
/>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<header class="notes-header">
|
<header class="notes-header">
|
||||||
<span>👤 Личный кабинет</span>
|
<span
|
||||||
|
><span class="iconify" data-icon="mdi:account"></span> Личный
|
||||||
|
кабинет</span
|
||||||
|
>
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<a href="/notes" class="back-btn">← Назад к заметкам</a>
|
<a href="/notes" class="back-btn">← Назад к заметкам</a>
|
||||||
<form action="/logout" method="POST" style="display: inline">
|
<form action="/logout" method="POST" style="display: inline">
|
||||||
<button type="submit" class="logout-btn">🚪 Выйти</button>
|
<button type="submit" class="logout-btn">
|
||||||
|
<span class="iconify" data-icon="mdi:logout"></span> Выйти
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@ -34,12 +36,13 @@
|
|||||||
style="display: none"
|
style="display: none"
|
||||||
/>
|
/>
|
||||||
<div id="avatarPlaceholder" class="avatar-placeholder">
|
<div id="avatarPlaceholder" class="avatar-placeholder">
|
||||||
<i class="fas fa-user"></i>
|
<span class="iconify" data-icon="mdi:account"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="avatar-buttons">
|
<div class="avatar-buttons">
|
||||||
<label for="avatarInput" class="btn-upload">
|
<label for="avatarInput" class="btn-upload">
|
||||||
<i class="fas fa-upload"></i> Загрузить аватар
|
<span class="iconify" data-icon="mdi:upload"></span> Загрузить
|
||||||
|
аватар
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
@ -52,7 +55,7 @@
|
|||||||
class="btn-delete"
|
class="btn-delete"
|
||||||
style="display: none"
|
style="display: none"
|
||||||
>
|
>
|
||||||
<i class="fas fa-trash"></i> Удалить
|
<span class="iconify" data-icon="mdi:delete"></span> Удалить
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="avatar-hint">
|
<p class="avatar-hint">
|
||||||
|
|||||||
@ -5,14 +5,13 @@
|
|||||||
<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" />
|
<link rel="stylesheet" href="/style.css" />
|
||||||
<link
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/iconify/2.0.0/iconify.min.js"></script>
|
||||||
rel="stylesheet"
|
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"
|
|
||||||
/>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<header>📝 Регистрация</header>
|
<header>
|
||||||
|
<span class="iconify" data-icon="mdi:account-plus"></span> Регистрация
|
||||||
|
</header>
|
||||||
<div class="login-form">
|
<div class="login-form">
|
||||||
<form id="registerForm">
|
<form id="registerForm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
144
public/style-calendar.css
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
/* Компактные стили для календаря */
|
||||||
|
.main-wrapper {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-box {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 190px;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
min-width: 75px;
|
||||||
|
text-align: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-nav-btn {
|
||||||
|
background: #f0f0f0;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #333;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-nav-btn:hover {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
border-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-weekdays {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
gap: 1px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weekday {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #666;
|
||||||
|
padding: 1px 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-days {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #666;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day:hover {
|
||||||
|
background: #e8f4f8;
|
||||||
|
border-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day.other-month {
|
||||||
|
color: #ccc;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day.today {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
border-color: #0056cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day.today:hover {
|
||||||
|
background: #0056cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day.selected {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
border-color: #1e7e34;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-day.selected:hover {
|
||||||
|
background: #1e7e34;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Адаптация для мобильных устройств */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.main-wrapper {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-box {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
255
public/style.css
@ -10,6 +10,108 @@ body {
|
|||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Стили для Iconify иконок */
|
||||||
|
.iconify {
|
||||||
|
font-size: 16px;
|
||||||
|
vertical-align: middle;
|
||||||
|
display: inline-block;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
iconify-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
vertical-align: middle;
|
||||||
|
display: inline-block;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стили для иконок в заголовках */
|
||||||
|
header .iconify {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стили для иконок в кнопках */
|
||||||
|
.logout-btn .iconify {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стили для иконок в секциях */
|
||||||
|
.search-title .iconify,
|
||||||
|
.tags-title .iconify {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Цветные иконки */
|
||||||
|
/* Иконка поиска - синий */
|
||||||
|
.search-title .iconify[data-icon="mdi:magnify"] {
|
||||||
|
color: #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Иконка тегов - зеленый */
|
||||||
|
.tags-title .iconify[data-icon="mdi:tag"] {
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Иконка заметок - оранжевый */
|
||||||
|
header .iconify[data-icon="mdi:note-text"] {
|
||||||
|
color: #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Иконка пользователя - фиолетовый */
|
||||||
|
header .iconify[data-icon="mdi:account"],
|
||||||
|
.username-clickable .iconify[data-icon="mdi:account"] {
|
||||||
|
color: #9c27b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Иконка выхода - красный */
|
||||||
|
.logout-btn .iconify[data-icon="mdi:logout"] {
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Иконка входа - синий */
|
||||||
|
header .iconify[data-icon="mdi:login"] {
|
||||||
|
color: #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Иконка регистрации - зеленый */
|
||||||
|
header .iconify[data-icon="mdi:account-plus"] {
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Markdown кнопки - разные цвета */
|
||||||
|
.btnMarkdown .iconify[data-icon="mdi:format-bold"] {
|
||||||
|
color: #424242;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnMarkdown .iconify[data-icon="mdi:format-italic"] {
|
||||||
|
color: #757575;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnMarkdown .iconify[data-icon="mdi:format-header-1"] {
|
||||||
|
color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnMarkdown .iconify[data-icon="mdi:format-list-bulleted"] {
|
||||||
|
color: #388e3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnMarkdown .iconify[data-icon="mdi:format-quote-close"] {
|
||||||
|
color: #f57c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnMarkdown .iconify[data-icon="mdi:code-tags"] {
|
||||||
|
color: #7b1fa2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnMarkdown .iconify[data-icon="mdi:link"] {
|
||||||
|
color: #0288d1;
|
||||||
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@ -28,6 +130,66 @@ header {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Стили для секции поиска в левой панели */
|
||||||
|
.search-section {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-header {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 30px 6px 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 15px;
|
||||||
|
font-size: 12px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #007bff;
|
||||||
|
background-color: white;
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-search-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 6px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #999;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-search-btn:hover {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
.filter-indicator {
|
.filter-indicator {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
@ -341,6 +503,9 @@ textarea:focus {
|
|||||||
|
|
||||||
.avatar-wrapper {
|
.avatar-wrapper {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-preview {
|
.avatar-preview {
|
||||||
@ -601,3 +766,93 @@ textarea:focus {
|
|||||||
.calendar-day.today.has-notes::after {
|
.calendar-day.today.has-notes::after {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Стили для секции тегов */
|
||||||
|
.tags-section {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-header {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background-color: #e7f3ff;
|
||||||
|
color: #007bff;
|
||||||
|
border: 1px solid #007bff;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag:hover {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag.active {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-count {
|
||||||
|
margin-left: 4px;
|
||||||
|
font-size: 9px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стили для тегов в заметках */
|
||||||
|
.textNote .tag-in-note {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 6px;
|
||||||
|
background-color: #e7f3ff;
|
||||||
|
color: #007bff;
|
||||||
|
border: 1px solid #007bff;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textNote .tag-in-note:hover {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 123, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стили для подсветки результатов поиска */
|
||||||
|
.search-highlight {
|
||||||
|
background-color: #fff3cd;
|
||||||
|
padding: 1px 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-highlight.current {
|
||||||
|
background-color: #ffc107;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|||||||
42
server.js
@ -68,6 +68,12 @@ app.use(
|
|||||||
styleSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com"],
|
styleSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com"],
|
||||||
fontSrc: ["'self'", "https://cdnjs.cloudflare.com", "data:"],
|
fontSrc: ["'self'", "https://cdnjs.cloudflare.com", "data:"],
|
||||||
imgSrc: ["'self'", "data:", "blob:"],
|
imgSrc: ["'self'", "data:", "blob:"],
|
||||||
|
connectSrc: [
|
||||||
|
"'self'",
|
||||||
|
"https://api.iconify.design",
|
||||||
|
"https://api.simplesvg.com",
|
||||||
|
"https://api.unisvg.com",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -305,6 +311,42 @@ app.get("/notes", requireAuth, (req, res) => {
|
|||||||
res.sendFile(path.join(__dirname, "public", "notes.html"));
|
res.sendFile(path.join(__dirname, "public", "notes.html"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// API для поиска заметок (должен быть ПЕРЕД /api/notes/:id)
|
||||||
|
app.get("/api/notes/search", requireAuth, (req, res) => {
|
||||||
|
const { q, tag, date } = req.query;
|
||||||
|
|
||||||
|
let sql = "SELECT * FROM notes WHERE user_id = ?";
|
||||||
|
let params = [req.session.userId];
|
||||||
|
|
||||||
|
// Поиск по тексту
|
||||||
|
if (q && q.trim()) {
|
||||||
|
sql += " AND content LIKE ?";
|
||||||
|
params.push(`%${q.trim()}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Поиск по тегу
|
||||||
|
if (tag && tag.trim()) {
|
||||||
|
sql += " AND content LIKE ?";
|
||||||
|
params.push(`%#${tag.trim()}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Поиск по дате
|
||||||
|
if (date && date.trim()) {
|
||||||
|
sql += " AND date = ?";
|
||||||
|
params.push(date.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
sql += " ORDER BY created_at DESC";
|
||||||
|
|
||||||
|
db.all(sql, params, (err, rows) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Ошибка поиска заметок:", err.message);
|
||||||
|
return res.status(500).json({ error: "Ошибка сервера" });
|
||||||
|
}
|
||||||
|
res.json(rows);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// API для получения всех заметок
|
// API для получения всех заметок
|
||||||
app.get("/api/notes", requireAuth, (req, res) => {
|
app.get("/api/notes", requireAuth, (req, res) => {
|
||||||
const sql = "SELECT * FROM notes WHERE user_id = ? ORDER BY created_at ASC";
|
const sql = "SELECT * FROM notes WHERE user_id = ? ORDER BY created_at ASC";
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
self.addEventListener("install", (event) => {
|
|
||||||
console.log("Service Worker установлен.");
|
|
||||||
});
|
|
||||||
|
|
||||||
self.addEventListener("fetch", (event) => {
|
|
||||||
console.log("Обработан запрос:", event.request.url);
|
|
||||||
});
|
|
||||||
69
style.css
@ -8,6 +8,75 @@ body {
|
|||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Стили для Iconify иконок */
|
||||||
|
.iconify {
|
||||||
|
font-size: 16px;
|
||||||
|
vertical-align: middle;
|
||||||
|
display: inline-block;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
iconify-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
vertical-align: middle;
|
||||||
|
display: inline-block;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стили для иконок в заголовках */
|
||||||
|
header .iconify {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стили для иконок в кнопках */
|
||||||
|
.logout-btn .iconify {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Цветные иконки */
|
||||||
|
/* Иконка входа - синий */
|
||||||
|
header .iconify[data-icon="mdi:login"] {
|
||||||
|
color: #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Иконка регистрации - зеленый */
|
||||||
|
header .iconify[data-icon="mdi:account-plus"] {
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Markdown кнопки - разные цвета */
|
||||||
|
.btnMarkdown .iconify[data-icon="mdi:format-bold"] {
|
||||||
|
color: #424242;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnMarkdown .iconify[data-icon="mdi:format-italic"] {
|
||||||
|
color: #757575;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnMarkdown .iconify[data-icon="mdi:format-header-1"] {
|
||||||
|
color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnMarkdown .iconify[data-icon="mdi:format-list-bulleted"] {
|
||||||
|
color: #388e3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnMarkdown .iconify[data-icon="mdi:format-quote-close"] {
|
||||||
|
color: #f57c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnMarkdown .iconify[data-icon="mdi:code-tags"] {
|
||||||
|
color: #7b1fa2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnMarkdown .iconify[data-icon="mdi:link"] {
|
||||||
|
color: #0288d1;
|
||||||
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|||||||
BIN
tags_functionality_screenshot.png
Normal file
|
After Width: | Height: | Size: 103 KiB |