From 62d9b6c7cefd83b339b863d5afcce9ec4eda8551 Mon Sep 17 00:00:00 2001 From: Fovway Date: Sun, 19 Oct 2025 23:27:57 +0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80?= =?UTF-8?q?=D0=B6=D0=BA=D0=B0=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=B8=20=D1=83=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=B8=D0=B7=D0=BE=D0=B1=D1=80=D0=B0=D0=B6?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=D0=BC=D0=B8=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BC=D0=B5=D1=82=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Реализована возможность загрузки изображений к заметкам с использованием multer - Добавлены API для загрузки, получения и удаления изображений заметок - Обновлен интерфейс для отображения загруженных изображений и их предварительного просмотра - Добавлены стили для управления изображениями и модального окна просмотра --- public/app.js | 332 ++++++++++++++++++++++++++++++++++++++++------ public/notes.html | 22 +++ public/style.css | 191 ++++++++++++++++++++++++++ server.js | 277 ++++++++++++++++++++++++++++++++++++-- test_images.html | 292 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1066 insertions(+), 48 deletions(-) create mode 100644 test_images.html diff --git a/public/app.js b/public/app.js index 891765c..af87151 100644 --- a/public/app.js +++ b/public/app.js @@ -11,6 +11,21 @@ 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 = []; @@ -134,12 +149,12 @@ function renderTags() { // Добавляем обработчики кликов для тегов tagsContainer.querySelectorAll(".tag").forEach((tagElement) => { - tagElement.addEventListener("click", handleTagClick); + tagElement.addEventListener("click", async (event) => await handleTagClick(event)); }); } // Обработчик клика на тег -function handleTagClick(event) { +async function handleTagClick(event) { const clickedTag = event.target.closest(".tag").dataset.tag; // Если кликнули на тот же тег, снимаем фильтр @@ -150,7 +165,7 @@ function handleTagClick(event) { } // Перерисовываем заметки и теги - renderNotes(allNotes); + await renderNotes(allNotes); renderTags(); updateFilterIndicator(); } @@ -308,6 +323,155 @@ 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() { try { @@ -317,7 +481,7 @@ async function loadNotes() { } const notes = await response.json(); allNotes = notes; // Сохраняем все заметки в глобальную переменную - renderNotes(notes); + await renderNotes(notes); renderCalendar(); // Обновляем календарь после загрузки заметок renderTags(); // Обновляем теги после загрузки заметок renderCalendarMobile(); // Обновляем мобильный календарь после загрузки заметок @@ -333,7 +497,7 @@ async function searchNotes(query) { if (!query || query.trim() === "") { searchQuery = ""; searchResults = []; - renderNotes(allNotes); + await renderNotes(allNotes); return; } @@ -356,11 +520,11 @@ async function searchNotes(query) { searchResults = await response.json(); searchQuery = query.trim(); - renderNotes(searchResults); + await renderNotes(searchResults); } catch (error) { console.error("Ошибка поиска:", error); searchResults = []; - renderNotes(allNotes); + await renderNotes(allNotes); } } @@ -378,7 +542,7 @@ function highlightSearchText(content, query) { } // Функция для отображения заметок -function renderNotes(notes) { +async function renderNotes(notes) { notesList.innerHTML = ""; // Фильтруем заметки по дате и тегам @@ -414,7 +578,7 @@ function renderNotes(notes) { } // Итерируемся по заметкам в обычном порядке, чтобы новые были сверху - notesToDisplay.forEach(function (note) { + for (const note of notesToDisplay) { let contentToProcess = note.content; // Сначала подсвечиваем найденный текст в исходном markdown @@ -427,8 +591,25 @@ function renderNotes(notes) { const parsedContent = marked.parse(contentWithClickableTags); + // Получаем изображения заметки + const noteImages = await getNoteImages(note.id); + 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(); @@ -453,6 +635,9 @@ function renderNotes(notes) { // Добавляем обработчики кликов для тегов в заметках addTagClickListeners(); + // Добавляем обработчики для изображений в заметках + addImageEventListeners(); + // Обрабатываем длинные заметки handleLongNotes(); } @@ -518,7 +703,7 @@ function addNoteEventListeners() { } // Перезагружаем заметки - loadNotes(); + await loadNotes(); } catch (error) { console.error("Ошибка:", error); alert("Ошибка удаления заметки"); @@ -617,7 +802,7 @@ function addNoteEventListeners() { } // Перезагружаем заметки - loadNotes(); + await loadNotes(); } catch (error) { console.error("Ошибка:", error); alert("Ошибка сохранения заметки"); @@ -662,7 +847,7 @@ function addNoteEventListeners() { // Функция для добавления обработчиков кликов на теги в заметках function addTagClickListeners() { document.querySelectorAll(".textNote .tag-in-note").forEach((tagElement) => { - tagElement.addEventListener("click", function (event) { + tagElement.addEventListener("click", async function (event) { event.preventDefault(); event.stopPropagation(); @@ -676,16 +861,69 @@ function addTagClickListeners() { } // Перерисовываем заметки и теги - renderNotes(allNotes); + 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(); // Перезагружаем заметки + } else { + alert("Ошибка удаления изображения"); + } + } + }; + + buttonElement.addEventListener("click", buttonElement._clickHandler); + }); +} + + // Функция сохранения заметки (вынесена отдельно для повторного использования) async function saveNote() { - if (noteInput.value.trim() !== "") { + if (noteInput.value.trim() !== "" || selectedImages.length > 0) { try { const { date, time } = getFormattedDateTime(); @@ -695,7 +933,7 @@ async function saveNote() { "Content-Type": "application/json", }, body: JSON.stringify({ - content: noteInput.value, + content: noteInput.value || " ", // Минимальный контент, если только изображения date: date, time: time, }), @@ -705,10 +943,21 @@ async function saveNote() { 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"; - loadNotes(); + selectedImages = []; + updateImagePreview(); + imageInput.value = ""; + await loadNotes(); } catch (error) { console.error("Ошибка:", error); alert("Ошибка сохранения заметки"); @@ -899,7 +1148,7 @@ function renderCalendar() { } // Добавляем обработчик клика - dayDiv.addEventListener("click", handleDayClick); + dayDiv.addEventListener("click", async (event) => await handleDayClick(event)); calendarDays.appendChild(dayDiv); } @@ -936,7 +1185,7 @@ function renderCalendar() { } // Добавляем обработчик клика - dayDiv.addEventListener("click", handleDayClick); + dayDiv.addEventListener("click", async (event) => await handleDayClick(event)); calendarDays.appendChild(dayDiv); } @@ -968,14 +1217,14 @@ function renderCalendar() { } // Добавляем обработчик клика - dayDiv.addEventListener("click", handleDayClick); + dayDiv.addEventListener("click", async (event) => await handleDayClick(event)); calendarDays.appendChild(dayDiv); } } // Обработчик клика на день в календаре -function handleDayClick(event) { +async function handleDayClick(event) { const clickedDate = event.target.dataset.date; // Если кликнули на тот же день, снимаем фильтр @@ -986,7 +1235,7 @@ function handleDayClick(event) { } // Перерисовываем заметки, календарь и теги - renderNotes(allNotes); + await renderNotes(allNotes); renderCalendar(); renderTags(); updateFilterIndicator(); @@ -1028,7 +1277,7 @@ function updateFilterIndicator() { } // Функция для сброса фильтра (глобальная) -window.clearFilter = function () { +window.clearFilter = async function () { selectedDateFilter = null; selectedTagFilter = null; searchQuery = ""; @@ -1046,12 +1295,15 @@ window.clearFilter = function () { clearSearchBtn.style.display = "none"; } - renderNotes(allNotes); + await renderNotes(allNotes); renderCalendar(); renderTags(); updateFilterIndicator(); }; +// Глобальные функции для работы с изображениями (оставляем только showImageModal для совместимости) +// window.showImageModal удалена, так как она создавала рекурсию + // Обработчики для кнопок навигации календаря const prevMonthBtn = document.getElementById("prevMonth"); const nextMonthBtn = document.getElementById("nextMonth"); @@ -1106,23 +1358,23 @@ function initSearch() { }); // Обработчик клика на кнопку очистки поиска - clearSearchBtn.addEventListener("click", function () { + clearSearchBtn.addEventListener("click", async function () { searchInput.value = ""; this.style.display = "none"; searchQuery = ""; searchResults = []; - renderNotes(allNotes); + await renderNotes(allNotes); updateFilterIndicator(); }); // Обработчик клавиши Escape для очистки поиска - searchInput.addEventListener("keydown", function (event) { + searchInput.addEventListener("keydown", async function (event) { if (event.key === "Escape") { this.value = ""; clearSearchBtn.style.display = "none"; searchQuery = ""; searchResults = []; - renderNotes(allNotes); + await renderNotes(allNotes); updateFilterIndicator(); } }); @@ -1206,7 +1458,7 @@ function renderCalendarMobile() { } // Добавляем обработчик клика - dayDiv.addEventListener("click", handleDayClickMobile); + dayDiv.addEventListener("click", async (event) => await handleDayClickMobile(event)); calendarDays.appendChild(dayDiv); } @@ -1243,7 +1495,7 @@ function renderCalendarMobile() { } // Добавляем обработчик клика - dayDiv.addEventListener("click", handleDayClickMobile); + dayDiv.addEventListener("click", async (event) => await handleDayClickMobile(event)); calendarDays.appendChild(dayDiv); } @@ -1275,14 +1527,14 @@ function renderCalendarMobile() { } // Добавляем обработчик клика - dayDiv.addEventListener("click", handleDayClickMobile); + dayDiv.addEventListener("click", async (event) => await handleDayClickMobile(event)); calendarDays.appendChild(dayDiv); } } // Обработчик клика на день в календаре для мобильной версии -function handleDayClickMobile(event) { +async function handleDayClickMobile(event) { const clickedDate = event.target.dataset.date; // Если кликнули на тот же день, снимаем фильтр @@ -1293,7 +1545,7 @@ function handleDayClickMobile(event) { } // Перерисовываем заметки, оба календаря и теги - renderNotes(allNotes); + await renderNotes(allNotes); renderCalendar(); renderCalendarMobile(); renderTags(); @@ -1325,12 +1577,12 @@ function renderTagsMobile() { // Добавляем обработчики кликов для тегов tagsContainer.querySelectorAll(".tag").forEach((tagElement) => { - tagElement.addEventListener("click", handleTagClickMobile); + tagElement.addEventListener("click", async (event) => await handleTagClickMobile(event)); }); } // Обработчик клика на тег в мобильном слайдере -function handleTagClickMobile(event) { +async function handleTagClickMobile(event) { const clickedTag = event.target.closest(".tag").dataset.tag; // Если кликнули на тот же тег, снимаем фильтр @@ -1341,7 +1593,7 @@ function handleTagClickMobile(event) { } // Перерисовываем заметки, теги и оба календаря - renderNotes(allNotes); + await renderNotes(allNotes); renderTags(); renderTagsMobile(); renderCalendar(); @@ -1452,7 +1704,7 @@ function initSearchMobile() { }); // Обработчик клика на кнопку очистки поиска - clearSearchBtn.addEventListener("click", function () { + clearSearchBtn.addEventListener("click", async function () { searchInput.value = ""; this.style.display = "none"; searchQuery = ""; @@ -1466,7 +1718,7 @@ function initSearchMobile() { if (mainClearSearchBtn) { mainClearSearchBtn.style.display = "none"; } - renderNotes(allNotes); + await renderNotes(allNotes); updateFilterIndicator(); }); diff --git a/public/notes.html b/public/notes.html index d27cc49..7b6425b 100644 --- a/public/notes.html +++ b/public/notes.html @@ -197,6 +197,9 @@ +
+ + + + + + +
или нажмите Alt + Enter @@ -216,6 +232,12 @@ + + +
+ × + +
+ +