✨ Добавлена поддержка загрузки и управления изображениями для заметок
- Реализована возможность загрузки изображений к заметкам с использованием multer - Добавлены API для загрузки, получения и удаления изображений заметок - Обновлен интерфейс для отображения загруженных изображений и их предварительного просмотра - Добавлены стили для управления изображениями и модального окна просмотра
This commit is contained in:
parent
bb5c3fede7
commit
62d9b6c7ce
332
public/app.js
332
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 = `
|
||||
<img src="${e.target.result}" alt="Preview">
|
||||
<button class="remove-image-btn" data-index="${index}">×</button>
|
||||
<div class="image-info">${file.name}</div>
|
||||
`;
|
||||
|
||||
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 = '<div class="note-images-container">';
|
||||
noteImages.forEach(image => {
|
||||
imagesHtml += `
|
||||
<div class="note-image-item">
|
||||
<img src="${image.file_path}" alt="${image.original_name}" class="note-image" data-image-src="${image.file_path}">
|
||||
<button class="remove-note-image-btn" data-note-id="${note.id}" data-image-id="${image.id}" title="Удалить изображение">×</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
imagesHtml += '</div>';
|
||||
}
|
||||
|
||||
const noteHtml = `
|
||||
<div id="note" class="container">
|
||||
<div id="note" class="container" data-note-id="${note.id}">
|
||||
<div class="date">
|
||||
${note.date} ${note.time}
|
||||
<div id="editBtn" class="notesHeaderBtn" data-id="${
|
||||
@ -442,10 +623,11 @@ function renderNotes(notes) {
|
||||
/"/g,
|
||||
"""
|
||||
)}">${parsedContent}</div>
|
||||
${imagesHtml}
|
||||
</div>
|
||||
`;
|
||||
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();
|
||||
});
|
||||
|
||||
|
||||
@ -197,6 +197,9 @@
|
||||
<button class="btnMarkdown" id="linkBtn" title="Ссылка">
|
||||
<span class="iconify" data-icon="mdi:link"></span>
|
||||
</button>
|
||||
<button class="btnMarkdown" id="imageBtn" title="Загрузить изображения">
|
||||
<span class="iconify" data-icon="mdi:image-plus"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
@ -204,6 +207,19 @@
|
||||
id="noteInput"
|
||||
placeholder="Ваша заметка..."
|
||||
></textarea>
|
||||
|
||||
<!-- Скрытый input для загрузки изображений -->
|
||||
<input type="file" id="imageInput" accept="image/*" multiple style="display: none;">
|
||||
|
||||
<!-- Контейнер для отображения загруженных изображений -->
|
||||
<div id="imagePreviewContainer" class="image-preview-container" style="display: none;">
|
||||
<div class="image-preview-header">
|
||||
<span>Загруженные изображения:</span>
|
||||
<button type="button" id="clearImagesBtn" class="clear-images-btn">Очистить все</button>
|
||||
</div>
|
||||
<div id="imagePreviewList" class="image-preview-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="save-button-container">
|
||||
<button class="btnSave" id="saveBtn">Сохранить</button>
|
||||
<span class="save-hint">или нажмите Alt + Enter</span>
|
||||
@ -216,6 +232,12 @@
|
||||
<div class="footer">
|
||||
<p>Создатель: <span>Fovway</span></p>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно для просмотра изображений -->
|
||||
<div id="imageModal" class="image-modal">
|
||||
<span class="image-modal-close">×</span>
|
||||
<img class="image-modal-content" id="modalImage">
|
||||
</div>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/11.1.0/marked.min.js"></script>
|
||||
<script src="/app.js"></script>
|
||||
<script>
|
||||
|
||||
191
public/style.css
191
public/style.css
@ -1298,3 +1298,194 @@ textarea:focus {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Стили для загрузки изображений */
|
||||
.image-preview-container {
|
||||
margin: 15px 0;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border: 2px dashed #dee2e6;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.image-preview-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.clear-images-btn {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.clear-images-btn:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
.image-preview-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.image-preview-item {
|
||||
position: relative;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.image-preview-item img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.image-preview-item .remove-image-btn {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
background: rgba(220, 53, 69, 0.8);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.image-preview-item .remove-image-btn:hover {
|
||||
background: rgba(220, 53, 69, 1);
|
||||
}
|
||||
|
||||
.image-preview-item .image-info {
|
||||
padding: 4px 6px;
|
||||
font-size: 10px;
|
||||
color: #6c757d;
|
||||
background: #f8f9fa;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Стили для изображений в заметках */
|
||||
.note-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 6px;
|
||||
margin: 10px 0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.note-image:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.note-images-container {
|
||||
margin: 10px 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.note-image-item {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.note-image-item .remove-note-image-btn {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
background: rgba(220, 53, 69, 0.8);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.3s ease;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.note-image-item:hover .remove-note-image-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.note-image-item .remove-note-image-btn:hover {
|
||||
background: rgba(220, 53, 69, 1);
|
||||
}
|
||||
|
||||
/* Модальное окно для просмотра изображений */
|
||||
.image-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0,0,0,0.9);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.image-modal-content {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.image-modal-close {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 35px;
|
||||
color: #f1f1f1;
|
||||
font-size: 40px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.image-modal-close:hover {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
/* Адаптивность для изображений */
|
||||
@media (max-width: 768px) {
|
||||
.image-preview-list {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.note-images-container {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.image-preview-item img {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
277
server.js
277
server.js
@ -27,7 +27,7 @@ if (!fs.existsSync(databaseDir)) {
|
||||
}
|
||||
|
||||
// Настройка multer для загрузки аватарок
|
||||
const storage = multer.diskStorage({
|
||||
const avatarStorage = multer.diskStorage({
|
||||
destination: function (req, file, cb) {
|
||||
cb(null, uploadsDir);
|
||||
},
|
||||
@ -44,8 +44,26 @@ const storage = multer.diskStorage({
|
||||
},
|
||||
});
|
||||
|
||||
// Настройка multer для загрузки изображений заметок
|
||||
const noteImageStorage = multer.diskStorage({
|
||||
destination: function (req, file, cb) {
|
||||
cb(null, uploadsDir);
|
||||
},
|
||||
filename: function (req, file, cb) {
|
||||
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9);
|
||||
cb(
|
||||
null,
|
||||
"note-image-" +
|
||||
req.session.userId +
|
||||
"-" +
|
||||
uniqueSuffix +
|
||||
path.extname(file.originalname)
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage: storage,
|
||||
storage: avatarStorage,
|
||||
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB максимум
|
||||
fileFilter: function (req, file, cb) {
|
||||
const filetypes = /jpeg|jpg|png|gif/;
|
||||
@ -61,6 +79,23 @@ const upload = multer({
|
||||
},
|
||||
});
|
||||
|
||||
const uploadNoteImages = multer({
|
||||
storage: noteImageStorage,
|
||||
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB максимум для изображений заметок
|
||||
fileFilter: function (req, file, cb) {
|
||||
const filetypes = /jpeg|jpg|png|gif|webp/;
|
||||
const mimetype = filetypes.test(file.mimetype);
|
||||
const extname = filetypes.test(
|
||||
path.extname(file.originalname).toLowerCase()
|
||||
);
|
||||
|
||||
if (mimetype && extname) {
|
||||
return cb(null, true);
|
||||
}
|
||||
cb(new Error("Только изображения (jpeg, jpg, png, gif, webp) разрешены!"));
|
||||
},
|
||||
});
|
||||
|
||||
// Middleware для безопасности
|
||||
app.use(
|
||||
helmet({
|
||||
@ -143,6 +178,20 @@ function createTables() {
|
||||
)
|
||||
`;
|
||||
|
||||
const createNoteImagesTable = `
|
||||
CREATE TABLE IF NOT EXISTS note_images (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
note_id INTEGER NOT NULL,
|
||||
filename TEXT NOT NULL,
|
||||
original_name TEXT NOT NULL,
|
||||
file_path TEXT NOT NULL,
|
||||
file_size INTEGER NOT NULL,
|
||||
mime_type TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE
|
||||
)
|
||||
`;
|
||||
|
||||
const createUsersTable = `
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@ -162,6 +211,14 @@ function createTables() {
|
||||
}
|
||||
});
|
||||
|
||||
db.run(createNoteImagesTable, (err) => {
|
||||
if (err) {
|
||||
console.error("Ошибка создания таблицы изображений заметок:", err.message);
|
||||
} else {
|
||||
console.log("Таблица изображений заметок готова");
|
||||
}
|
||||
});
|
||||
|
||||
db.run(createUsersTable, (err) => {
|
||||
if (err) {
|
||||
console.error("Ошибка создания таблицы пользователей:", err.message);
|
||||
@ -472,17 +529,221 @@ app.delete("/api/notes/:id", requireAuth, (req, res) => {
|
||||
return res.status(403).json({ error: "Нет доступа к этой заметке" });
|
||||
}
|
||||
|
||||
const deleteSql = "DELETE FROM notes WHERE id = ?";
|
||||
|
||||
db.run(deleteSql, id, function (err) {
|
||||
// Сначала удаляем все изображения заметки
|
||||
const getImagesSql = "SELECT file_path FROM note_images WHERE note_id = ?";
|
||||
db.all(getImagesSql, [id], (err, images) => {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления заметки:", err.message);
|
||||
console.error("Ошибка получения изображений:", err.message);
|
||||
} else {
|
||||
// Удаляем файлы изображений
|
||||
images.forEach((image) => {
|
||||
const imagePath = path.join(__dirname, "public", image.file_path);
|
||||
if (fs.existsSync(imagePath)) {
|
||||
fs.unlinkSync(imagePath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Удаляем записи об изображениях из БД
|
||||
const deleteImagesSql = "DELETE FROM note_images WHERE note_id = ?";
|
||||
db.run(deleteImagesSql, [id], (err) => {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления изображений:", err.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Удаляем саму заметку
|
||||
const deleteSql = "DELETE FROM notes WHERE id = ?";
|
||||
db.run(deleteSql, id, function (err) {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления заметки:", err.message);
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
if (this.changes === 0) {
|
||||
return res.status(404).json({ error: "Заметка не найдена" });
|
||||
}
|
||||
res.json({ message: "Заметка удалена" });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// API для загрузки изображений к заметке
|
||||
app.post(
|
||||
"/api/notes/:id/images",
|
||||
requireAuth,
|
||||
uploadNoteImages.array("images", 10), // Максимум 10 изображений
|
||||
(req, res) => {
|
||||
const { id } = req.params;
|
||||
const files = req.files;
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
return res.status(400).json({ error: "Файлы не загружены" });
|
||||
}
|
||||
|
||||
// Проверяем, что заметка принадлежит текущему пользователю
|
||||
const checkSql = "SELECT user_id FROM notes WHERE id = ?";
|
||||
db.get(checkSql, [id], (err, row) => {
|
||||
if (err) {
|
||||
console.error("Ошибка проверки доступа:", err.message);
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
if (this.changes === 0) {
|
||||
|
||||
if (!row) {
|
||||
return res.status(404).json({ error: "Заметка не найдена" });
|
||||
}
|
||||
res.json({ message: "Заметка удалена" });
|
||||
|
||||
if (row.user_id !== req.session.userId) {
|
||||
return res.status(403).json({ error: "Нет доступа к этой заметке" });
|
||||
}
|
||||
|
||||
// Сохраняем информацию об изображениях в БД
|
||||
const insertSql = `
|
||||
INSERT INTO note_images (note_id, filename, original_name, file_path, file_size, mime_type)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`;
|
||||
|
||||
const uploadedImages = [];
|
||||
let completed = 0;
|
||||
|
||||
files.forEach((file) => {
|
||||
const filePath = "/uploads/" + file.filename;
|
||||
const params = [
|
||||
id,
|
||||
file.filename,
|
||||
file.originalname,
|
||||
filePath,
|
||||
file.size,
|
||||
file.mimetype,
|
||||
];
|
||||
|
||||
db.run(insertSql, params, function (err) {
|
||||
if (err) {
|
||||
console.error("Ошибка сохранения изображения:", err.message);
|
||||
// Удаляем файл, если не удалось сохранить в БД
|
||||
const imagePath = path.join(__dirname, "public", filePath);
|
||||
if (fs.existsSync(imagePath)) {
|
||||
fs.unlinkSync(imagePath);
|
||||
}
|
||||
} else {
|
||||
uploadedImages.push({
|
||||
id: this.lastID,
|
||||
filename: file.filename,
|
||||
original_name: file.originalname,
|
||||
file_path: filePath,
|
||||
file_size: file.size,
|
||||
mime_type: file.mimetype,
|
||||
});
|
||||
}
|
||||
|
||||
completed++;
|
||||
if (completed === files.length) {
|
||||
if (uploadedImages.length === 0) {
|
||||
return res.status(500).json({ error: "Не удалось загрузить изображения" });
|
||||
}
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Загружено ${uploadedImages.length} изображений`,
|
||||
images: uploadedImages
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// API для получения изображений заметки
|
||||
app.get("/api/notes/:id/images", requireAuth, (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
// Проверяем, что заметка принадлежит текущему пользователю
|
||||
const checkSql = "SELECT user_id FROM notes WHERE id = ?";
|
||||
db.get(checkSql, [id], (err, row) => {
|
||||
if (err) {
|
||||
console.error("Ошибка проверки доступа:", err.message);
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
|
||||
if (!row) {
|
||||
return res.status(404).json({ error: "Заметка не найдена" });
|
||||
}
|
||||
|
||||
if (row.user_id !== req.session.userId) {
|
||||
return res.status(403).json({ error: "Нет доступа к этой заметке" });
|
||||
}
|
||||
|
||||
// Получаем изображения заметки
|
||||
const getImagesSql = `
|
||||
SELECT id, filename, original_name, file_path, file_size, mime_type, created_at
|
||||
FROM note_images
|
||||
WHERE note_id = ?
|
||||
ORDER BY created_at ASC
|
||||
`;
|
||||
|
||||
db.all(getImagesSql, [id], (err, images) => {
|
||||
if (err) {
|
||||
console.error("Ошибка получения изображений:", err.message);
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
|
||||
res.json(images);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// API для удаления изображения заметки
|
||||
app.delete("/api/notes/:noteId/images/:imageId", requireAuth, (req, res) => {
|
||||
const { noteId, imageId } = req.params;
|
||||
|
||||
// Проверяем, что заметка принадлежит текущему пользователю
|
||||
const checkSql = "SELECT user_id FROM notes WHERE id = ?";
|
||||
db.get(checkSql, [noteId], (err, row) => {
|
||||
if (err) {
|
||||
console.error("Ошибка проверки доступа:", err.message);
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
|
||||
if (!row) {
|
||||
return res.status(404).json({ error: "Заметка не найдена" });
|
||||
}
|
||||
|
||||
if (row.user_id !== req.session.userId) {
|
||||
return res.status(403).json({ error: "Нет доступа к этой заметке" });
|
||||
}
|
||||
|
||||
// Получаем информацию об изображении
|
||||
const getImageSql = "SELECT file_path FROM note_images WHERE id = ? AND note_id = ?";
|
||||
db.get(getImageSql, [imageId, noteId], (err, image) => {
|
||||
if (err) {
|
||||
console.error("Ошибка получения изображения:", err.message);
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
|
||||
if (!image) {
|
||||
return res.status(404).json({ error: "Изображение не найдено" });
|
||||
}
|
||||
|
||||
// Удаляем файл изображения
|
||||
const imagePath = path.join(__dirname, "public", image.file_path);
|
||||
if (fs.existsSync(imagePath)) {
|
||||
fs.unlinkSync(imagePath);
|
||||
}
|
||||
|
||||
// Удаляем запись из БД
|
||||
const deleteSql = "DELETE FROM note_images WHERE id = ? AND note_id = ?";
|
||||
db.run(deleteSql, [imageId, noteId], function (err) {
|
||||
if (err) {
|
||||
console.error("Ошибка удаления изображения:", err.message);
|
||||
return res.status(500).json({ error: "Ошибка сервера" });
|
||||
}
|
||||
|
||||
if (this.changes === 0) {
|
||||
return res.status(404).json({ error: "Изображение не найдено" });
|
||||
}
|
||||
|
||||
res.json({ success: true, message: "Изображение удалено" });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
292
test_images.html
Normal file
292
test_images.html
Normal file
@ -0,0 +1,292 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Тест загрузки изображений</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.test-section {
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.success {
|
||||
color: green;
|
||||
}
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
.info {
|
||||
color: blue;
|
||||
}
|
||||
button {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
}
|
||||
button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
input[type="file"] {
|
||||
margin: 10px 0;
|
||||
}
|
||||
.image-preview {
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
margin: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Тест функциональности загрузки изображений</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>1. Регистрация тестового пользователя</h2>
|
||||
<button onclick="registerTestUser()">Зарегистрировать тестового пользователя</button>
|
||||
<div id="registerResult"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>2. Вход в систему</h2>
|
||||
<button onclick="loginTestUser()">Войти как тестовый пользователь</button>
|
||||
<div id="loginResult"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>3. Создание заметки с изображениями</h2>
|
||||
<input type="file" id="imageInput" accept="image/*" multiple>
|
||||
<button onclick="createNoteWithImages()">Создать заметку с изображениями</button>
|
||||
<div id="noteResult"></div>
|
||||
<div id="imagePreviews"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>4. Получение изображений заметки</h2>
|
||||
<input type="number" id="noteIdInput" placeholder="ID заметки">
|
||||
<button onclick="getNoteImages()">Получить изображения</button>
|
||||
<div id="getImagesResult"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>5. Удаление изображения</h2>
|
||||
<input type="number" id="deleteNoteIdInput" placeholder="ID заметки">
|
||||
<input type="number" id="deleteImageIdInput" placeholder="ID изображения">
|
||||
<button onclick="deleteImage()">Удалить изображение</button>
|
||||
<div id="deleteResult"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let authToken = null;
|
||||
let selectedFiles = [];
|
||||
|
||||
// Обработчик выбора файлов
|
||||
document.getElementById('imageInput').addEventListener('change', function(e) {
|
||||
selectedFiles = Array.from(e.target.files);
|
||||
displayImagePreviews();
|
||||
});
|
||||
|
||||
function displayImagePreviews() {
|
||||
const container = document.getElementById('imagePreviews');
|
||||
container.innerHTML = '';
|
||||
|
||||
selectedFiles.forEach((file, index) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
const img = document.createElement('img');
|
||||
img.src = e.target.result;
|
||||
img.className = 'image-preview';
|
||||
img.title = file.name;
|
||||
container.appendChild(img);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
async function registerTestUser() {
|
||||
try {
|
||||
const response = await fetch('/api/register', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: 'testuser',
|
||||
password: 'testpass123',
|
||||
confirmPassword: 'testpass123'
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
const resultDiv = document.getElementById('registerResult');
|
||||
|
||||
if (response.ok) {
|
||||
resultDiv.innerHTML = '<div class="success">✓ Пользователь успешно зарегистрирован</div>';
|
||||
} else {
|
||||
resultDiv.innerHTML = `<div class="error">✗ Ошибка: ${result.error}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('registerResult').innerHTML = `<div class="error">✗ Ошибка: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loginTestUser() {
|
||||
try {
|
||||
const response = await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: 'testuser',
|
||||
password: 'testpass123'
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
const resultDiv = document.getElementById('loginResult');
|
||||
|
||||
if (response.ok) {
|
||||
resultDiv.innerHTML = '<div class="success">✓ Успешный вход в систему</div>';
|
||||
authToken = 'authenticated'; // В реальном приложении здесь был бы JWT токен
|
||||
} else {
|
||||
resultDiv.innerHTML = `<div class="error">✗ Ошибка: ${result.error}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('loginResult').innerHTML = `<div class="error">✗ Ошибка: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function createNoteWithImages() {
|
||||
if (!authToken) {
|
||||
document.getElementById('noteResult').innerHTML = '<div class="error">✗ Сначала войдите в систему</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Сначала создаем заметку
|
||||
const noteResponse = await fetch('/api/notes', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: 'Тестовая заметка с изображениями',
|
||||
date: new Date().toLocaleDateString('ru-RU'),
|
||||
time: new Date().toLocaleTimeString('ru-RU', {hour: '2-digit', minute: '2-digit'})
|
||||
})
|
||||
});
|
||||
|
||||
const noteResult = await noteResponse.json();
|
||||
|
||||
if (!noteResponse.ok) {
|
||||
throw new Error(noteResult.error);
|
||||
}
|
||||
|
||||
const noteId = noteResult.id;
|
||||
document.getElementById('noteResult').innerHTML = `<div class="success">✓ Заметка создана с ID: ${noteId}</div>`;
|
||||
|
||||
// Теперь загружаем изображения
|
||||
if (selectedFiles.length > 0) {
|
||||
const formData = new FormData();
|
||||
selectedFiles.forEach(file => {
|
||||
formData.append('images', file);
|
||||
});
|
||||
|
||||
const imageResponse = await fetch(`/api/notes/${noteId}/images`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const imageResult = await imageResponse.json();
|
||||
|
||||
if (imageResponse.ok) {
|
||||
document.getElementById('noteResult').innerHTML += `<div class="success">✓ Загружено ${imageResult.images.length} изображений</div>`;
|
||||
document.getElementById('noteIdInput').value = noteId;
|
||||
} else {
|
||||
document.getElementById('noteResult').innerHTML += `<div class="error">✗ Ошибка загрузки изображений: ${imageResult.error}</div>`;
|
||||
}
|
||||
} else {
|
||||
document.getElementById('noteResult').innerHTML += '<div class="info">ℹ Изображения не выбраны</div>';
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('noteResult').innerHTML = `<div class="error">✗ Ошибка: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function getNoteImages() {
|
||||
const noteId = document.getElementById('noteIdInput').value;
|
||||
if (!noteId) {
|
||||
document.getElementById('getImagesResult').innerHTML = '<div class="error">✗ Введите ID заметки</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/notes/${noteId}/images`);
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
const resultDiv = document.getElementById('getImagesResult');
|
||||
resultDiv.innerHTML = `<div class="success">✓ Найдено ${result.length} изображений:</div>`;
|
||||
|
||||
result.forEach(image => {
|
||||
resultDiv.innerHTML += `
|
||||
<div style="margin: 10px 0; padding: 10px; border: 1px solid #ddd; border-radius: 4px;">
|
||||
<img src="${image.file_path}" style="max-width: 100px; max-height: 100px; margin-right: 10px;">
|
||||
<div>
|
||||
<strong>ID:</strong> ${image.id}<br>
|
||||
<strong>Имя файла:</strong> ${image.original_name}<br>
|
||||
<strong>Размер:</strong> ${(image.file_size / 1024).toFixed(2)} KB<br>
|
||||
<strong>Тип:</strong> ${image.mime_type}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
document.getElementById('getImagesResult').innerHTML = `<div class="error">✗ Ошибка: ${result.error}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('getImagesResult').innerHTML = `<div class="error">✗ Ошибка: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteImage() {
|
||||
const noteId = document.getElementById('deleteNoteIdInput').value;
|
||||
const imageId = document.getElementById('deleteImageIdInput').value;
|
||||
|
||||
if (!noteId || !imageId) {
|
||||
document.getElementById('deleteResult').innerHTML = '<div class="error">✗ Введите ID заметки и ID изображения</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/notes/${noteId}/images/${imageId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
document.getElementById('deleteResult').innerHTML = '<div class="success">✓ Изображение успешно удалено</div>';
|
||||
} else {
|
||||
document.getElementById('deleteResult').innerHTML = `<div class="error">✗ Ошибка: ${result.error}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('deleteResult').innerHTML = `<div class="error">✗ Ошибка: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user