Compare commits

...

3 Commits

Author SHA1 Message Date
f8692177f9 🎨 Обновлены стили изображений в заметках
- Изменены размеры изображений в заметках на 150x150 пикселей с использованием свойства object-fit для обрезки
- Добавлен эффект тени при наведении на изображения
- Реализован элемент с увеличительным стеклом, который появляется при наведении на изображение для улучшения взаимодействия
2025-10-19 23:34:50 +07:00
62d9b6c7ce Добавлена поддержка загрузки и управления изображениями для заметок
- Реализована возможность загрузки изображений к заметкам с использованием multer
- Добавлены API для загрузки, получения и удаления изображений заметок
- Обновлен интерфейс для отображения загруженных изображений и их предварительного просмотра
- Добавлены стили для управления изображениями и модального окна просмотра
2025-10-19 23:27:57 +07:00
bb5c3fede7 Улучшено редактирование заметок
- Добавлено разворачивание заметки при редактировании для улучшения пользовательского опыта
- Скрыта кнопка "Показать полностью" во время редактирования, если она присутствует
2025-10-19 23:04:56 +07:00
5 changed files with 1106 additions and 48 deletions

View File

@ -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,
"&quot;"
)}">${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("Ошибка удаления заметки");
@ -534,6 +719,15 @@ function addNoteEventListeners() {
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");
@ -608,7 +802,7 @@ function addNoteEventListeners() {
}
// Перезагружаем заметки
loadNotes();
await loadNotes();
} catch (error) {
console.error("Ошибка:", error);
alert("Ошибка сохранения заметки");
@ -653,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();
@ -667,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();
@ -686,7 +933,7 @@ async function saveNote() {
"Content-Type": "application/json",
},
body: JSON.stringify({
content: noteInput.value,
content: noteInput.value || " ", // Минимальный контент, если только изображения
date: date,
time: time,
}),
@ -696,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("Ошибка сохранения заметки");
@ -890,7 +1148,7 @@ function renderCalendar() {
}
// Добавляем обработчик клика
dayDiv.addEventListener("click", handleDayClick);
dayDiv.addEventListener("click", async (event) => await handleDayClick(event));
calendarDays.appendChild(dayDiv);
}
@ -927,7 +1185,7 @@ function renderCalendar() {
}
// Добавляем обработчик клика
dayDiv.addEventListener("click", handleDayClick);
dayDiv.addEventListener("click", async (event) => await handleDayClick(event));
calendarDays.appendChild(dayDiv);
}
@ -959,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;
// Если кликнули на тот же день, снимаем фильтр
@ -977,7 +1235,7 @@ function handleDayClick(event) {
}
// Перерисовываем заметки, календарь и теги
renderNotes(allNotes);
await renderNotes(allNotes);
renderCalendar();
renderTags();
updateFilterIndicator();
@ -1019,7 +1277,7 @@ function updateFilterIndicator() {
}
// Функция для сброса фильтра (глобальная)
window.clearFilter = function () {
window.clearFilter = async function () {
selectedDateFilter = null;
selectedTagFilter = null;
searchQuery = "";
@ -1037,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");
@ -1097,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();
}
});
@ -1197,7 +1458,7 @@ function renderCalendarMobile() {
}
// Добавляем обработчик клика
dayDiv.addEventListener("click", handleDayClickMobile);
dayDiv.addEventListener("click", async (event) => await handleDayClickMobile(event));
calendarDays.appendChild(dayDiv);
}
@ -1234,7 +1495,7 @@ function renderCalendarMobile() {
}
// Добавляем обработчик клика
dayDiv.addEventListener("click", handleDayClickMobile);
dayDiv.addEventListener("click", async (event) => await handleDayClickMobile(event));
calendarDays.appendChild(dayDiv);
}
@ -1266,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;
// Если кликнули на тот же день, снимаем фильтр
@ -1284,7 +1545,7 @@ function handleDayClickMobile(event) {
}
// Перерисовываем заметки, оба календаря и теги
renderNotes(allNotes);
await renderNotes(allNotes);
renderCalendar();
renderCalendarMobile();
renderTags();
@ -1316,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;
// Если кликнули на тот же тег, снимаем фильтр
@ -1332,7 +1593,7 @@ function handleTagClickMobile(event) {
}
// Перерисовываем заметки, теги и оба календаря
renderNotes(allNotes);
await renderNotes(allNotes);
renderTags();
renderTagsMobile();
renderCalendar();
@ -1443,7 +1704,7 @@ function initSearchMobile() {
});
// Обработчик клика на кнопку очистки поиска
clearSearchBtn.addEventListener("click", function () {
clearSearchBtn.addEventListener("click", async function () {
searchInput.value = "";
this.style.display = "none";
searchQuery = "";
@ -1457,7 +1718,7 @@ function initSearchMobile() {
if (mainClearSearchBtn) {
mainClearSearchBtn.style.display = "none";
}
renderNotes(allNotes);
await renderNotes(allNotes);
updateFilterIndicator();
});

View File

@ -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">&times;</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>

View File

@ -1298,3 +1298,225 @@ 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 {
width: 150px;
height: 150px;
object-fit: cover;
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);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
.note-image::after {
content: "🔍";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0,0,0,0.7);
color: white;
padding: 8px;
border-radius: 50%;
font-size: 16px;
opacity: 0;
transition: opacity 0.2s ease;
pointer-events: none;
}
.note-image:hover::after {
opacity: 1;
}
.note-images-container {
margin: 10px 0;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.note-image-item {
position: relative;
display: inline-block;
}
.note-image-item .note-image {
position: relative;
}
.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;
}
.note-image {
width: 120px;
height: 120px;
}
}

277
server.js
View File

@ -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
View 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>