NoteJS/0002-feat.patch
Fovway 07fc786dc1 docs: добавлены файлы для развертывания и документация
- Созданы патч файлы для применения изменений
- Добавлен архив с полными изменениями
- Создан скрипт автоматического применения
- Добавлена документация по развертыванию
- Создан отчет о попытке отправки изменений
2025-10-19 00:42:33 +07:00

1168 lines
42 KiB
Diff
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

From 7376de1a5b448b5a4e33079e7927cb354992ba59 Mon Sep 17 00:00:00 2001
From: Fovway <fovway@gmail.com>
Date: Sun, 19 Oct 2025 00:36:19 +0700
Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?=
=?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D0=BB=D0=B8=D1=87=D0=BD=D1=8B=D0=B9=20?=
=?UTF-8?q?=D0=BA=D0=B0=D0=B1=D0=B8=D0=BD=D0=B5=D1=82=20=D1=81=20=D0=B0?=
=?UTF-8?q?=D0=B2=D0=B0=D1=82=D0=B0=D1=80=D0=BA=D0=B0=D0=BC=D0=B8=20=D0=B8?=
=?UTF-8?q?=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD=D0=B0=20=D0=BD?=
=?UTF-8?q?=D0=B0=D0=B2=D0=B8=D0=B3=D0=B0=D1=86=D0=B8=D1=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Добавлена страница личного кабинета с возможностью загрузки аватарки
- Реализовано управление аватарками: загрузка, удаление, предварительный просмотр
- Исправлено отображение аватарки на странице профиля (центрирование)
- Убрано отображение аватарки со страницы заметок для чистоты интерфейса
- Обновлен .gitignore с исключениями для загруженных файлов и временных файлов
- Обновлен README.md с документацией по новым функциям
- Добавлена валидация загружаемых файлов (тип, размер, формат)
- Улучшена безопасность с изоляцией пользовательских данных
---
.gitignore | 24 ++++-
README.md | 65 ++++++++++++--
index.html | 19 ++--
package-lock.json | 18 ++++
package.json | 1 +
public/app.js | 205 +++++++++++++++++++++++++++++++++++++------
public/index.html | 9 +-
public/notes.html | 84 +++++++++++++-----
public/profile.html | 21 +++--
public/register.html | 9 +-
public/style.css | 178 +++++++++++++++++++++++++++++++++++++
server.js | 42 +++++++++
style.css | 69 +++++++++++++++
13 files changed, 658 insertions(+), 86 deletions(-)
diff --git a/.gitignore b/.gitignore
index e1b049b..a0e03f0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,4 +33,26 @@ Thumbs.db
dist/
build/
-.cursor/
\ No newline at end of file
+# Cursor IDE
+.cursor/
+
+# Загруженные файлы пользователей
+public/uploads/
+
+# Тестовые файлы
+test-icons.html
+
+# Планы и заметки разработчика
+планы.txt
+*.txt
+
+# Скриншоты
+*.png
+*.jpg
+*.jpeg
+*.gif
+*.webp
+
+# Временные файлы
+*.tmp
+*.temp
\ No newline at end of file
diff --git a/README.md b/README.md
index e737285..97c6105 100644
--- a/README.md
+++ b/README.md
@@ -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
+- **Автоматическое изменение размера**: изображения автоматически обрезаются до квадратного формата
+- **Безопасность**: проверка типа файла и размера перед загрузкой
+
## Разработка
Для разработки используйте:
diff --git a/index.html b/index.html
index 0c71d4c..0fbc7f6 100644
--- a/index.html
+++ b/index.html
@@ -5,10 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</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">
@@ -16,31 +13,31 @@
<div class="main">
<div class="markdown-buttons">
<button class="btnMarkdown" id="boldBtn">
- <i class="fas fa-bold"></i>
+ <span class="iconify" data-icon="mdi:format-bold"></span>
<!-- Иконка для жирного текста -->
</button>
<button class="btnMarkdown" id="italicBtn">
- <i class="fas fa-italic"></i>
+ <span class="iconify" data-icon="mdi:format-italic"></span>
<!-- Иконка для курсива -->
</button>
<button class="btnMarkdown" id="headerBtn">
- <i class="fas fa-heading"></i>
+ <span class="iconify" data-icon="mdi:format-header-1"></span>
<!-- Иконка для заголовка -->
</button>
<button class="btnMarkdown" id="listBtn">
- <i class="fas fa-list-ul"></i>
+ <span class="iconify" data-icon="mdi:format-list-bulleted"></span>
<!-- Иконка для списка -->
</button>
<button class="btnMarkdown" id="quoteBtn">
- <i class="fas fa-quote-right"></i>
+ <span class="iconify" data-icon="mdi:format-quote-close"></span>
<!-- Иконка для цитаты -->
</button>
<button class="btnMarkdown" id="codeBtn">
- <i class="fas fa-code"></i>
+ <span class="iconify" data-icon="mdi:code-tags"></span>
<!-- Иконка для кода -->
</button>
<button class="btnMarkdown" id="linkBtn">
- <i class="fas fa-link"></i>
+ <span class="iconify" data-icon="mdi:link"></span>
<!-- Иконка для ссылки -->
</button>
</div>
diff --git a/package-lock.json b/package-lock.json
index 55781e1..bbf1df8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index f8ea894..97a92c7 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/public/app.js b/public/app.js
index 2cd5180..3c37637 100644
--- a/public/app.js
+++ b/public/app.js
@@ -16,6 +16,8 @@ const linkBtn = document.getElementById("linkBtn");
let allNotes = [];
let selectedDateFilter = null;
let selectedTagFilter = null;
+let searchQuery = "";
+let searchResults = [];
// Функция для получения текущей даты и времени
function getFormattedDateTime() {
@@ -56,11 +58,42 @@ function extractTags(content) {
// Функция для преобразования тегов в заметках в кликабельные элементы
function makeTagsClickable(content) {
+ // Сначала находим все теги, которые еще не обернуты в HTML
const tagRegex = /#(\w+)/g;
- return content.replace(
- tagRegex,
- '<span class="tag-in-note" data-tag="$1">#$1</span>'
- );
+ 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;
}
// Функция для получения всех уникальных тегов из заметок
@@ -293,6 +326,55 @@ async function loadNotes() {
}
}
+// Функция для поиска заметок
+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 = "";
@@ -331,8 +413,16 @@ function renderNotes(notes) {
// Итерируемся по заметкам в обычном порядке, чтобы новые были сверху
notesToDisplay.forEach(function (note) {
- // Преобразуем теги в кликабельные элементы перед парсингом markdown
- const contentWithClickableTags = makeTagsClickable(note.content);
+ let contentToProcess = note.content;
+
+ // Сначала подсвечиваем найденный текст в исходном markdown
+ if (searchQuery) {
+ contentToProcess = highlightSearchText(contentToProcess, searchQuery);
+ }
+
+ // Затем преобразуем теги в кликабельные элементы
+ const contentWithClickableTags = makeTagsClickable(contentToProcess);
+
const parsedContent = marked.parse(contentWithClickableTags);
const noteHtml = `
@@ -401,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);
});
@@ -571,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";
@@ -580,16 +670,9 @@ 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) {
@@ -776,6 +859,10 @@ function updateFilterIndicator() {
const filters = [];
+ if (searchQuery) {
+ filters.push(`Поиск: "${searchQuery}"`);
+ }
+
if (selectedDateFilter) {
filters.push(`Дата: ${selectedDateFilter}`);
}
@@ -804,6 +891,21 @@ function updateFilterIndicator() {
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();
@@ -831,4 +933,57 @@ if (nextMonthBtn) {
// Инициализируем календарь при загрузке страницы
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();
+ }
+ });
+}
diff --git a/public/index.html b/public/index.html
index ae39215..bfa9bd7 100644
--- a/public/index.html
+++ b/public/index.html
@@ -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">
diff --git a/public/notes.html b/public/notes.html
index b1a4c8c..f44e007 100644
--- a/public/notes.html
+++ b/public/notes.html
@@ -5,10 +5,7 @@
<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-leftside">
@@ -30,10 +27,36 @@
<div class="calendar-days" id="calendarDays"></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>
+ <span class="tags-title"
+ ><span class="iconify" data-icon="mdi:tag"></span> Теги</span
+ >
</div>
<div class="tags-container" id="tagsContainer">
<!-- Теги будут добавлены динамически -->
@@ -44,7 +67,10 @@
<div class="container">
<header class="notes-header">
<div class="notes-header-left">
- <span>📝 Мои заметки</span>
+ <span
+ ><span class="iconify" data-icon="mdi:note-text"></span> Мои
+ заметки</span
+ >
<div
id="filter-indicator"
class="filter-indicator"
@@ -72,32 +98,34 @@
</div>
<span id="username-display" class="username-clickable"></span>
<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>
<div class="main">
<div class="markdown-buttons">
- <button class="btnMarkdown" id="boldBtn">
- <i class="fas fa-bold"></i>
+ <button class="btnMarkdown" id="boldBtn" title="Жирный текст">
+ <span class="iconify" data-icon="mdi:format-bold"></span>
</button>
- <button class="btnMarkdown" id="italicBtn">
- <i class="fas fa-italic"></i>
+ <button class="btnMarkdown" id="italicBtn" title="Курсив">
+ <span class="iconify" data-icon="mdi:format-italic"></span>
</button>
- <button class="btnMarkdown" id="headerBtn">
- <i class="fas fa-heading"></i>
+ <button class="btnMarkdown" id="headerBtn" title="Заголовок">
+ <span class="iconify" data-icon="mdi:format-header-1"></span>
</button>
- <button class="btnMarkdown" id="listBtn">
- <i class="fas fa-list-ul"></i>
+ <button class="btnMarkdown" id="listBtn" title="Список">
+ <span class="iconify" data-icon="mdi:format-list-bulleted"></span>
</button>
- <button class="btnMarkdown" id="quoteBtn">
- <i class="fas fa-quote-right"></i>
+ <button class="btnMarkdown" id="quoteBtn" title="Цитата">
+ <span class="iconify" data-icon="mdi:format-quote-close"></span>
</button>
- <button class="btnMarkdown" id="codeBtn">
- <i class="fas fa-code"></i>
+ <button class="btnMarkdown" id="codeBtn" title="Код">
+ <span class="iconify" data-icon="mdi:code-tags"></span>
</button>
- <button class="btnMarkdown" id="linkBtn">
- <i class="fas fa-link"></i>
+ <button class="btnMarkdown" id="linkBtn" title="Ссылка">
+ <span class="iconify" data-icon="mdi:link"></span>
</button>
</div>
@@ -120,5 +148,19 @@
</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>
diff --git a/public/profile.html b/public/profile.html
index d7ddb49..6b92616 100644
--- a/public/profile.html
+++ b/public/profile.html
@@ -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">
diff --git a/public/register.html b/public/register.html
index c0fb230..f3ebb8c 100644
--- a/public/register.html
+++ b/public/register.html
@@ -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">
diff --git a/public/style.css b/public/style.css
index d674180..a61ce1f 100644
--- a/public/style.css
+++ b/public/style.css
@@ -10,6 +10,108 @@ body {
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 {
font-size: 20px;
font-weight: bold;
@@ -28,6 +130,66 @@ header {
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;
@@ -341,6 +503,9 @@ textarea:focus {
.avatar-wrapper {
margin-bottom: 15px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
}
.avatar-preview {
@@ -678,3 +843,16 @@ textarea:focus {
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;
+}
diff --git a/server.js b/server.js
index 550897d..71f62fd 100644
--- a/server.js
+++ b/server.js
@@ -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";
diff --git a/style.css b/style.css
index 9da575e..d2dd926 100644
--- a/style.css
+++ b/style.css
@@ -8,6 +8,75 @@ body {
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;
--
2.51.0