merge: объединение ветки feature/avatar-profile с main

- Разрешены конфликты в .gitignore, public/style.css
- Добавлены файлы index.html и style.css
- Объединены все изменения из feature ветки
- Включены все новые функции: личный кабинет, аватарки, теги
- Добавлена документация и файлы для развертывания
This commit is contained in:
Fovway 2025-10-19 00:43:26 +07:00
commit 37b6b7f3a1
21 changed files with 7544 additions and 230 deletions

24
.gitignore vendored
View File

@ -33,12 +33,28 @@ Thumbs.db
dist/
build/
# Загруженные аватарки пользователей
public/uploads/*
# Cursor IDE
.cursor/
# Загруженные файлы пользователей
public/uploads/
!public/uploads/.gitignore
# Тестовые файлы
test-icons.html
# Планы и заметки разработчика
планы.txt
*.txt
# Скриншоты
*.png
*.jpg
*.jpeg
*.gif
*.webp
# Временные файлы
/tmp/
*.tmp
.cursor/
*.temp

4579
0001-feat.patch Normal file

File diff suppressed because it is too large Load Diff

1167
0002-feat.patch Normal file

File diff suppressed because it is too large Load Diff

115
DEPLOYMENT.md Normal file
View 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. Убедитесь, что база данных обновлена корректно

BIN
NoteJS-changes.tar.gz Normal file

Binary file not shown.

110
PUSH_REPORT.md Normal file
View 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)_
_Статус: Готово к ручному применению_

View File

@ -5,11 +5,16 @@
## Особенности
- 🚀 Создано на Node.js + Express
- 🔐 **Система регистрации и авторизации по логину и паролю** (NEW!)
- 🔐 **Система регистрации и авторизации по логину и паролю**
- 🔒 Безопасное хранение паролей с bcrypt хешированием
- 💾 Хранение данных в SQLite базе данных
- 👥 **Изолированные заметки - каждый пользователь видит только свои заметки** (NEW!)
- 👥 **Изолированные заметки - каждый пользователь видит только свои заметки**
- 👤 **Личный кабинет с возможностью загрузки аватарки** (NEW!)
- 🖼️ **Управление аватаркой: загрузка, удаление, предварительный просмотр** (NEW!)
- 📝 Поддержка Markdown форматирования
- 🏷️ **Система тегов с автоматическим извлечением из заметок** (NEW!)
- 🔍 **Поиск по заметкам с подсветкой результатов** (NEW!)
- 📅 **Мини-календарь для навигации по датам заметок** (NEW!)
- 🎨 Простой и интуитивный интерфейс
- 📱 Адаптивный дизайн
@ -98,6 +103,27 @@ npm start
1. Нажмите кнопку "Удалить" рядом с заметкой
2. Подтвердите удаление в появившемся диалоговом окне
### Личный кабинет
1. Нажмите на ваше имя пользователя в верхней части страницы заметок
2. В личном кабинете вы можете:
- **Загрузить аватарку**: нажмите "Загрузить аватар" и выберите изображение (JPG, PNG, GIF до 5 МБ)
- **Удалить аватарку**: нажмите кнопку "Удалить" рядом с аватаркой
- **Изменить данные профиля**: отредактируйте логин и email
- **Изменить пароль**: введите текущий пароль и новый пароль
### Поиск и фильтрация
1. **Поиск по заметкам**: используйте поле поиска в левой панели для поиска по содержимому заметок
2. **Фильтрация по тегам**: кликайте на теги в левой панели для фильтрации заметок
3. **Навигация по календарю**: кликайте на даты в мини-календаре для просмотра заметок за определенный день
### Теги
- Теги автоматически извлекаются из заметок при использовании символа `#` (например: `#важное`)
- Теги отображаются в левой панели с количеством заметок
- Кликабельные теги в заметках позволяют быстро фильтровать контент
### Выход из системы
Нажмите кнопку "🚪 Выйти" в верхней части страницы заметок
@ -108,14 +134,18 @@ npm start
NoteJS/
├── public/ # Статические файлы (клиентская часть)
│ ├── index.html # Страница входа
│ ├── register.html # Страница регистрации (NEW!)
│ ├── register.html # Страница регистрации
│ ├── notes.html # Страница заметок
│ ├── login.js # Логика входа (обновлена)
│ ├── register.js # Логика регистрации (NEW!)
│ ├── app.js # Клиентский JavaScript
│ └── style.css # Стили
├── server.js # Express сервер
│ ├── profile.html # Страница личного кабинета (NEW!)
│ ├── login.js # Логика входа
│ ├── register.js # Логика регистрации
│ ├── profile.js # Логика личного кабинета (NEW!)
│ ├── app.js # Клиентский JavaScript (обновлен)
│ ├── style.css # Стили (обновлены)
│ └── uploads/ # Загруженные аватарки пользователей (NEW!)
├── server.js # Express сервер (обновлен)
├── .env # Конфигурация (не включать в git!)
├── .gitignore # Исключения для git (обновлен)
├── package.json # Зависимости проекта
├── notes.db # SQLite база данных (создается автоматически)
└── README.md # Документация
@ -132,10 +162,17 @@ NoteJS/
- `POST /logout` - выход из системы
- `GET /api/user` - получить информацию о текущем пользователе (требует аутентификации)
### Профиль пользователя (требует аутентификации)
- `GET /profile` - страница личного кабинета
- `PUT /api/user/profile` - обновить данные профиля или пароль
- `POST /api/user/avatar` - загрузить аватарку
- `DELETE /api/user/avatar` - удалить аватарку
### Заметки (требуют аутентификации)
- `GET /notes` - страница заметок
- `GET /api/notes` - получить все заметки
- `GET /api/notes` - получить все заметки пользователя
- `POST /api/notes` - создать новую заметку
- `PUT /api/notes/:id` - обновить заметку
- `DELETE /api/notes/:id` - удалить заметку
@ -147,7 +184,10 @@ NoteJS/
- **Helmet** для защиты от распространенных уязвимостей
- **CORS** конфигурация
- **Body Parser** для безопасной обработки запросов
- **Multer** для безопасной загрузки файлов с валидацией
- Защищенные маршруты с проверкой аутентификации
- **Валидация загружаемых файлов**: проверка типа, размера и формата
- **Изоляция данных**: каждый пользователь видит только свои заметки и файлы
## Требования к паролям
@ -160,6 +200,13 @@ NoteJS/
- Минимум 3 символа
- Должен быть уникальным (нельзя создать два аккаунта с одинаковым логином)
## Требования к аватаркам
- **Максимальный размер**: 5 МБ
- **Поддерживаемые форматы**: JPG, PNG, GIF
- **Автоматическое изменение размера**: изображения автоматически обрезаются до квадратного формата
- **Безопасность**: проверка типа файла и размера перед загрузкой
## Разработка
Для разработки используйте:

32
apply-changes.sh Executable file
View 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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

69
index.html Normal file
View File

@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="style.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/iconify/2.0.0/iconify.min.js"></script>
</head>
<body>
<div class="container">
<header>Заметки</header>
<div class="main">
<div class="markdown-buttons">
<button class="btnMarkdown" id="boldBtn">
<span class="iconify" data-icon="mdi:format-bold"></span>
<!-- Иконка для жирного текста -->
</button>
<button class="btnMarkdown" id="italicBtn">
<span class="iconify" data-icon="mdi:format-italic"></span>
<!-- Иконка для курсива -->
</button>
<button class="btnMarkdown" id="headerBtn">
<span class="iconify" data-icon="mdi:format-header-1"></span>
<!-- Иконка для заголовка -->
</button>
<button class="btnMarkdown" id="listBtn">
<span class="iconify" data-icon="mdi:format-list-bulleted"></span>
<!-- Иконка для списка -->
</button>
<button class="btnMarkdown" id="quoteBtn">
<span class="iconify" data-icon="mdi:format-quote-close"></span>
<!-- Иконка для цитаты -->
</button>
<button class="btnMarkdown" id="codeBtn">
<span class="iconify" data-icon="mdi:code-tags"></span>
<!-- Иконка для кода -->
</button>
<button class="btnMarkdown" id="linkBtn">
<span class="iconify" data-icon="mdi:link"></span>
<!-- Иконка для ссылки -->
</button>
</div>
<textarea
class="textInput"
name=""
id=""
placeholder="Ваша заметка..."
></textarea>
<button class="btnSave">Сохранить</button>
</div>
</div>
<div class="notes-container">
<div id="note" class="container">
<div class="date">
<div class="notesHeaderBtn"></div>
<div class="notesHeaderBtn"></div>
</div>
<div class="textNote"></div>
</div>
</div>
<div class="footer">
<p>Создатель: <span>Fovway</span></p>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/11.1.0/marked.min.js"></script>
<script src="app.js"></script>
</body>
</html>

18
package-lock.json generated
View File

@ -14,6 +14,7 @@
"@codemirror/state": "^6.5.2",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.38.6",
"@iconify/iconify": "^3.1.1",
"bcryptjs": "^3.0.2",
"body-parser": "^2.2.0",
"codemirror": "^6.0.2",
@ -507,6 +508,23 @@
"integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==",
"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": {
"version": "0.16.1",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-0.16.1.tgz",

View File

@ -20,6 +20,7 @@
"@codemirror/state": "^6.5.2",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.38.6",
"@iconify/iconify": "^3.1.1",
"bcryptjs": "^3.0.2",
"body-parser": "^2.2.0",
"codemirror": "^6.0.2",

View File

@ -1,7 +1,7 @@
// DOM элементы
const noteInput = document.getElementById("noteInput");
const saveBtn = document.getElementById("saveBtn");
const notesList = document.getElementById("notesList");
const notesList = document.getElementById("notes-container");
// Получаем кнопки markdown
const boldBtn = document.getElementById("boldBtn");
@ -12,6 +12,13 @@ const quoteBtn = document.getElementById("quoteBtn");
const codeBtn = document.getElementById("codeBtn");
const linkBtn = document.getElementById("linkBtn");
// Глобальные переменные для заметок и фильтрации
let allNotes = [];
let selectedDateFilter = null;
let selectedTagFilter = null;
let searchQuery = "";
let searchResults = [];
// Функция для получения текущей даты и времени
function getFormattedDateTime() {
let now = new Date();
@ -33,6 +40,121 @@ function autoExpandTextarea(textarea) {
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 () {
autoExpandTextarea(noteInput);
@ -194,19 +316,115 @@ async function loadNotes() {
throw new Error("Ошибка загрузки заметок");
}
const notes = await response.json();
allNotes = notes; // Сохраняем все заметки в глобальную переменную
renderNotes(notes);
renderCalendar(); // Обновляем календарь после загрузки заметок
renderTags(); // Обновляем теги после загрузки заметок
} catch (error) {
console.error("Ошибка:", error);
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) {
notesList.innerHTML = "";
// Фильтруем заметки по дате и тегам
let notesToDisplay = notes;
if (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) {
let message = "Заметок пока нет";
if (selectedDateFilter && selectedTagFilter) {
message = `Нет заметок за ${selectedDateFilter} с тегом #${selectedTagFilter}`;
} 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;
}
// Итерируемся по заметкам в обычном порядке, чтобы новые были сверху
notes.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 = `
<div id="note" class="container">
<div class="date">
@ -221,7 +439,7 @@ function renderNotes(notes) {
<div class="textNote" data-original-content="${note.content.replace(
/"/g,
"&quot;"
)}">${marked.parse(note.content)}</div>
)}">${parsedContent}</div>
</div>
`;
notesList.insertAdjacentHTML("afterbegin", noteHtml);
@ -229,6 +447,9 @@ function renderNotes(notes) {
// Добавляем обработчики событий для кнопок редактирования и удаления
addNoteEventListeners();
// Добавляем обработчики кликов для тегов в заметках
addTagClickListeners();
}
// Функция для добавления обработчиков событий к заметкам
@ -270,20 +491,20 @@ function addNoteEventListeners() {
// Создаем markdown кнопки
const markdownButtons = [
{ id: "editBoldBtn", icon: "fas fa-bold", tag: "**" },
{ id: "editItalicBtn", icon: "fas fa-italic", tag: "*" },
{ id: "editHeaderBtn", icon: "fas fa-heading", tag: "# " },
{ id: "editListBtn", icon: "fas fa-list-ul", tag: "- " },
{ id: "editQuoteBtn", icon: "fas fa-quote-right", tag: "> " },
{ id: "editCodeBtn", icon: "fas fa-code", tag: "`" },
{ id: "editLinkBtn", icon: "fas fa-link", tag: "[Текст ссылки](URL)" },
{ id: "editBoldBtn", icon: "mdi:format-bold", tag: "**" },
{ id: "editItalicBtn", icon: "mdi:format-italic", tag: "*" },
{ id: "editHeaderBtn", icon: "mdi:format-header-1", tag: "# " },
{ id: "editListBtn", icon: "mdi:format-list-bulleted", tag: "- " },
{ id: "editQuoteBtn", icon: "mdi:format-quote-close", tag: "> " },
{ id: "editCodeBtn", icon: "mdi:code-tags", tag: "`" },
{ id: "editLinkBtn", icon: "mdi:link", tag: "[Текст ссылки](URL)" },
];
markdownButtons.forEach((button) => {
const btn = document.createElement("button");
btn.classList.add("btnMarkdown");
btn.id = button.id;
btn.innerHTML = `<i class="${button.icon}"></i>`;
btn.innerHTML = `<span class="iconify" data-icon="${button.icon}"></span>`;
markdownButtonsContainer.appendChild(btn);
});
@ -352,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() {
if (noteInput.value.trim() !== "") {
@ -400,6 +645,7 @@ noteInput.addEventListener("keydown", function (event) {
document.addEventListener("DOMContentLoaded", function () {
loadUserInfo();
loadNotes();
updateFilterIndicator();
});
// Функция для загрузки информации о пользователе
@ -415,7 +661,7 @@ async function loadUserInfo() {
);
if (usernameDisplay) {
usernameDisplay.textContent = `👤 ${user.username}`;
usernameDisplay.innerHTML = `<span class="iconify" data-icon="mdi:account"></span> ${user.username}`;
// Делаем ник кликабельным для перехода в личный кабинет
usernameDisplay.style.cursor = "pointer";
@ -424,19 +670,320 @@ async function loadUserInfo() {
});
}
// Отображаем аватарку, если она есть
if (user.avatar && userAvatar && userAvatarContainer) {
userAvatar.src = user.avatar;
userAvatarContainer.style.display = "inline-block";
// Аватарка также кликабельна
userAvatarContainer.style.cursor = "pointer";
userAvatarContainer.addEventListener("click", function () {
window.location.href = "/profile";
});
// Аватарка скрыта на странице заметок
if (userAvatarContainer) {
userAvatarContainer.style.display = "none";
}
}
} catch (error) {
console.error("Ошибка загрузки информации о пользователе:", error);
}
}
// Календарь
let currentDate = new Date();
// Функция для отображения календаря
function renderCalendar() {
const calendarDays = document.getElementById("calendarDays");
const monthYear = document.getElementById("monthYear");
// Проверяем, существуют ли элементы календаря
if (!calendarDays || !monthYear) return;
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
// Массив названий месяцев
const monthNames = [
"Январь",
"Февраль",
"Март",
"Апрель",
"Май",
"Июнь",
"Июль",
"Август",
"Сентябрь",
"Октябрь",
"Ноябрь",
"Декабрь",
];
// Устанавливаем заголовок месяца и года
monthYear.textContent = `${monthNames[month]} ${year}`;
// Получаем первый день месяца
const firstDay = new Date(year, month, 1);
// Получаем последний день месяца
const lastDay = new Date(year, month + 1, 0);
// Получаем день недели первого дня (0 - воскресенье, 1 - понедельник и т.д.)
let firstDayOfWeek = firstDay.getDay();
// Преобразуем так, чтобы понедельник был первым днем (0)
firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1;
// Очищаем календарь
calendarDays.innerHTML = "";
// Создаём Set дат, когда были созданы заметки
const noteDates = new Set();
allNotes.forEach((note) => {
noteDates.add(note.date);
});
// Получаем последний день предыдущего месяца
const prevMonthLastDay = new Date(year, month, 0).getDate();
const prevMonth = month === 0 ? 11 : month - 1;
const prevYear = month === 0 ? year - 1 : year;
// Добавляем дни предыдущего месяца
for (let i = firstDayOfWeek - 1; i >= 0; i--) {
const day = prevMonthLastDay - i;
const dateStr = `${String(day).padStart(2, "0")}.${String(
prevMonth + 1
).padStart(2, "0")}.${prevYear}`;
const dayDiv = document.createElement("div");
dayDiv.classList.add("calendar-day", "other-month");
dayDiv.textContent = day;
dayDiv.dataset.date = dateStr;
// Проверяем, есть ли заметки на этот день
if (noteDates.has(dateStr)) {
dayDiv.classList.add("has-notes");
}
// Проверяем, выбран ли этот день
if (selectedDateFilter === dateStr) {
dayDiv.classList.add("selected");
}
// Добавляем обработчик клика
dayDiv.addEventListener("click", handleDayClick);
calendarDays.appendChild(dayDiv);
}
// Добавляем дни текущего месяца
const today = new Date();
for (let day = 1; day <= lastDay.getDate(); day++) {
const dateStr = `${String(day).padStart(2, "0")}.${String(
month + 1
).padStart(2, "0")}.${year}`;
const dayDiv = document.createElement("div");
dayDiv.classList.add("calendar-day");
dayDiv.textContent = day;
dayDiv.dataset.date = dateStr;
// Проверяем, является ли день сегодняшним
if (
day === today.getDate() &&
month === today.getMonth() &&
year === today.getFullYear()
) {
dayDiv.classList.add("today");
}
// Проверяем, есть ли заметки на этот день
if (noteDates.has(dateStr)) {
dayDiv.classList.add("has-notes");
}
// Проверяем, выбран ли этот день
if (selectedDateFilter === dateStr) {
dayDiv.classList.add("selected");
}
// Добавляем обработчик клика
dayDiv.addEventListener("click", handleDayClick);
calendarDays.appendChild(dayDiv);
}
// Добавляем дни следующего месяца
const totalCells = calendarDays.children.length;
const remainingCells = 42 - totalCells; // 6 недель по 7 дней
const nextMonth = month === 11 ? 0 : month + 1;
const nextYear = month === 11 ? year + 1 : year;
for (let day = 1; day <= remainingCells; day++) {
const dateStr = `${String(day).padStart(2, "0")}.${String(
nextMonth + 1
).padStart(2, "0")}.${nextYear}`;
const dayDiv = document.createElement("div");
dayDiv.classList.add("calendar-day", "other-month");
dayDiv.textContent = day;
dayDiv.dataset.date = dateStr;
// Проверяем, есть ли заметки на этот день
if (noteDates.has(dateStr)) {
dayDiv.classList.add("has-notes");
}
// Проверяем, выбран ли этот день
if (selectedDateFilter === dateStr) {
dayDiv.classList.add("selected");
}
// Добавляем обработчик клика
dayDiv.addEventListener("click", handleDayClick);
calendarDays.appendChild(dayDiv);
}
}
// Обработчик клика на день в календаре
function handleDayClick(event) {
const clickedDate = event.target.dataset.date;
// Если кликнули на тот же день, снимаем фильтр
if (selectedDateFilter === clickedDate) {
selectedDateFilter = null;
} else {
selectedDateFilter = clickedDate;
}
// Перерисовываем заметки, календарь и теги
renderNotes(allNotes);
renderCalendar();
renderTags();
updateFilterIndicator();
}
// Функция для обновления индикатора фильтра
function updateFilterIndicator() {
const filterIndicator = document.getElementById("filter-indicator");
if (!filterIndicator) return;
const filters = [];
if (searchQuery) {
filters.push(`Поиск: "${searchQuery}"`);
}
if (selectedDateFilter) {
filters.push(`Дата: ${selectedDateFilter}`);
}
if (selectedTagFilter) {
filters.push(`Тег: #${selectedTagFilter}`);
}
if (filters.length > 0) {
filterIndicator.style.display = "inline-block";
filterIndicator.innerHTML = `Фильтр: ${filters.join(
", "
)} <button id="clear-filter-btn"></button>`;
// Добавляем обработчик клика для кнопки сброса
const clearBtn = document.getElementById("clear-filter-btn");
if (clearBtn) {
clearBtn.addEventListener("click", clearFilter);
}
} else {
filterIndicator.style.display = "none";
}
}
// Функция для сброса фильтра (глобальная)
window.clearFilter = function () {
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);
renderCalendar();
renderTags();
updateFilterIndicator();
};
// Обработчики для кнопок навигации календаря
const prevMonthBtn = document.getElementById("prevMonth");
const nextMonthBtn = document.getElementById("nextMonth");
if (prevMonthBtn) {
prevMonthBtn.addEventListener("click", function () {
currentDate.setMonth(currentDate.getMonth() - 1);
renderCalendar();
});
}
if (nextMonthBtn) {
nextMonthBtn.addEventListener("click", function () {
currentDate.setMonth(currentDate.getMonth() + 1);
renderCalendar();
});
}
// Инициализируем календарь при загрузке страницы
document.addEventListener("DOMContentLoaded", function () {
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();
}
});
}

View File

@ -5,14 +5,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Вход в систему заметок</title>
<link rel="stylesheet" href="/style.css" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"
/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/iconify/2.0.0/iconify.min.js"></script>
</head>
<body>
<div class="container">
<header>🔐 Вход в систему</header>
<header>
<span class="iconify" data-icon="mdi:login"></span> Вход в систему
</header>
<div class="login-form">
<form id="loginForm">
<div class="form-group">

View File

@ -5,85 +5,162 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Заметки</title>
<link rel="stylesheet" href="/style.css?v=2" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"
/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/iconify/2.0.0/iconify.min.js"></script>
</head>
<body>
<div class="container">
<header class="notes-header">
<span>📝 Мои заметки</span>
<div class="user-info">
<div
id="user-avatar-container"
style="display: none; margin-right: 10px"
>
<img
id="user-avatar"
src=""
alt="Аватар"
style="
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
vertical-align: middle;
border: 2px solid #007bff;
"
/>
</div>
<span id="username-display" class="username-clickable"></span>
<form action="/logout" method="POST" style="display: inline">
<button type="submit" class="logout-btn">🚪 Выйти</button>
</form>
<div class="container-leftside">
<div class="mini-calendar">
<div class="calendar-header">
<button class="calendar-nav" id="prevMonth"></button>
<span class="calendar-month-year" id="monthYear"></span>
<button class="calendar-nav" id="nextMonth"></button>
</div>
</header>
<div class="main">
<div class="markdown-buttons">
<button class="btnMarkdown" id="boldBtn">
<i class="fas fa-bold"></i>
</button>
<button class="btnMarkdown" id="italicBtn">
<i class="fas fa-italic"></i>
</button>
<button class="btnMarkdown" id="headerBtn">
<i class="fas fa-heading"></i>
</button>
<button class="btnMarkdown" id="listBtn">
<i class="fas fa-list-ul"></i>
</button>
<button class="btnMarkdown" id="quoteBtn">
<i class="fas fa-quote-right"></i>
</button>
<button class="btnMarkdown" id="codeBtn">
<i class="fas fa-code"></i>
</button>
<button class="btnMarkdown" id="linkBtn">
<i class="fas fa-link"></i>
</button>
<div class="calendar-weekdays">
<div class="calendar-weekday">Пн</div>
<div class="calendar-weekday">Вт</div>
<div class="calendar-weekday">Ср</div>
<div class="calendar-weekday">Чт</div>
<div class="calendar-weekday">Пт</div>
<div class="calendar-weekday">Сб</div>
<div class="calendar-weekday">Вс</div>
</div>
<div class="calendar-days" id="calendarDays"></div>
</div>
<textarea
class="textInput"
id="noteInput"
placeholder="Ваша заметка..."
></textarea>
<div class="save-button-container">
<button class="btnSave" id="saveBtn">Сохранить</button>
<span class="save-hint">или нажмите Alt + Enter</span>
<!-- Секция поиска -->
<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 class="notes-container">
<div id="notesList">
<!-- Заметки будут загружаться здесь -->
<div class="center">
<div class="container">
<header class="notes-header">
<div class="notes-header-left">
<span
><span class="iconify" data-icon="mdi:note-text"></span> Мои
заметки</span
>
<div
id="filter-indicator"
class="filter-indicator"
style="display: none"
></div>
</div>
<div class="user-info">
<div
id="user-avatar-container"
style="display: none; margin-right: 10px"
>
<img
id="user-avatar"
src=""
alt="Аватар"
style="
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
vertical-align: middle;
border: 2px solid #007bff;
"
/>
</div>
<span id="username-display" class="username-clickable"></span>
<form action="/logout" method="POST" style="display: inline">
<button type="submit" class="logout-btn">
<span class="iconify" data-icon="mdi:logout"></span> Выйти
</button>
</form>
</div>
</header>
<div class="main">
<div class="markdown-buttons">
<button class="btnMarkdown" id="boldBtn" title="Жирный текст">
<span class="iconify" data-icon="mdi:format-bold"></span>
</button>
<button class="btnMarkdown" id="italicBtn" title="Курсив">
<span class="iconify" data-icon="mdi:format-italic"></span>
</button>
<button class="btnMarkdown" id="headerBtn" title="Заголовок">
<span class="iconify" data-icon="mdi:format-header-1"></span>
</button>
<button class="btnMarkdown" id="listBtn" title="Список">
<span class="iconify" data-icon="mdi:format-list-bulleted"></span>
</button>
<button class="btnMarkdown" id="quoteBtn" title="Цитата">
<span class="iconify" data-icon="mdi:format-quote-close"></span>
</button>
<button class="btnMarkdown" id="codeBtn" title="Код">
<span class="iconify" data-icon="mdi:code-tags"></span>
</button>
<button class="btnMarkdown" id="linkBtn" title="Ссылка">
<span class="iconify" data-icon="mdi:link"></span>
</button>
</div>
<textarea
class="textInput"
id="noteInput"
placeholder="Ваша заметка..."
></textarea>
<div class="save-button-container">
<button class="btnSave" id="saveBtn">Сохранить</button>
<span class="save-hint">или нажмите Alt + Enter</span>
</div>
</div>
</div>
<div id="notes-container" class="notes-container"></div>
</div>
<div class="footer">
<p>Создатель: <span>Fovway</span></p>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/11.1.0/marked.min.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>
</html>

View File

@ -5,19 +5,21 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Личный кабинет</title>
<link rel="stylesheet" href="/style.css?v=3" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"
/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/iconify/2.0.0/iconify.min.js"></script>
</head>
<body>
<div class="container">
<header class="notes-header">
<span>👤 Личный кабинет</span>
<span
><span class="iconify" data-icon="mdi:account"></span> Личный
кабинет</span
>
<div class="user-info">
<a href="/notes" class="back-btn">← Назад к заметкам</a>
<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>
</div>
</header>
@ -34,12 +36,13 @@
style="display: none"
/>
<div id="avatarPlaceholder" class="avatar-placeholder">
<i class="fas fa-user"></i>
<span class="iconify" data-icon="mdi:account"></span>
</div>
</div>
<div class="avatar-buttons">
<label for="avatarInput" class="btn-upload">
<i class="fas fa-upload"></i> Загрузить аватар
<span class="iconify" data-icon="mdi:upload"></span> Загрузить
аватар
</label>
<input
type="file"
@ -52,7 +55,7 @@
class="btn-delete"
style="display: none"
>
<i class="fas fa-trash"></i> Удалить
<span class="iconify" data-icon="mdi:delete"></span> Удалить
</button>
</div>
<p class="avatar-hint">

View File

@ -5,14 +5,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Регистрация</title>
<link rel="stylesheet" href="/style.css" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"
/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/iconify/2.0.0/iconify.min.js"></script>
</head>
<body>
<div class="container">
<header>📝 Регистрация</header>
<header>
<span class="iconify" data-icon="mdi:account-plus"></span> Регистрация
</header>
<div class="login-form">
<form id="registerForm">
<div class="form-group">

View File

@ -2,10 +2,114 @@ body {
font-family: "Open Sans", sans-serif;
padding: 0;
margin: 0;
display: flex;
flex-wrap: wrap;
justify-content: center;
background: #f5f5f5;
display: flex;
justify-content: center;
align-items: flex-start;
gap: 30px;
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 {
@ -17,7 +121,100 @@ header {
.notes-header {
display: flex;
justify-content: space-between;
align-items: center;
align-items: flex-start;
}
.notes-header-left {
display: flex;
flex-direction: column;
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 {
display: inline-block;
margin-top: 5px;
padding: 4px 10px;
background-color: #e7f3ff;
border: 1px solid #007bff;
border-radius: 15px;
font-size: 12px;
color: #007bff;
font-weight: 500;
}
.filter-indicator button {
background: none;
border: none;
color: #dc3545;
cursor: pointer;
margin-left: 8px;
font-weight: bold;
padding: 0;
font-size: 14px;
}
.filter-indicator button:hover {
color: #a71d2a;
}
.user-info {
@ -295,7 +492,6 @@ textarea:focus {
/* Стили для личного кабинета */
.profile-container {
margin-top: 20px;
padding-bottom: 80px; /* Отступ снизу, чтобы контент не обрезался футером */
}
.avatar-section {
@ -325,7 +521,7 @@ textarea:focus {
height: 150px;
border-radius: 50%;
background-color: #e0e0e0;
display: flex;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 64px;
@ -440,82 +636,77 @@ textarea:focus {
text-decoration: underline;
}
/* Стили для календаря */
.main-wrapper {
.center {
display: flex;
gap: 20px;
align-items: flex-start;
width: 100%;
justify-content: center;
align-items: center;
flex-direction: column;
width: 600px;
height: 100%;
}
.calendar-sidebar {
flex-shrink: 0;
width: 240px;
}
.calendar-container {
background: white;
border: 1px solid #e0e0e0;
.container-leftside {
width: 200px;
height: auto;
max-width: 200px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
padding: 12px;
margin-top: 20px;
background: white;
}
/* Мини-календарь */
.mini-calendar {
width: 100%;
}
.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
gap: 10px;
margin-bottom: 10px;
}
.calendar-header h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
.calendar-month-year {
font-size: 13px;
font-weight: bold;
color: #333;
min-width: 120px;
text-align: center;
flex: 1;
}
.calendar-nav-btn {
background: #f0f0f0;
border: 1px solid #ddd;
border-radius: 4px;
padding: 4px 8px;
.calendar-nav {
background: none;
border: none;
font-size: 18px;
cursor: pointer;
font-size: 14px;
color: #333;
transition: all 0.2s ease;
color: #007bff;
padding: 0 3px;
transition: color 0.3s ease;
}
.calendar-nav-btn:hover {
background: #007bff;
color: white;
border-color: #007bff;
.calendar-nav:hover {
color: #0056b3;
}
.calendar-weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 2px;
margin-bottom: 10px;
gap: 1px;
margin-bottom: 3px;
}
.weekday {
.calendar-weekday {
text-align: center;
font-size: 11px;
font-weight: 600;
font-size: 9px;
font-weight: bold;
color: #666;
padding: 5px 0;
text-transform: uppercase;
padding: 3px 0;
}
.calendar-days {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 2px;
gap: 1px;
}
.calendar-day {
@ -523,90 +714,145 @@ textarea:focus {
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
border-radius: 4px;
font-size: 11px;
cursor: pointer;
background: #f8f9fa;
color: #666;
border: 1px solid transparent;
border-radius: 3px;
transition: all 0.2s ease;
color: #333;
padding: 1px;
font-weight: 500;
position: relative;
}
.calendar-day:hover {
background: #e8f4f8;
border-color: #007bff;
background-color: #e7f3ff;
}
.calendar-day.today {
background-color: #007bff;
color: white;
font-weight: bold;
}
.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;
background-color: #0056b3;
color: white;
font-weight: 600;
border-color: #1e7e34;
font-weight: bold;
}
.calendar-day.selected:hover {
background: #1e7e34;
}
.calendar-day.has-notes {
font-weight: 600;
color: #007bff;
}
.calendar-day.has-notes::before {
/* Индикатор для дней с заметками */
.calendar-day.has-notes::after {
content: "";
display: block;
width: 4px;
height: 4px;
background: #007bff;
border-radius: 50%;
position: absolute;
bottom: 2px;
left: 50%;
transform: translateX(-50%);
width: 4px;
height: 4px;
background-color: #28a745;
border-radius: 50%;
}
.main {
flex: 1;
min-width: 300px;
/* Индикатор для выбранного дня с заметками */
.calendar-day.selected.has-notes::after {
background-color: #fff;
}
/* Адаптация для мобильных устройств */
@media (max-width: 768px) {
.main-wrapper {
flex-direction: column;
}
.calendar-sidebar {
width: 100%;
}
.calendar-container {
max-width: 100%;
}
.main {
min-width: auto;
}
.container {
max-width: 900px;
}
/* Индикатор для сегодняшнего дня с заметками */
.calendar-day.today.has-notes::after {
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;
}

View File

@ -68,6 +68,12 @@ app.use(
styleSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com"],
fontSrc: ["'self'", "https://cdnjs.cloudflare.com", "data:"],
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"));
});
// 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 для получения всех заметок
app.get("/api/notes", requireAuth, (req, res) => {
const sql = "SELECT * FROM notes WHERE user_id = ? ORDER BY created_at ASC";

247
style.css Normal file
View File

@ -0,0 +1,247 @@
body {
font-family: "Open Sans", sans-serif;
padding: 0;
margin: 0;
display: flex;
flex-wrap: wrap;
justify-content: center;
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 {
font-size: 20px;
font-weight: bold;
margin-bottom: 20px;
}
.container {
width: 90%;
max-width: 600px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
border-radius: 8px;
padding: 15px;
margin-top: 20px;
background: white;
}
textarea {
width: 100%;
min-height: 50px; /* Минимальная высота */
resize: none; /* Отключаем возможность ручного изменения размера */
border: none;
background: white;
margin-bottom: 5px;
overflow-y: hidden; /* Отключаем полосу прокрутки по вертикали */
}
textarea:focus {
outline: none; /* Убираем обводку */
}
.btnSave {
padding: 5px;
cursor: pointer;
border-width: 1px;
background: white;
border-radius: 5px;
font-family: "Open Sans", sans-serif;
transition: all 0.3s ease;
}
.date {
font-size: 11px;
color: grey;
}
.notesHeaderBtn {
display: inline-block;
cursor: pointer;
color: black;
font-weight: bold;
}
.textNote {
margin-top: 10px;
white-space: pre-wrap;
}
/* Убираем стандартные отступы для абзацев */
.textNote p {
margin: 0;
padding: 0;
}
/* Убираем маргины у заголовков */
.textNote h1,
.textNote h2,
.textNote h3,
.textNote h4,
.textNote h5,
.textNote h6 {
margin: 0;
padding: 0;
}
/* Убираем отступы у списков */
.textNote ul,
.textNote ol {
margin: 0;
padding-left: 20px;
}
/* Убираем маргины у элементов списка */
.textNote li {
margin: 0;
padding: 0;
}
/* Стили для ссылок */
.textNote a {
color: #007bff;
text-decoration: none;
}
.textNote a:hover {
text-decoration: underline;
}
/* Стили для цитат */
.textNote blockquote {
border-left: 4px solid #007bff;
padding-left: 16px;
margin: 10px 0;
color: #555;
font-style: italic;
background-color: #f8f9fa;
padding: 10px 16px;
border-radius: 0 4px 4px 0;
}
.textNote blockquote p {
margin: 0;
}
/* Стили для кода */
.textNote pre {
background-color: #f5f5f5;
padding: 10px;
border-radius: 5px;
font-size: 14px;
overflow-x: auto;
}
.textNote code {
background-color: #f5f5f5;
padding: 2px 4px;
border-radius: 5px;
font-size: 14px;
}
.notes-container {
width: 100%;
display: flex;
flex-direction: column; /* Располагаем элементы в колонку */
align-items: center; /* Центрируем */
}
.markdown-buttons {
margin-top: 10px;
margin-bottom: 10px;
}
.markdown-buttons .btnMarkdown {
padding: 5px 10px;
margin-right: 5px;
cursor: pointer;
border: 1px solid #ddd;
background-color: #f0f0f0;
border-radius: 5px;
font-size: 14px;
}
.markdown-buttons .btnMarkdown:hover {
background-color: #e0e0e0;
}
.footer {
text-align: center;
font-size: 12px;
color: #999;
position: fixed;
bottom: 0;
width: 100%;
padding: 10px 0;
}
.footer span {
font-weight: bold;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB