// DOM элементы const noteInput = document.getElementById("noteInput"); const saveBtn = document.getElementById("saveBtn"); const notesList = document.getElementById("notes-container"); // Получаем кнопки markdown const boldBtn = document.getElementById("boldBtn"); const italicBtn = document.getElementById("italicBtn"); const headerBtn = document.getElementById("headerBtn"); const listBtn = document.getElementById("listBtn"); const quoteBtn = document.getElementById("quoteBtn"); const codeBtn = document.getElementById("codeBtn"); const linkBtn = document.getElementById("linkBtn"); const imageBtn = document.getElementById("imageBtn"); // Элементы для загрузки изображений const imageInput = document.getElementById("imageInput"); const imagePreviewContainer = document.getElementById("imagePreviewContainer"); const imagePreviewList = document.getElementById("imagePreviewList"); const clearImagesBtn = document.getElementById("clearImagesBtn"); // Модальное окно для просмотра изображений const imageModal = document.getElementById("imageModal"); const modalImage = document.getElementById("modalImage"); const modalClose = document.querySelector(".image-modal-close"); // Массив для хранения выбранных изображений let selectedImages = []; // Глобальные переменные для заметок и фильтрации let allNotes = []; let selectedDateFilter = null; let selectedTagFilter = null; let searchQuery = ""; let searchResults = []; let notesCache = null; // Кэш для заметок let lastLoadTime = 0; // Время последней загрузки // Функция для получения текущей даты и времени function getFormattedDateTime() { let now = new Date(); let day = String(now.getDate()).padStart(2, "0"); let month = String(now.getMonth() + 1).padStart(2, "0"); let year = now.getFullYear(); let hours = String(now.getHours()).padStart(2, "0"); let minutes = String(now.getMinutes()).padStart(2, "0"); return { date: `${day}.${month}.${year}`, time: `${hours}:${minutes}`, }; } // Функция для авторасширения текстового поля function autoExpandTextarea(textarea) { textarea.style.height = "auto"; textarea.style.height = textarea.scrollHeight + "px"; } // Функция для извлечения тегов из текста заметки function extractTags(content) { const tagRegex = /#([а-яё\w]+)/gi; 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]+)/gi; 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; } // Функция для получения всех уникальных тегов из заметок 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 = '
Нет тегов
'; return; } tagsContainer.innerHTML = sortedTags .map((tag) => { const count = tagCounts[tag]; const isActive = selectedTagFilter === tag ? "active" : ""; return `#${tag}${count}`; }) .join(""); // Добавляем обработчики кликов для тегов tagsContainer.querySelectorAll(".tag").forEach((tagElement) => { tagElement.addEventListener("click", async (event) => await handleTagClick(event)); }); } // Обработчик клика на тег async function handleTagClick(event) { const clickedTag = event.target.closest(".tag").dataset.tag; // Если кликнули на тот же тег, снимаем фильтр if (selectedTagFilter === clickedTag) { selectedTagFilter = null; } else { selectedTagFilter = clickedTag; } // Перерисовываем заметки и теги await renderNotes(allNotes); renderTags(); updateFilterIndicator(); } // Привязываем авторасширение к текстовому полю для создания заметки noteInput.addEventListener("input", function () { autoExpandTextarea(noteInput); }); // Изначально запускаем для установки правильной высоты autoExpandTextarea(noteInput); // Функция для вставки markdown function insertMarkdown(tag) { const start = noteInput.selectionStart; const end = noteInput.selectionEnd; const text = noteInput.value; const before = text.substring(0, start); const selected = text.substring(start, end); const after = text.substring(end); if (selected.startsWith(tag) && selected.endsWith(tag)) { // Если теги уже есть, удаляем их noteInput.value = `${before}${selected.slice( tag.length, -tag.length )}${after}`; noteInput.setSelectionRange(start, end - 2 * tag.length); } else if (selected.trim() === "") { // Если текст не выделен if (tag === "[Текст ссылки](URL)") { // Для ссылок создаем шаблон с двумя кавычками noteInput.value = `${before}[Текст ссылки](URL)${after}`; const cursorPosition = start + 1; // Помещаем курсор внутрь текста ссылки noteInput.setSelectionRange(cursorPosition, cursorPosition + 12); } else if (tag === "- " || tag === "> " || tag === "# ") { // Для списка, цитаты и заголовка помещаем курсор после `- `, `> ` или `# ` noteInput.value = `${before}${tag}${after}`; const cursorPosition = start + tag.length; noteInput.setSelectionRange(cursorPosition, cursorPosition); } else { // Для остальных типов создаем два тега noteInput.value = `${before}${tag}${tag}${after}`; const cursorPosition = start + tag.length; noteInput.setSelectionRange(cursorPosition, cursorPosition); } } else { // Если текст выделен if (tag === "[Текст ссылки](URL)") { // Для ссылок используем выделенный текст вместо "Текст ссылки" noteInput.value = `${before}[${selected}](URL)${after}`; const cursorPosition = start + selected.length + 3; // Помещаем курсор в URL noteInput.setSelectionRange(cursorPosition, cursorPosition + 3); } else if (tag === "- " || tag === "> " || tag === "# ") { // Для списка, цитаты и заголовка добавляем `- `, `> ` или `# ` перед выделенным текстом noteInput.value = `${before}${tag}${selected}${after}`; const cursorPosition = start + tag.length + selected.length; noteInput.setSelectionRange(cursorPosition, cursorPosition); } else { // Для остальных типов оборачиваем выделенный текст noteInput.value = `${before}${tag}${selected}${tag}${after}`; const cursorPosition = start + tag.length + selected.length + tag.length; noteInput.setSelectionRange(cursorPosition, cursorPosition); } } noteInput.focus(); } // Функция для вставки markdown в режиме редактирования function insertMarkdownForEdit(textarea, tag) { const start = textarea.selectionStart; const end = textarea.selectionEnd; const text = textarea.value; const before = text.substring(0, start); const selected = text.substring(start, end); const after = text.substring(end); if (selected.startsWith(tag) && selected.endsWith(tag)) { // Если теги уже есть, удаляем их textarea.value = `${before}${selected.slice( tag.length, -tag.length )}${after}`; textarea.setSelectionRange(start, end - 2 * tag.length); } else if (selected.trim() === "") { // Если текст не выделен if (tag === "[Текст ссылки](URL)") { // Для ссылок создаем шаблон с двумя кавычками textarea.value = `${before}[Текст ссылки](URL)${after}`; const cursorPosition = start + 1; // Помещаем курсор внутрь текста ссылки textarea.setSelectionRange(cursorPosition, cursorPosition + 12); } else if (tag === "- " || tag === "> " || tag === "# ") { // Для списка, цитаты и заголовка помещаем курсор после `- `, `> ` или `# ` textarea.value = `${before}${tag}${after}`; const cursorPosition = start + tag.length; textarea.setSelectionRange(cursorPosition, cursorPosition); } else { // Для остальных типов создаем два тега textarea.value = `${before}${tag}${tag}${after}`; const cursorPosition = start + tag.length; textarea.setSelectionRange(cursorPosition, cursorPosition); } } else { // Если текст выделен if (tag === "[Текст ссылки](URL)") { // Для ссылок используем выделенный текст вместо "Текст ссылки" textarea.value = `${before}[${selected}](URL)${after}`; const cursorPosition = start + selected.length + 3; // Помещаем курсор в URL textarea.setSelectionRange(cursorPosition, cursorPosition + 3); } else if (tag === "- " || tag === "> " || tag === "# ") { // Для списка, цитаты и заголовка добавляем `- `, `> ` или `# ` перед выделенным текстом textarea.value = `${before}${tag}${selected}${after}`; const cursorPosition = start + tag.length + selected.length; textarea.setSelectionRange(cursorPosition, cursorPosition); } else { // Для остальных типов оборачиваем выделенный текст textarea.value = `${before}${tag}${selected}${tag}${after}`; const cursorPosition = start + tag.length + selected.length + tag.length; textarea.setSelectionRange(cursorPosition, cursorPosition); } } textarea.focus(); } // Обработчики для кнопок markdown boldBtn.addEventListener("click", function () { insertMarkdown("**"); }); italicBtn.addEventListener("click", function () { insertMarkdown("*"); }); headerBtn.addEventListener("click", function () { insertMarkdown("# "); }); listBtn.addEventListener("click", function () { insertMarkdown("- "); }); quoteBtn.addEventListener("click", function () { insertMarkdown("> "); }); codeBtn.addEventListener("click", function () { insertMarkdown("`"); }); linkBtn.addEventListener("click", function () { insertMarkdown("[Текст ссылки](URL)"); }); // Обработчик для кнопки загрузки изображений imageBtn.addEventListener("click", function () { imageInput.click(); }); // Обработчик выбора файлов imageInput.addEventListener("change", function (event) { const files = Array.from(event.target.files); files.forEach(file => { if (file.type.startsWith('image/')) { selectedImages.push(file); } }); updateImagePreview(); }); // Обработчик очистки всех изображений clearImagesBtn.addEventListener("click", function () { selectedImages = []; updateImagePreview(); imageInput.value = ""; }); // Обработчики модального окна modalClose.addEventListener("click", function () { imageModal.style.display = "none"; }); imageModal.addEventListener("click", function (event) { if (event.target === imageModal) { imageModal.style.display = "none"; } }); // Закрытие модального окна по Escape document.addEventListener("keydown", function (event) { if (event.key === "Escape" && imageModal.style.display === "block") { imageModal.style.display = "none"; } }); // Функция для обновления превью изображений function updateImagePreview() { if (selectedImages.length === 0) { imagePreviewContainer.style.display = "none"; return; } imagePreviewContainer.style.display = "block"; imagePreviewList.innerHTML = ""; selectedImages.forEach((file, index) => { const reader = new FileReader(); reader.onload = function (e) { const previewItem = document.createElement("div"); previewItem.className = "image-preview-item"; previewItem.innerHTML = ` Preview
${file.name}
`; imagePreviewList.appendChild(previewItem); // Обработчик удаления изображения const removeBtn = previewItem.querySelector(".remove-image-btn"); removeBtn.addEventListener("click", function () { selectedImages.splice(index, 1); updateImagePreview(); }); }; reader.readAsDataURL(file); }); } // Функция для отображения изображения в модальном окне function showImageModal(imageSrc) { console.log('showImageModal called with:', imageSrc); try { modalImage.src = imageSrc; imageModal.style.display = "block"; console.log('Modal opened successfully'); } catch (error) { console.error('Error in showImageModal:', error); } } // Функция для загрузки изображений на сервер async function uploadImages(noteId) { if (selectedImages.length === 0) { return []; } const formData = new FormData(); selectedImages.forEach(file => { formData.append("images", file); }); try { const response = await fetch(`/api/notes/${noteId}/images`, { method: "POST", body: formData, }); if (!response.ok) { throw new Error("Ошибка загрузки изображений"); } const result = await response.json(); return result.images || []; } catch (error) { console.error("Ошибка загрузки изображений:", error); return []; } } // Функция для получения изображений заметки async function getNoteImages(noteId) { try { const response = await fetch(`/api/notes/${noteId}/images`); if (!response.ok) { throw new Error("Ошибка получения изображений"); } return await response.json(); } catch (error) { console.error("Ошибка получения изображений:", error); return []; } } // Функция для удаления изображения заметки async function deleteNoteImage(noteId, imageId) { try { const response = await fetch(`/api/notes/${noteId}/images/${imageId}`, { method: "DELETE", }); if (!response.ok) { throw new Error("Ошибка удаления изображения"); } return true; } catch (error) { console.error("Ошибка удаления изображения:", error); return false; } } // Функция для загрузки заметок с сервера async function loadNotes(forceReload = false) { const now = Date.now(); const CACHE_DURATION = 30000; // 30 секунд кэширования // Используем кэш, если он не устарел и не требуется принудительная перезагрузка if (!forceReload && notesCache && (now - lastLoadTime) < CACHE_DURATION) { allNotes = notesCache; await renderNotes(notesCache); renderCalendar(); renderTags(); renderCalendarMobile(); renderTagsMobile(); return; } // Показываем индикатор загрузки showLoadingIndicator(); try { const response = await fetch("/api/notes"); if (!response.ok) { throw new Error("Ошибка загрузки заметок"); } const notes = await response.json(); allNotes = notes; // Сохраняем все заметки в глобальную переменную notesCache = notes; // Сохраняем в кэш lastLoadTime = now; await renderNotes(notes); renderCalendar(); // Обновляем календарь после загрузки заметок renderTags(); // Обновляем теги после загрузки заметок renderCalendarMobile(); // Обновляем мобильный календарь после загрузки заметок renderTagsMobile(); // Обновляем мобильные теги после загрузки заметок } catch (error) { console.error("Ошибка:", error); notesList.innerHTML = "

Ошибка загрузки заметок

"; } finally { // Скрываем индикатор загрузки hideLoadingIndicator(); } } // Функция для показа индикатора загрузки function showLoadingIndicator() { if (!document.getElementById("loading-indicator")) { const loadingDiv = document.createElement("div"); loadingDiv.id = "loading-indicator"; loadingDiv.innerHTML = `
Загрузка заметок...
`; document.body.appendChild(loadingDiv); } } // Функция для скрытия индикатора загрузки function hideLoadingIndicator() { const loadingIndicator = document.getElementById("loading-indicator"); if (loadingIndicator) { loadingIndicator.remove(); } } // Функция для поиска заметок async function searchNotes(query) { if (!query || query.trim() === "") { searchQuery = ""; searchResults = []; await 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(); await renderNotes(searchResults); } catch (error) { console.error("Ошибка поиска:", error); searchResults = []; await 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'); } // Функция для отображения заметок async 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 = `

${message}

`; return; } // Итерируемся по заметкам в обычном порядке, чтобы новые были сверху for (const note of notesToDisplay) { let contentToProcess = note.content; // Сначала подсвечиваем найденный текст в исходном markdown if (searchQuery) { contentToProcess = highlightSearchText(contentToProcess, searchQuery); } // Затем преобразуем теги в кликабельные элементы const contentWithClickableTags = makeTagsClickable(contentToProcess); const parsedContent = marked.parse(contentWithClickableTags); // Используем изображения, которые уже пришли с заметкой const noteImages = Array.isArray(note.images) ? note.images : []; let imagesHtml = ""; if (noteImages.length > 0) { imagesHtml = '
'; noteImages.forEach(image => { imagesHtml += `
${image.original_name}
`; }); imagesHtml += '
'; } const noteHtml = `
${note.date} ${note.time}
Редактировать
Удалить
${parsedContent}
${imagesHtml}
`; notesList.insertAdjacentHTML("afterbegin", noteHtml); } // Добавляем обработчики событий для кнопок редактирования и удаления addNoteEventListeners(); // Добавляем обработчики кликов для тегов в заметках addTagClickListeners(); // Добавляем обработчики для изображений в заметках addImageEventListeners(); // Обрабатываем длинные заметки handleLongNotes(); } // Функция для обработки длинных заметок function handleLongNotes() { const MAX_HEIGHT = 300; // Максимальная высота в пикселях document.querySelectorAll(".textNote").forEach((noteElement) => { // Проверяем высоту контента const contentHeight = noteElement.scrollHeight; if (contentHeight > MAX_HEIGHT) { // Добавляем класс для сворачивания noteElement.classList.add("collapsed"); // Создаем кнопку "Показать все" const showMoreBtn = document.createElement("button"); showMoreBtn.classList.add("show-more-btn"); showMoreBtn.textContent = "Показать полностью"; showMoreBtn.setAttribute("data-expanded", "false"); // Вставляем кнопку после заметки noteElement.parentElement.insertBefore( showMoreBtn, noteElement.nextSibling ); // Обработчик клика на кнопку showMoreBtn.addEventListener("click", function () { const isExpanded = this.getAttribute("data-expanded") === "true"; if (isExpanded) { // Сворачиваем noteElement.classList.add("collapsed"); this.textContent = "Показать полностью"; this.setAttribute("data-expanded", "false"); } else { // Разворачиваем noteElement.classList.remove("collapsed"); this.textContent = "Свернуть"; this.setAttribute("data-expanded", "true"); } }); } }); } // Функция для добавления обработчиков событий к заметкам function addNoteEventListeners() { // Обработчик удаления document.querySelectorAll("#deleteBtn").forEach((btn) => { btn.addEventListener("click", async function (event) { const noteId = event.target.dataset.id; if (confirm("Вы уверены, что хотите удалить эту заметку?")) { try { const response = await fetch(`/api/notes/${noteId}`, { method: "DELETE", }); if (!response.ok) { throw new Error("Ошибка удаления заметки"); } // Перезагружаем заметки await loadNotes(true); } catch (error) { console.error("Ошибка:", error); alert("Ошибка удаления заметки"); } } }); }); // Обработчик редактирования document.querySelectorAll("#editBtn").forEach((btn) => { btn.addEventListener("click", function (event) { const noteId = event.target.dataset.id; const noteContainer = event.target.closest("#note"); const noteContent = noteContainer.querySelector(".textNote"); // Разворачиваем заметку при редактировании noteContent.classList.remove("collapsed"); // Скрываем кнопку "Показать полностью" если она есть const showMoreBtn = noteContainer.querySelector(".show-more-btn"); if (showMoreBtn) { showMoreBtn.style.display = "none"; } // Создаем контейнер для markdown кнопок const markdownButtonsContainer = document.createElement("div"); markdownButtonsContainer.classList.add("markdown-buttons"); // Создаем markdown кнопки const markdownButtons = [ { 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 = ``; markdownButtonsContainer.appendChild(btn); }); // Создаем textarea с уже существующим классом textInput const textarea = document.createElement("textarea"); textarea.classList.add("textInput"); // Получаем исходный markdown контент из data-атрибута или используем textContent как fallback textarea.value = noteContent.dataset.originalContent || noteContent.textContent; // Привязываем авторасширение к textarea для редактирования textarea.addEventListener("input", function () { autoExpandTextarea(textarea); }); // Контейнер для кнопки сохранения и подсказки const saveButtonContainer = document.createElement("div"); saveButtonContainer.classList.add("save-button-container"); // Кнопка сохранить const saveEditBtn = document.createElement("button"); saveEditBtn.textContent = "Сохранить"; saveEditBtn.classList.add("btnSave"); // Подсказка о горячей клавише const saveHint = document.createElement("span"); saveHint.classList.add("save-hint"); saveHint.textContent = "или нажмите Alt + Enter"; saveButtonContainer.appendChild(saveEditBtn); saveButtonContainer.appendChild(saveHint); // Функция сохранения для редактирования const saveEditNote = async function () { if (textarea.value.trim() !== "") { try { const { date, time } = getFormattedDateTime(); const response = await fetch(`/api/notes/${noteId}`, { method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ content: textarea.value, date: date, time: time, }), }); if (!response.ok) { throw new Error("Ошибка сохранения заметки"); } // Перезагружаем заметки await loadNotes(true); } catch (error) { console.error("Ошибка:", error); alert("Ошибка сохранения заметки"); } } }; // Обработчик горячей клавиши Alt+Enter для сохранения редактирования textarea.addEventListener("keydown", function (event) { if (event.altKey && event.key === "Enter") { event.preventDefault(); saveEditNote(); } }); // Очищаем текущий контент и вставляем markdown кнопки, textarea и контейнер с кнопкой сохранить noteContent.innerHTML = ""; noteContent.appendChild(markdownButtonsContainer); noteContent.appendChild(textarea); noteContent.appendChild(saveButtonContainer); // Применяем авторасширение после добавления в DOM setTimeout(() => { autoExpandTextarea(textarea); textarea.focus(); }, 0); // Добавляем обработчики для markdown кнопок редактирования markdownButtons.forEach((button) => { const btn = document.getElementById(button.id); btn.addEventListener("click", function () { insertMarkdownForEdit(textarea, button.tag); }); }); // Обработчик сохранения редактирования saveEditBtn.addEventListener("click", saveEditNote); }); }); } // Функция для добавления обработчиков кликов на теги в заметках function addTagClickListeners() { document.querySelectorAll(".textNote .tag-in-note").forEach((tagElement) => { tagElement.addEventListener("click", async function (event) { event.preventDefault(); event.stopPropagation(); const clickedTag = this.dataset.tag.toLowerCase(); // Если кликнули на тот же тег, снимаем фильтр if (selectedTagFilter === clickedTag) { selectedTagFilter = null; } else { selectedTagFilter = clickedTag; } // Перерисовываем заметки и теги await renderNotes(allNotes); renderTags(); updateFilterIndicator(); }); }); } // Функция для добавления обработчиков для изображений в заметках function addImageEventListeners() { // Обработчики для кликов на изображения (открытие в модальном окне) document.querySelectorAll(".note-image").forEach((imageElement) => { // Проверяем, не добавлен ли уже обработчик if (imageElement._clickHandler) { return; // Пропускаем, если обработчик уже добавлен } // Создаем новый обработчик imageElement._clickHandler = function (event) { event.preventDefault(); event.stopPropagation(); const imageSrc = this.dataset.imageSrc; console.log('Image clicked, src:', imageSrc); // Для отладки if (imageSrc) { showImageModal(imageSrc); } }; imageElement.addEventListener("click", imageElement._clickHandler); }); // Обработчики для кнопок удаления изображений document.querySelectorAll(".remove-note-image-btn").forEach((buttonElement) => { // Удаляем старые обработчики, если они есть if (buttonElement._clickHandler) { buttonElement.removeEventListener("click", buttonElement._clickHandler); } // Создаем новый обработчик buttonElement._clickHandler = async function (event) { event.preventDefault(); event.stopPropagation(); const noteId = this.dataset.noteId; const imageId = this.dataset.imageId; if (noteId && imageId && confirm("Вы уверены, что хотите удалить это изображение?")) { const success = await deleteNoteImage(noteId, imageId); if (success) { await loadNotes(true); // Перезагружаем заметки } else { alert("Ошибка удаления изображения"); } } }; buttonElement.addEventListener("click", buttonElement._clickHandler); }); } // Функция сохранения заметки (вынесена отдельно для повторного использования) async function saveNote() { if (noteInput.value.trim() !== "" || selectedImages.length > 0) { try { const { date, time } = getFormattedDateTime(); const response = await fetch("/api/notes", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ content: noteInput.value || " ", // Минимальный контент, если только изображения date: date, time: time, }), }); if (!response.ok) { throw new Error("Ошибка сохранения заметки"); } const noteData = await response.json(); const noteId = noteData.id; // Загружаем изображения, если они есть if (selectedImages.length > 0) { await uploadImages(noteId); } // Очищаем поле ввода и изображения, перезагружаем заметки noteInput.value = ""; noteInput.style.height = "auto"; selectedImages = []; updateImagePreview(); imageInput.value = ""; await loadNotes(true); } catch (error) { console.error("Ошибка:", error); alert("Ошибка сохранения заметки"); } } } // Обработчик сохранения новой заметки saveBtn.addEventListener("click", saveNote); // Обработчик горячей клавиши Alt+Enter для сохранения заметки noteInput.addEventListener("keydown", function (event) { if (event.altKey && event.key === "Enter") { event.preventDefault(); // Предотвращаем стандартное поведение saveNote(); } }); // Загружаем заметки при загрузке страницы document.addEventListener("DOMContentLoaded", function () { // Проверяем аутентификацию при загрузке страницы checkAuthentication(); loadUserInfo(); loadNotes(); updateFilterIndicator(); // Добавляем обработчик для кнопки выхода setupLogoutHandler(); }); // Функция для настройки обработчика выхода function setupLogoutHandler() { const logoutForms = document.querySelectorAll('form[action="/logout"]'); logoutForms.forEach(form => { form.addEventListener('submit', function(e) { // Очищаем localStorage перед выходом localStorage.removeItem('isAuthenticated'); localStorage.removeItem('username'); }); }); } // Функция для проверки аутентификации async function checkAuthentication() { const isAuthenticated = localStorage.getItem('isAuthenticated'); if (isAuthenticated !== 'true') { // Если пользователь не аутентифицирован, перенаправляем на страницу входа window.location.href = "/"; return; } // Проверяем, что сессия на сервере еще действительна try { const response = await fetch("/api/auth/status"); if (!response.ok) { // Если сессия недействительна, очищаем localStorage и перенаправляем localStorage.removeItem('isAuthenticated'); localStorage.removeItem('username'); window.location.href = "/"; return; } const authData = await response.json(); if (!authData.authenticated) { // Если сервер говорит, что пользователь не аутентифицирован localStorage.removeItem('isAuthenticated'); localStorage.removeItem('username'); window.location.href = "/"; return; } } catch (error) { console.error("Ошибка проверки аутентификации:", error); // В случае ошибки сети, оставляем пользователя на странице // но показываем предупреждение console.warn("Не удалось проверить статус аутентификации"); } } // Функция для загрузки информации о пользователе async function loadUserInfo() { try { const response = await fetch("/api/user"); if (response.ok) { const user = await response.json(); const usernameDisplay = document.getElementById("username-display"); const userAvatar = document.getElementById("user-avatar"); const userAvatarContainer = document.getElementById( "user-avatar-container" ); if (usernameDisplay) { usernameDisplay.innerHTML = ` ${user.username}`; // Делаем ник кликабельным для перехода в личный кабинет usernameDisplay.style.cursor = "pointer"; usernameDisplay.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", async (event) => await handleDayClick(event)); 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", async (event) => await handleDayClick(event)); 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", async (event) => await handleDayClick(event)); calendarDays.appendChild(dayDiv); } } // Обработчик клика на день в календаре async function handleDayClick(event) { const clickedDate = event.target.dataset.date; // Если кликнули на тот же день, снимаем фильтр if (selectedDateFilter === clickedDate) { selectedDateFilter = null; } else { selectedDateFilter = clickedDate; } // Перерисовываем заметки, календарь и теги await 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( ", " )} `; // Добавляем обработчик клика для кнопки сброса const clearBtn = document.getElementById("clear-filter-btn"); if (clearBtn) { clearBtn.addEventListener("click", clearFilter); } } else { filterIndicator.style.display = "none"; } } // Функция для сброса фильтра (глобальная) window.clearFilter = async 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"; } await renderNotes(allNotes); renderCalendar(); renderTags(); updateFilterIndicator(); }; // Глобальные функции для работы с изображениями (оставляем только showImageModal для совместимости) // window.showImageModal удалена, так как она создавала рекурсию // Обработчики для кнопок навигации календаря 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", async function () { searchInput.value = ""; this.style.display = "none"; searchQuery = ""; searchResults = []; await renderNotes(allNotes); updateFilterIndicator(); }); // Обработчик клавиши Escape для очистки поиска searchInput.addEventListener("keydown", async function (event) { if (event.key === "Escape") { this.value = ""; clearSearchBtn.style.display = "none"; searchQuery = ""; searchResults = []; await renderNotes(allNotes); updateFilterIndicator(); } }); } // ==================== МОБИЛЬНЫЙ СЛАЙДЕР ==================== // Функция для отображения календаря в мобильном слайдере function renderCalendarMobile() { const calendarDays = document.getElementById("calendarDaysMobile"); const monthYear = document.getElementById("monthYearMobile"); // Проверяем, существуют ли элементы календаря для мобильной версии if (!calendarDays || !monthYear) return; const year = currentDate.getFullYear(); const month = currentDate.getMonth(); // Массив названий месяцев const monthNames = [ "Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь", ]; // Устанавливаем заголовок месяца и года monthYear.textContent = `${monthNames[month]} ${year}`; // Получаем первый день месяца const firstDay = new Date(year, month, 1); // Получаем последний день месяца const lastDay = new Date(year, month + 1, 0); // Получаем день недели первого дня (0 - воскресенье, 1 - понедельник и т.д.) let firstDayOfWeek = firstDay.getDay(); // Преобразуем так, чтобы понедельник был первым днем (0) firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1; // Очищаем календарь calendarDays.innerHTML = ""; // Создаём Set дат, когда были созданы заметки const noteDates = new Set(); allNotes.forEach((note) => { noteDates.add(note.date); }); // Получаем последний день предыдущего месяца const prevMonthLastDay = new Date(year, month, 0).getDate(); const prevMonth = month === 0 ? 11 : month - 1; const prevYear = month === 0 ? year - 1 : year; // Добавляем дни предыдущего месяца for (let i = firstDayOfWeek - 1; i >= 0; i--) { const day = prevMonthLastDay - i; const dateStr = `${String(day).padStart(2, "0")}.${String( prevMonth + 1 ).padStart(2, "0")}.${prevYear}`; const dayDiv = document.createElement("div"); dayDiv.classList.add("calendar-day", "other-month"); dayDiv.textContent = day; dayDiv.dataset.date = dateStr; // Проверяем, есть ли заметки на этот день if (noteDates.has(dateStr)) { dayDiv.classList.add("has-notes"); } // Проверяем, выбран ли этот день if (selectedDateFilter === dateStr) { dayDiv.classList.add("selected"); } // Добавляем обработчик клика dayDiv.addEventListener("click", async (event) => await handleDayClickMobile(event)); 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", async (event) => await handleDayClickMobile(event)); 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", async (event) => await handleDayClickMobile(event)); calendarDays.appendChild(dayDiv); } } // Обработчик клика на день в календаре для мобильной версии async function handleDayClickMobile(event) { const clickedDate = event.target.dataset.date; // Если кликнули на тот же день, снимаем фильтр if (selectedDateFilter === clickedDate) { selectedDateFilter = null; } else { selectedDateFilter = clickedDate; } // Перерисовываем заметки, оба календаря и теги await renderNotes(allNotes); renderCalendar(); renderCalendarMobile(); renderTags(); renderTagsMobile(); updateFilterIndicator(); } // Функция для отображения тегов в мобильном слайдере function renderTagsMobile() { const tagsContainer = document.getElementById("tagsContainerMobile"); if (!tagsContainer) return; const tagCounts = getAllTags(allNotes); const sortedTags = Object.keys(tagCounts).sort(); if (sortedTags.length === 0) { tagsContainer.innerHTML = '
Нет тегов
'; return; } tagsContainer.innerHTML = sortedTags .map((tag) => { const count = tagCounts[tag]; const isActive = selectedTagFilter === tag ? "active" : ""; return `#${tag}${count}`; }) .join(""); // Добавляем обработчики кликов для тегов tagsContainer.querySelectorAll(".tag").forEach((tagElement) => { tagElement.addEventListener("click", async (event) => await handleTagClickMobile(event)); }); } // Обработчик клика на тег в мобильном слайдере async function handleTagClickMobile(event) { const clickedTag = event.target.closest(".tag").dataset.tag; // Если кликнули на тот же тег, снимаем фильтр if (selectedTagFilter === clickedTag) { selectedTagFilter = null; } else { selectedTagFilter = clickedTag; } // Перерисовываем заметки, теги и оба календаря await renderNotes(allNotes); renderTags(); renderTagsMobile(); renderCalendar(); renderCalendarMobile(); updateFilterIndicator(); } // Инициализация мобильного слайдера document.addEventListener("DOMContentLoaded", function () { const mobileMenuBtn = document.getElementById("mobileMenuBtn"); const sidebarCloseBtn = document.getElementById("sidebarCloseBtn"); const mobileSidebar = document.getElementById("mobileSidebar"); const sidebarOverlay = document.getElementById("sidebarOverlay"); // Открытие слайдера при клике на кнопку меню if (mobileMenuBtn) { mobileMenuBtn.addEventListener("click", function () { mobileSidebar.classList.add("open"); sidebarOverlay.classList.add("open"); document.body.style.overflow = "hidden"; }); } // Закрытие слайдера при клике на кнопку закрытия if (sidebarCloseBtn) { sidebarCloseBtn.addEventListener("click", function () { mobileSidebar.classList.remove("open"); sidebarOverlay.classList.remove("open"); document.body.style.overflow = "auto"; }); } // Закрытие слайдера при клике на оверлей if (sidebarOverlay) { sidebarOverlay.addEventListener("click", function () { mobileSidebar.classList.remove("open"); sidebarOverlay.classList.remove("open"); document.body.style.overflow = "auto"; }); } // Инициализация мобильного поиска initSearchMobile(); // Инициализация мобильного календаря renderCalendarMobile(); renderTagsMobile(); // Обработчики для кнопок навигации мобильного календаря const prevMonthBtnMobile = document.getElementById("prevMonthMobile"); const nextMonthBtnMobile = document.getElementById("nextMonthMobile"); if (prevMonthBtnMobile) { prevMonthBtnMobile.addEventListener("click", function () { currentDate.setMonth(currentDate.getMonth() - 1); renderCalendar(); renderCalendarMobile(); }); } if (nextMonthBtnMobile) { nextMonthBtnMobile.addEventListener("click", function () { currentDate.setMonth(currentDate.getMonth() + 1); renderCalendar(); renderCalendarMobile(); }); } }); // Функция для инициализации мобильного поиска function initSearchMobile() { const searchInput = document.getElementById("searchInputMobile"); const clearSearchBtn = document.getElementById("clearSearchBtnMobile"); if (!searchInput || !clearSearchBtn) return; // Обработчик ввода в поле поиска с задержкой let searchTimeout; searchInput.addEventListener("input", function () { clearTimeout(searchTimeout); const query = this.value; // Показываем/скрываем кнопку очистки if (query.trim()) { clearSearchBtn.style.display = "block"; } else { clearSearchBtn.style.display = "none"; } // Задержка перед поиском для оптимизации searchTimeout = setTimeout(() => { searchNotes(query); // Обновляем основное поле поиска const mainSearchInput = document.getElementById("searchInput"); if (mainSearchInput) { mainSearchInput.value = query; } const mainClearSearchBtn = document.getElementById("clearSearchBtn"); if (mainClearSearchBtn) { if (query.trim()) { mainClearSearchBtn.style.display = "block"; } else { mainClearSearchBtn.style.display = "none"; } } updateFilterIndicator(); }, 300); }); // Обработчик клика на кнопку очистки поиска clearSearchBtn.addEventListener("click", async function () { searchInput.value = ""; this.style.display = "none"; searchQuery = ""; searchResults = []; // Обновляем основное поле поиска const mainSearchInput = document.getElementById("searchInput"); if (mainSearchInput) { mainSearchInput.value = ""; } const mainClearSearchBtn = document.getElementById("clearSearchBtn"); if (mainClearSearchBtn) { mainClearSearchBtn.style.display = "none"; } await renderNotes(allNotes); updateFilterIndicator(); }); // Обработчик клавиши Escape для очистки поиска searchInput.addEventListener("keydown", function (event) { if (event.key === "Escape") { this.value = ""; clearSearchBtn.style.display = "none"; searchQuery = ""; searchResults = []; // Обновляем основное поле поиска const mainSearchInput = document.getElementById("searchInput"); if (mainSearchInput) { mainSearchInput.value = ""; } const mainClearSearchBtn = document.getElementById("clearSearchBtn"); if (mainClearSearchBtn) { mainClearSearchBtn.style.display = "none"; } renderNotes(allNotes); updateFilterIndicator(); } }); }