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 @@ Document - +
@@ -16,31 +13,31 @@
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, - '#$1' - ); + 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 = `${match.fullMatch}`; + 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, '$1'); +} + // Функция для отображения заметок 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 = ``; + btn.innerHTML = ``; markdownButtonsContainer.appendChild(btn); }); @@ -571,7 +661,7 @@ async function loadUserInfo() { ); if (usernameDisplay) { - usernameDisplay.textContent = `👤 ${user.username}`; + usernameDisplay.innerHTML = ` ${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 @@ Вход в систему заметок - +
-
🔐 Вход в систему
+
+ Вход в систему +