✨ Добавлена поддержка загрузки и управления файлами в заметках
- Реализована возможность загрузки файлов (PDF, DOC, XLS и др.) к заметкам с помощью нового API. - Добавлены функции для получения, удаления и отображения прикрепленных файлов в интерфейсе заметок. - Обновлены стили и обработчики событий для работы с файлами в режиме редактирования и создания заметок. - Улучшена логика обработки файлов, включая проверку на дубликаты и ограничения по размеру.
This commit is contained in:
parent
372cea2e92
commit
49834d4ef4
545
public/app.js
545
public/app.js
@ -318,6 +318,13 @@ const imagePreviewContainer = document.getElementById("imagePreviewContainer");
|
|||||||
const imagePreviewList = document.getElementById("imagePreviewList");
|
const imagePreviewList = document.getElementById("imagePreviewList");
|
||||||
const clearImagesBtn = document.getElementById("clearImagesBtn");
|
const clearImagesBtn = document.getElementById("clearImagesBtn");
|
||||||
|
|
||||||
|
// Элементы для загрузки файлов
|
||||||
|
const fileBtn = document.getElementById("fileBtn");
|
||||||
|
const fileInput = document.getElementById("fileInput");
|
||||||
|
const filePreviewContainer = document.getElementById("filePreviewContainer");
|
||||||
|
const filePreviewList = document.getElementById("filePreviewList");
|
||||||
|
const clearFilesBtn = document.getElementById("clearFilesBtn");
|
||||||
|
|
||||||
// Элементы для предпросмотра заметки
|
// Элементы для предпросмотра заметки
|
||||||
const notePreviewContainer = document.getElementById("notePreviewContainer");
|
const notePreviewContainer = document.getElementById("notePreviewContainer");
|
||||||
const notePreviewContent = document.getElementById("notePreviewContent");
|
const notePreviewContent = document.getElementById("notePreviewContent");
|
||||||
@ -329,6 +336,7 @@ const modalClose = document.querySelector(".image-modal-close");
|
|||||||
|
|
||||||
// Массив для хранения выбранных изображений
|
// Массив для хранения выбранных изображений
|
||||||
let selectedImages = [];
|
let selectedImages = [];
|
||||||
|
let selectedFiles = [];
|
||||||
|
|
||||||
// Флаг режима предпросмотра
|
// Флаг режима предпросмотра
|
||||||
let isPreviewMode = false;
|
let isPreviewMode = false;
|
||||||
@ -1156,6 +1164,19 @@ imageBtn.addEventListener("touchend", function (event) {
|
|||||||
imageInput.click();
|
imageInput.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Обработчик для кнопки прикрепления файлов
|
||||||
|
fileBtn.addEventListener("click", function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
fileInput.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
fileBtn.addEventListener("touchend", function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
fileInput.click();
|
||||||
|
});
|
||||||
|
|
||||||
// Обработчик для кнопки предпросмотра
|
// Обработчик для кнопки предпросмотра
|
||||||
previewBtn.addEventListener("click", function () {
|
previewBtn.addEventListener("click", function () {
|
||||||
togglePreview();
|
togglePreview();
|
||||||
@ -1399,6 +1420,48 @@ clearImagesBtn.addEventListener("click", function () {
|
|||||||
imageInput.value = "";
|
imageInput.value = "";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Обработчик для загрузки файлов
|
||||||
|
fileInput.addEventListener("change", function (event) {
|
||||||
|
const files = Array.from(event.target.files);
|
||||||
|
let addedCount = 0;
|
||||||
|
|
||||||
|
files.forEach((file) => {
|
||||||
|
// Проверяем размер файла (максимум 50MB)
|
||||||
|
if (file.size > 50 * 1024 * 1024) {
|
||||||
|
showNotification(
|
||||||
|
`Файл "${file.name}" слишком большой. Максимальный размер: 50MB`,
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, не добавлен ли уже файл с таким именем и размером
|
||||||
|
const isDuplicate = selectedFiles.some(
|
||||||
|
(existingFile) =>
|
||||||
|
existingFile.name === file.name && existingFile.size === file.size
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isDuplicate) {
|
||||||
|
selectedFiles.push(file);
|
||||||
|
addedCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (addedCount > 0) {
|
||||||
|
updateFilePreview();
|
||||||
|
showNotification(`Добавлено файлов: ${addedCount}`, "success");
|
||||||
|
}
|
||||||
|
|
||||||
|
fileInput.value = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработчик очистки всех файлов
|
||||||
|
clearFilesBtn.addEventListener("click", function () {
|
||||||
|
selectedFiles = [];
|
||||||
|
updateFilePreview();
|
||||||
|
fileInput.value = "";
|
||||||
|
});
|
||||||
|
|
||||||
// Обработчики модального окна
|
// Обработчики модального окна
|
||||||
modalClose.addEventListener("click", function () {
|
modalClose.addEventListener("click", function () {
|
||||||
imageModal.style.display = "none";
|
imageModal.style.display = "none";
|
||||||
@ -1473,6 +1536,65 @@ function updateImagePreview() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Функция для обновления превью файлов
|
||||||
|
function updateFilePreview() {
|
||||||
|
if (selectedFiles.length === 0) {
|
||||||
|
filePreviewContainer.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
filePreviewContainer.style.display = "block";
|
||||||
|
filePreviewList.innerHTML = "";
|
||||||
|
|
||||||
|
selectedFiles.forEach((file, index) => {
|
||||||
|
const previewItem = document.createElement("div");
|
||||||
|
previewItem.className = "file-preview-item";
|
||||||
|
|
||||||
|
// Форматируем размер файла
|
||||||
|
const fileSize = (file.size / 1024 / 1024).toFixed(2);
|
||||||
|
const fileName =
|
||||||
|
file.name.length > 30 ? file.name.substring(0, 30) + "..." : file.name;
|
||||||
|
|
||||||
|
// Определяем иконку по расширению файла
|
||||||
|
const ext = file.name.split(".").pop().toLowerCase();
|
||||||
|
let icon = "mdi:file";
|
||||||
|
if (ext === "pdf") icon = "mdi:file-pdf";
|
||||||
|
else if (["doc", "docx"].includes(ext)) icon = "mdi:file-word";
|
||||||
|
else if (["xls", "xlsx"].includes(ext)) icon = "mdi:file-excel";
|
||||||
|
else if (ext === "txt") icon = "mdi:file-document";
|
||||||
|
else if (["zip", "rar", "7z"].includes(ext)) icon = "mdi:folder-zip";
|
||||||
|
|
||||||
|
previewItem.innerHTML = `
|
||||||
|
<div class="file-icon">
|
||||||
|
<span class="iconify" data-icon="${icon}"></span>
|
||||||
|
</div>
|
||||||
|
<div class="file-info">
|
||||||
|
<div class="file-name">${fileName}</div>
|
||||||
|
<div class="file-size">${fileSize} MB</div>
|
||||||
|
</div>
|
||||||
|
<button class="remove-file-btn" data-index="${index}" title="Удалить файл">×</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
filePreviewList.appendChild(previewItem);
|
||||||
|
|
||||||
|
// Обработчик удаления файла
|
||||||
|
const removeBtn = previewItem.querySelector(".remove-file-btn");
|
||||||
|
removeBtn.addEventListener("click", function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
selectedFiles.splice(index, 1);
|
||||||
|
updateFilePreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
removeBtn.addEventListener("touchend", function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
selectedFiles.splice(index, 1);
|
||||||
|
updateFilePreview();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Функция для отображения изображения в модальном окне
|
// Функция для отображения изображения в модальном окне
|
||||||
function showImageModal(imageSrc) {
|
function showImageModal(imageSrc) {
|
||||||
console.log("showImageModal called with:", imageSrc);
|
console.log("showImageModal called with:", imageSrc);
|
||||||
@ -1576,6 +1698,70 @@ async function getNoteImages(noteId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Функция для загрузки файлов на сервер
|
||||||
|
async function uploadFiles(noteId) {
|
||||||
|
if (selectedFiles.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
selectedFiles.forEach((file) => {
|
||||||
|
formData.append("files", file);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const isMobile =
|
||||||
|
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||||
|
navigator.userAgent
|
||||||
|
) ||
|
||||||
|
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2) ||
|
||||||
|
window.matchMedia("(max-width: 768px)").matches;
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
const loadingDiv = document.createElement("div");
|
||||||
|
loadingDiv.id = "mobile-file-upload-loading";
|
||||||
|
loadingDiv.style.cssText = `
|
||||||
|
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
||||||
|
background: rgba(0, 0, 0, 0.8); color: white; padding: 20px;
|
||||||
|
border-radius: 10px; z-index: 10000; font-size: 16px; text-align: center;
|
||||||
|
`;
|
||||||
|
loadingDiv.innerHTML = `
|
||||||
|
<div>📎 Загрузка файлов...</div>
|
||||||
|
<div style="font-size: 12px; margin-top: 10px;">${selectedFiles.length} файл(ов)</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(loadingDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`/api/notes/${noteId}/files`, {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Ошибка загрузки файлов");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
const loadingDiv = document.getElementById("mobile-file-upload-loading");
|
||||||
|
if (loadingDiv) {
|
||||||
|
loadingDiv.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.files || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Ошибка загрузки файлов:", error);
|
||||||
|
|
||||||
|
const loadingDiv = document.getElementById("mobile-file-upload-loading");
|
||||||
|
if (loadingDiv) {
|
||||||
|
loadingDiv.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification(`Ошибка загрузки файлов: ${error.message}`, "error");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Функция для удаления изображения заметки
|
// Функция для удаления изображения заметки
|
||||||
async function deleteNoteImage(noteId, imageId) {
|
async function deleteNoteImage(noteId, imageId) {
|
||||||
try {
|
try {
|
||||||
@ -1594,6 +1780,24 @@ async function deleteNoteImage(noteId, imageId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Функция для удаления файла заметки
|
||||||
|
async function deleteNoteFile(noteId, fileId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/notes/${noteId}/files/${fileId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Ошибка удаления файла");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Ошибка удаления файла:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Функция для загрузки заметок с сервера
|
// Функция для загрузки заметок с сервера
|
||||||
async function loadNotes(forceReload = false) {
|
async function loadNotes(forceReload = false) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@ -1881,6 +2085,37 @@ async function renderNotes(notes) {
|
|||||||
imagesHtml += "</div>";
|
imagesHtml += "</div>";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Используем файлы, которые уже пришли с заметкой
|
||||||
|
const noteFiles = Array.isArray(note.files) ? note.files : [];
|
||||||
|
let filesHtml = "";
|
||||||
|
|
||||||
|
if (noteFiles.length > 0) {
|
||||||
|
filesHtml = '<div class="note-files-container">';
|
||||||
|
noteFiles.forEach((file) => {
|
||||||
|
const ext = file.original_name.split(".").pop().toLowerCase();
|
||||||
|
let icon = "mdi:file";
|
||||||
|
if (ext === "pdf") icon = "mdi:file-pdf";
|
||||||
|
else if (["doc", "docx"].includes(ext)) icon = "mdi:file-word";
|
||||||
|
else if (["xls", "xlsx"].includes(ext)) icon = "mdi:file-excel";
|
||||||
|
else if (ext === "txt") icon = "mdi:file-document";
|
||||||
|
else if (["zip", "rar", "7z"].includes(ext)) icon = "mdi:folder-zip";
|
||||||
|
|
||||||
|
const fileSize = (file.file_size / 1024 / 1024).toFixed(2);
|
||||||
|
filesHtml += `
|
||||||
|
<div class="note-file-item">
|
||||||
|
<a href="${file.file_path}" download="${file.original_name}" class="note-file-link" data-file-id="${file.id}">
|
||||||
|
<span class="iconify file-icon" data-icon="${icon}"></span>
|
||||||
|
<div class="file-info">
|
||||||
|
<div class="file-name">${file.original_name}</div>
|
||||||
|
<div class="file-size">${fileSize} MB</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
filesHtml += "</div>";
|
||||||
|
}
|
||||||
|
|
||||||
// Форматируем дату создания и изменения по локали устройства
|
// Форматируем дату создания и изменения по локали устройства
|
||||||
let dateDisplay;
|
let dateDisplay;
|
||||||
if (note.created_at) {
|
if (note.created_at) {
|
||||||
@ -1935,6 +2170,7 @@ async function renderNotes(notes) {
|
|||||||
"""
|
"""
|
||||||
)}">${parsedContent}</div>
|
)}">${parsedContent}</div>
|
||||||
${imagesHtml}
|
${imagesHtml}
|
||||||
|
${filesHtml}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
notesList.insertAdjacentHTML("beforeend", noteHtml);
|
notesList.insertAdjacentHTML("beforeend", noteHtml);
|
||||||
@ -2128,6 +2364,32 @@ function addNoteEventListeners() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Получаем файлы из контейнера заметки
|
||||||
|
const filesContainer = noteContainer.querySelector(
|
||||||
|
".note-files-container"
|
||||||
|
);
|
||||||
|
let noteFilesList = [];
|
||||||
|
if (filesContainer) {
|
||||||
|
const fileElements = filesContainer.querySelectorAll(".note-file-item");
|
||||||
|
fileElements.forEach((fileElement) => {
|
||||||
|
const link = fileElement.querySelector(".note-file-link");
|
||||||
|
if (link) {
|
||||||
|
const fileId = link.dataset.fileId || null;
|
||||||
|
const fileName =
|
||||||
|
link.querySelector(".file-name")?.textContent || "";
|
||||||
|
const fileSize =
|
||||||
|
link.querySelector(".file-size")?.textContent || "";
|
||||||
|
const filePath = link.getAttribute("href") || "";
|
||||||
|
noteFilesList.push({
|
||||||
|
id: fileId,
|
||||||
|
name: fileName,
|
||||||
|
size: fileSize,
|
||||||
|
path: filePath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Разворачиваем заметку при редактировании
|
// Разворачиваем заметку при редактировании
|
||||||
noteContent.classList.remove("collapsed");
|
noteContent.classList.remove("collapsed");
|
||||||
|
|
||||||
@ -2142,6 +2404,11 @@ function addNoteEventListeners() {
|
|||||||
imagesContainer.style.display = "none";
|
imagesContainer.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Скрываем контейнер с файлами заметки при редактировании
|
||||||
|
if (filesContainer) {
|
||||||
|
filesContainer.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
// Создаем контейнер для markdown кнопок
|
// Создаем контейнер для markdown кнопок
|
||||||
const markdownButtonsContainer = document.createElement("div");
|
const markdownButtonsContainer = document.createElement("div");
|
||||||
markdownButtonsContainer.classList.add(
|
markdownButtonsContainer.classList.add(
|
||||||
@ -2176,6 +2443,7 @@ function addNoteEventListeners() {
|
|||||||
tag: "- [ ] ",
|
tag: "- [ ] ",
|
||||||
},
|
},
|
||||||
{ id: "editImageBtn", icon: "mdi:image-plus", tag: "image" },
|
{ id: "editImageBtn", icon: "mdi:image-plus", tag: "image" },
|
||||||
|
{ id: "editFileBtn", icon: "mdi:file-plus", tag: "file" },
|
||||||
{ id: "editPreviewBtn", icon: "mdi:eye", tag: "preview" },
|
{ id: "editPreviewBtn", icon: "mdi:eye", tag: "preview" },
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -2384,6 +2652,14 @@ function addNoteEventListeners() {
|
|||||||
editImageInput.style.display = "none";
|
editImageInput.style.display = "none";
|
||||||
editImageInput.id = `editImageInput-${noteId}`;
|
editImageInput.id = `editImageInput-${noteId}`;
|
||||||
|
|
||||||
|
// Создаем элементы для загрузки файлов в режиме редактирования
|
||||||
|
const editFileInput = document.createElement("input");
|
||||||
|
editFileInput.type = "file";
|
||||||
|
editFileInput.accept = ".pdf,.doc,.docx,.xls,.xlsx,.txt,.zip,.rar,.7z";
|
||||||
|
editFileInput.multiple = true;
|
||||||
|
editFileInput.style.display = "none";
|
||||||
|
editFileInput.id = `editFileInput-${noteId}`;
|
||||||
|
|
||||||
// Контейнер для существующих изображений
|
// Контейнер для существующих изображений
|
||||||
const existingImagesContainer = document.createElement("div");
|
const existingImagesContainer = document.createElement("div");
|
||||||
existingImagesContainer.id = `existingImagesContainer-${noteId}`;
|
existingImagesContainer.id = `existingImagesContainer-${noteId}`;
|
||||||
@ -2466,6 +2742,98 @@ function addNoteEventListeners() {
|
|||||||
// Массив для хранения новых изображений в режиме редактирования
|
// Массив для хранения новых изображений в режиме редактирования
|
||||||
const editSelectedImages = [];
|
const editSelectedImages = [];
|
||||||
|
|
||||||
|
// Контейнер для существующих файлов
|
||||||
|
const existingFilesContainer = document.createElement("div");
|
||||||
|
existingFilesContainer.id = `existingFilesContainer-${noteId}`;
|
||||||
|
existingFilesContainer.classList.add("file-preview-container");
|
||||||
|
existingFilesContainer.style.display =
|
||||||
|
noteFilesList.length > 0 ? "block" : "none";
|
||||||
|
|
||||||
|
const existingFilesHeader = document.createElement("div");
|
||||||
|
existingFilesHeader.classList.add("file-preview-header");
|
||||||
|
existingFilesHeader.innerHTML = `<span>Прикрепленные файлы:</span>`;
|
||||||
|
|
||||||
|
const existingFilesList = document.createElement("div");
|
||||||
|
existingFilesList.id = `existingFilesList-${noteId}`;
|
||||||
|
existingFilesList.classList.add("file-preview-list");
|
||||||
|
|
||||||
|
existingFilesContainer.appendChild(existingFilesHeader);
|
||||||
|
existingFilesContainer.appendChild(existingFilesList);
|
||||||
|
|
||||||
|
// Массив для отслеживания удаленных файлов
|
||||||
|
const deletedFilesIds = [];
|
||||||
|
|
||||||
|
// Отображаем существующие файлы
|
||||||
|
if (noteFilesList.length > 0) {
|
||||||
|
noteFilesList.forEach((file) => {
|
||||||
|
const previewItem = document.createElement("div");
|
||||||
|
previewItem.className = "file-preview-item";
|
||||||
|
previewItem.dataset.fileId = file.id;
|
||||||
|
|
||||||
|
const ext = file.name.split(".").pop().toLowerCase();
|
||||||
|
let icon = "mdi:file";
|
||||||
|
if (ext === "pdf") icon = "mdi:file-pdf-box";
|
||||||
|
else if (["doc", "docx"].includes(ext)) icon = "mdi:file-word-box";
|
||||||
|
else if (["xls", "xlsx"].includes(ext)) icon = "mdi:file-excel-box";
|
||||||
|
else if (ext === "txt") icon = "mdi:file-document-outline";
|
||||||
|
else if (["zip", "rar", "7z"].includes(ext)) icon = "mdi:folder-zip";
|
||||||
|
|
||||||
|
const fileIcon = document.createElement("span");
|
||||||
|
fileIcon.className = "iconify file-icon";
|
||||||
|
fileIcon.setAttribute("data-icon", icon);
|
||||||
|
|
||||||
|
const fileInfo = document.createElement("div");
|
||||||
|
fileInfo.className = "file-info-edit";
|
||||||
|
fileInfo.innerHTML = `
|
||||||
|
<div class="file-name">${file.name}</div>
|
||||||
|
<div class="file-size">${file.size}</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const removeBtn = document.createElement("button");
|
||||||
|
removeBtn.className = "remove-file-btn";
|
||||||
|
removeBtn.textContent = "×";
|
||||||
|
removeBtn.title = "Удалить файл";
|
||||||
|
|
||||||
|
// Обработчик удаления существующего файла
|
||||||
|
removeBtn.addEventListener("click", function () {
|
||||||
|
deletedFilesIds.push(file.id);
|
||||||
|
previewItem.remove();
|
||||||
|
// Если больше нет файлов, скрываем контейнер
|
||||||
|
if (existingFilesList.children.length === 0) {
|
||||||
|
existingFilesContainer.style.display = "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
previewItem.appendChild(fileIcon);
|
||||||
|
previewItem.appendChild(fileInfo);
|
||||||
|
previewItem.appendChild(removeBtn);
|
||||||
|
|
||||||
|
existingFilesList.appendChild(previewItem);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const editFilePreviewContainer = document.createElement("div");
|
||||||
|
editFilePreviewContainer.id = `editFilePreviewContainer-${noteId}`;
|
||||||
|
editFilePreviewContainer.classList.add("file-preview-container");
|
||||||
|
editFilePreviewContainer.style.display = "none";
|
||||||
|
|
||||||
|
const editFilePreviewHeader = document.createElement("div");
|
||||||
|
editFilePreviewHeader.classList.add("file-preview-header");
|
||||||
|
editFilePreviewHeader.innerHTML = `
|
||||||
|
<span>Новые файлы:</span>
|
||||||
|
<button type="button" class="clear-files-btn" id="editClearFilesBtn-${noteId}">Очистить все</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const editFilePreviewList = document.createElement("div");
|
||||||
|
editFilePreviewList.id = `editFilePreviewList-${noteId}`;
|
||||||
|
editFilePreviewList.classList.add("file-preview-list");
|
||||||
|
|
||||||
|
editFilePreviewContainer.appendChild(editFilePreviewHeader);
|
||||||
|
editFilePreviewContainer.appendChild(editFilePreviewList);
|
||||||
|
|
||||||
|
// Массив для хранения новых файлов в режиме редактирования
|
||||||
|
const editSelectedFiles = [];
|
||||||
|
|
||||||
// Контейнер для кнопки сохранения и подсказки
|
// Контейнер для кнопки сохранения и подсказки
|
||||||
const saveButtonContainer = document.createElement("div");
|
const saveButtonContainer = document.createElement("div");
|
||||||
saveButtonContainer.classList.add("save-button-container");
|
saveButtonContainer.classList.add("save-button-container");
|
||||||
@ -2545,6 +2913,50 @@ function addNoteEventListeners() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Функция обновления превью файлов для режима редактирования
|
||||||
|
const updateEditFilePreview = function () {
|
||||||
|
if (editSelectedFiles.length === 0) {
|
||||||
|
editFilePreviewContainer.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
editFilePreviewContainer.style.display = "block";
|
||||||
|
editFilePreviewList.innerHTML = "";
|
||||||
|
|
||||||
|
editSelectedFiles.forEach((file, index) => {
|
||||||
|
const previewItem = document.createElement("div");
|
||||||
|
previewItem.className = "file-preview-item";
|
||||||
|
|
||||||
|
const ext = file.name.split(".").pop().toLowerCase();
|
||||||
|
let icon = "mdi:file";
|
||||||
|
if (ext === "pdf") icon = "mdi:file-pdf-box";
|
||||||
|
else if (["doc", "docx"].includes(ext)) icon = "mdi:file-word-box";
|
||||||
|
else if (["xls", "xlsx"].includes(ext)) icon = "mdi:file-excel-box";
|
||||||
|
else if (ext === "txt") icon = "mdi:file-document-outline";
|
||||||
|
else if (["zip", "rar", "7z"].includes(ext)) icon = "mdi:folder-zip";
|
||||||
|
|
||||||
|
const fileSize = (file.size / 1024 / 1024).toFixed(2);
|
||||||
|
|
||||||
|
previewItem.innerHTML = `
|
||||||
|
<span class="iconify file-icon" data-icon="${icon}"></span>
|
||||||
|
<div class="file-info-edit">
|
||||||
|
<div class="file-name">${file.name}</div>
|
||||||
|
<div class="file-size">${fileSize} MB</div>
|
||||||
|
</div>
|
||||||
|
<button class="remove-file-btn" data-index="${index}">×</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
editFilePreviewList.appendChild(previewItem);
|
||||||
|
|
||||||
|
// Обработчик удаления файла
|
||||||
|
const removeBtn = previewItem.querySelector(".remove-file-btn");
|
||||||
|
removeBtn.addEventListener("click", function () {
|
||||||
|
editSelectedFiles.splice(index, 1);
|
||||||
|
updateEditFilePreview();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Функция загрузки изображений для режима редактирования
|
// Функция загрузки изображений для режима редактирования
|
||||||
const uploadEditImages = async function (noteId) {
|
const uploadEditImages = async function (noteId) {
|
||||||
if (editSelectedImages.length === 0) {
|
if (editSelectedImages.length === 0) {
|
||||||
@ -2574,12 +2986,43 @@ function addNoteEventListeners() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Функция загрузки файлов для режима редактирования
|
||||||
|
const uploadEditFiles = async function (noteId) {
|
||||||
|
if (editSelectedFiles.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
editSelectedFiles.forEach((file) => {
|
||||||
|
formData.append("files", file);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/notes/${noteId}/files`, {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Ошибка загрузки файлов");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
return result.files || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Ошибка загрузки файлов:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Функция сохранения для редактирования
|
// Функция сохранения для редактирования
|
||||||
const saveEditNote = async function () {
|
const saveEditNote = async function () {
|
||||||
if (
|
if (
|
||||||
textarea.value.trim() !== "" ||
|
textarea.value.trim() !== "" ||
|
||||||
editSelectedImages.length > 0 ||
|
editSelectedImages.length > 0 ||
|
||||||
deletedImagesIds.length > 0
|
deletedImagesIds.length > 0 ||
|
||||||
|
editSelectedFiles.length > 0 ||
|
||||||
|
deletedFilesIds.length > 0
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
// Сбрасываем режим предпросмотра перед сохранением
|
// Сбрасываем режим предпросмотра перед сохранением
|
||||||
@ -2595,7 +3038,7 @@ function addNoteEventListeners() {
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
content: textarea.value || " ", // Минимальный контент, если только изображения
|
content: textarea.value || " ", // Минимальный контент, если только изображения/файлы
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -2608,11 +3051,21 @@ function addNoteEventListeners() {
|
|||||||
await deleteNoteImage(noteId, imageId);
|
await deleteNoteImage(noteId, imageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Удаляем файлы, которые были помечены на удаление
|
||||||
|
for (const fileId of deletedFilesIds) {
|
||||||
|
await deleteNoteFile(noteId, fileId);
|
||||||
|
}
|
||||||
|
|
||||||
// Загружаем новые изображения, если они есть
|
// Загружаем новые изображения, если они есть
|
||||||
if (editSelectedImages.length > 0) {
|
if (editSelectedImages.length > 0) {
|
||||||
await uploadEditImages(noteId);
|
await uploadEditImages(noteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Загружаем новые файлы, если они есть
|
||||||
|
if (editSelectedFiles.length > 0) {
|
||||||
|
await uploadEditFiles(noteId);
|
||||||
|
}
|
||||||
|
|
||||||
// Перезагружаем заметки
|
// Перезагружаем заметки
|
||||||
await loadNotes(true);
|
await loadNotes(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -2630,8 +3083,9 @@ function addNoteEventListeners() {
|
|||||||
const originalMarkdown = noteContent.dataset.originalContent || "";
|
const originalMarkdown = noteContent.dataset.originalContent || "";
|
||||||
const hasTextChanges = textarea.value !== originalMarkdown;
|
const hasTextChanges = textarea.value !== originalMarkdown;
|
||||||
const hasNewImages = editSelectedImages.length > 0;
|
const hasNewImages = editSelectedImages.length > 0;
|
||||||
|
const hasNewFiles = editSelectedFiles.length > 0;
|
||||||
|
|
||||||
if (hasTextChanges || hasNewImages) {
|
if (hasTextChanges || hasNewImages || hasNewFiles) {
|
||||||
const ok = await showConfirmModal(
|
const ok = await showConfirmModal(
|
||||||
"Подтверждение отмены",
|
"Подтверждение отмены",
|
||||||
"Отменить изменения?",
|
"Отменить изменения?",
|
||||||
@ -2659,6 +3113,18 @@ function addNoteEventListeners() {
|
|||||||
editImageInput.value = "";
|
editImageInput.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Очистить выбранные новые файлы и превью
|
||||||
|
editSelectedFiles.length = 0;
|
||||||
|
if (editFilePreviewContainer) {
|
||||||
|
editFilePreviewContainer.style.display = "none";
|
||||||
|
}
|
||||||
|
if (editFilePreviewList) {
|
||||||
|
editFilePreviewList.innerHTML = "";
|
||||||
|
}
|
||||||
|
if (editFileInput) {
|
||||||
|
editFileInput.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
// Перерисовать заметки, вернув исходное состояние
|
// Перерисовать заметки, вернув исходное состояние
|
||||||
await loadNotes(true);
|
await loadNotes(true);
|
||||||
};
|
};
|
||||||
@ -2696,6 +3162,41 @@ function addNoteEventListeners() {
|
|||||||
editImageInput.value = "";
|
editImageInput.value = "";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Обработчики для загрузки файлов в режиме редактирования
|
||||||
|
editFileInput.addEventListener("change", function (event) {
|
||||||
|
const files = Array.from(event.target.files);
|
||||||
|
const allowedExtensions = /\.(pdf|doc|docx|xls|xlsx|txt|zip|rar|7z)$/i;
|
||||||
|
|
||||||
|
files.forEach((file) => {
|
||||||
|
if (allowedExtensions.test(file.name)) {
|
||||||
|
if (file.size <= 50 * 1024 * 1024) {
|
||||||
|
editSelectedFiles.push(file);
|
||||||
|
} else {
|
||||||
|
showNotification(
|
||||||
|
`Файл ${file.name} слишком большой (максимум 50 МБ)`,
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showNotification(
|
||||||
|
`Файл ${file.name} имеет недопустимый формат`,
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
updateEditFilePreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработчик очистки всех файлов в режиме редактирования
|
||||||
|
const editClearFilesBtn = editFilePreviewHeader.querySelector(
|
||||||
|
`#editClearFilesBtn-${noteId}`
|
||||||
|
);
|
||||||
|
editClearFilesBtn.addEventListener("click", function () {
|
||||||
|
editSelectedFiles.length = 0;
|
||||||
|
updateEditFilePreview();
|
||||||
|
editFileInput.value = "";
|
||||||
|
});
|
||||||
|
|
||||||
// Создаем контейнер для предпросмотра в режиме редактирования
|
// Создаем контейнер для предпросмотра в режиме редактирования
|
||||||
const editPreviewContainer = document.createElement("div");
|
const editPreviewContainer = document.createElement("div");
|
||||||
editPreviewContainer.classList.add("note-preview-container");
|
editPreviewContainer.classList.add("note-preview-container");
|
||||||
@ -2714,14 +3215,17 @@ function addNoteEventListeners() {
|
|||||||
// Флаг для режима предпросмотра редактирования
|
// Флаг для режима предпросмотра редактирования
|
||||||
let isEditPreviewMode = false;
|
let isEditPreviewMode = false;
|
||||||
|
|
||||||
// Очищаем текущий контент и вставляем markdown кнопки, textarea, элементы для изображений и контейнер с кнопкой сохранить
|
// Очищаем текущий контент и вставляем markdown кнопки, textarea, элементы для изображений и файлов и контейнер с кнопкой сохранить
|
||||||
noteContent.innerHTML = "";
|
noteContent.innerHTML = "";
|
||||||
noteContent.appendChild(markdownButtonsContainer);
|
noteContent.appendChild(markdownButtonsContainer);
|
||||||
noteContent.appendChild(textarea);
|
noteContent.appendChild(textarea);
|
||||||
noteContent.appendChild(editPreviewContainer);
|
noteContent.appendChild(editPreviewContainer);
|
||||||
noteContent.appendChild(editImageInput);
|
noteContent.appendChild(editImageInput);
|
||||||
|
noteContent.appendChild(editFileInput);
|
||||||
noteContent.appendChild(existingImagesContainer);
|
noteContent.appendChild(existingImagesContainer);
|
||||||
noteContent.appendChild(editImagePreviewContainer);
|
noteContent.appendChild(editImagePreviewContainer);
|
||||||
|
noteContent.appendChild(existingFilesContainer);
|
||||||
|
noteContent.appendChild(editFilePreviewContainer);
|
||||||
noteContent.appendChild(saveButtonContainer);
|
noteContent.appendChild(saveButtonContainer);
|
||||||
|
|
||||||
// Применяем авторасширение после добавления в DOM
|
// Применяем авторасширение после добавления в DOM
|
||||||
@ -2741,6 +3245,9 @@ function addNoteEventListeners() {
|
|||||||
if (button.tag === "image") {
|
if (button.tag === "image") {
|
||||||
// Для кнопки изображения открываем диалог выбора файлов
|
// Для кнопки изображения открываем диалог выбора файлов
|
||||||
editImageInput.click();
|
editImageInput.click();
|
||||||
|
} else if (button.tag === "file") {
|
||||||
|
// Для кнопки файлов открываем диалог выбора файлов
|
||||||
|
editFileInput.click();
|
||||||
} else if (button.tag === "color") {
|
} else if (button.tag === "color") {
|
||||||
// Для кнопки цвета открываем диалог выбора цвета
|
// Для кнопки цвета открываем диалог выбора цвета
|
||||||
insertColorTagForEdit(textarea);
|
insertColorTagForEdit(textarea);
|
||||||
@ -3099,7 +3606,11 @@ function addSpoilerEventListeners() {
|
|||||||
|
|
||||||
// Функция сохранения заметки (вынесена отдельно для повторного использования)
|
// Функция сохранения заметки (вынесена отдельно для повторного использования)
|
||||||
async function saveNote() {
|
async function saveNote() {
|
||||||
if (noteInput.value.trim() !== "" || selectedImages.length > 0) {
|
if (
|
||||||
|
noteInput.value.trim() !== "" ||
|
||||||
|
selectedImages.length > 0 ||
|
||||||
|
selectedFiles.length > 0
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const { date, time } = getFormattedDateTime();
|
const { date, time } = getFormattedDateTime();
|
||||||
|
|
||||||
@ -3128,11 +3639,21 @@ async function saveNote() {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
`;
|
`;
|
||||||
|
const attachmentsInfo = [];
|
||||||
|
if (selectedImages.length > 0) {
|
||||||
|
attachmentsInfo.push(`${selectedImages.length} изображений`);
|
||||||
|
}
|
||||||
|
if (selectedFiles.length > 0) {
|
||||||
|
attachmentsInfo.push(`${selectedFiles.length} файлов`);
|
||||||
|
}
|
||||||
|
|
||||||
savingIndicator.innerHTML = `
|
savingIndicator.innerHTML = `
|
||||||
<div>💾 Сохранение заметки...</div>
|
<div>💾 Сохранение заметки...</div>
|
||||||
${
|
${
|
||||||
selectedImages.length > 0
|
attachmentsInfo.length > 0
|
||||||
? `<div style="font-size: 12px; margin-top: 10px;">+ ${selectedImages.length} изображений</div>`
|
? `<div style="font-size: 12px; margin-top: 10px;">+ ${attachmentsInfo.join(
|
||||||
|
", "
|
||||||
|
)}</div>`
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -3163,17 +3684,25 @@ async function saveNote() {
|
|||||||
await uploadImages(noteId);
|
await uploadImages(noteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Загружаем файлы, если они есть
|
||||||
|
if (selectedFiles.length > 0) {
|
||||||
|
await uploadFiles(noteId);
|
||||||
|
}
|
||||||
|
|
||||||
// Удаляем индикатор сохранения
|
// Удаляем индикатор сохранения
|
||||||
if (savingIndicator) {
|
if (savingIndicator) {
|
||||||
savingIndicator.remove();
|
savingIndicator.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Очищаем поле ввода и изображения, перезагружаем заметки
|
// Очищаем поле ввода, изображения и файлы, перезагружаем заметки
|
||||||
noteInput.value = "";
|
noteInput.value = "";
|
||||||
noteInput.style.height = "auto";
|
noteInput.style.height = "auto";
|
||||||
selectedImages = [];
|
selectedImages = [];
|
||||||
|
selectedFiles = [];
|
||||||
updateImagePreview();
|
updateImagePreview();
|
||||||
|
updateFilePreview();
|
||||||
imageInput.value = "";
|
imageInput.value = "";
|
||||||
|
fileInput.value = "";
|
||||||
|
|
||||||
// Сбрасываем режим предпросмотра, если он был активен
|
// Сбрасываем режим предпросмотра, если он был активен
|
||||||
if (isPreviewMode) {
|
if (isPreviewMode) {
|
||||||
|
|||||||
@ -351,6 +351,9 @@
|
|||||||
>
|
>
|
||||||
<span class="iconify" data-icon="mdi:image-plus"></span>
|
<span class="iconify" data-icon="mdi:image-plus"></span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btnMarkdown" id="fileBtn" title="Прикрепить файлы">
|
||||||
|
<span class="iconify" data-icon="mdi:file-plus"></span>
|
||||||
|
</button>
|
||||||
<button class="btnMarkdown" id="previewBtn" title="Предпросмотр">
|
<button class="btnMarkdown" id="previewBtn" title="Предпросмотр">
|
||||||
<span class="iconify" data-icon="mdi:eye"></span>
|
<span class="iconify" data-icon="mdi:eye"></span>
|
||||||
</button>
|
</button>
|
||||||
@ -383,6 +386,15 @@
|
|||||||
style="display: none"
|
style="display: none"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Скрытый input для загрузки файлов -->
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="fileInput"
|
||||||
|
accept=".pdf,.doc,.docx,.xls,.xlsx,.txt,.zip,.rar,.7z"
|
||||||
|
multiple
|
||||||
|
style="display: none"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Контейнер для отображения загруженных изображений -->
|
<!-- Контейнер для отображения загруженных изображений -->
|
||||||
<div
|
<div
|
||||||
id="imagePreviewContainer"
|
id="imagePreviewContainer"
|
||||||
@ -402,6 +414,21 @@
|
|||||||
<div id="imagePreviewList" class="image-preview-list"></div>
|
<div id="imagePreviewList" class="image-preview-list"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Контейнер для отображения прикрепленных файлов -->
|
||||||
|
<div
|
||||||
|
id="filePreviewContainer"
|
||||||
|
class="file-preview-container"
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
|
<div class="file-preview-header">
|
||||||
|
<span>Прикрепленные файлы:</span>
|
||||||
|
<button type="button" id="clearFilesBtn" class="clear-files-btn">
|
||||||
|
Очистить все
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="filePreviewList" class="file-preview-list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="save-button-container">
|
<div class="save-button-container">
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<button
|
<button
|
||||||
|
|||||||
322
public/style.css
322
public/style.css
@ -2175,6 +2175,44 @@ textarea:focus {
|
|||||||
transition: background-color 0.3s ease, color 0.3s ease;
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Выравниваем размер контейнеров в режиме редактирования под размер контейнеров создания */
|
||||||
|
.textNote .markdown-buttons--edit,
|
||||||
|
.textNote .note-preview-container,
|
||||||
|
.textNote .image-preview-container,
|
||||||
|
.textNote .file-preview-container,
|
||||||
|
.textNote .textInput,
|
||||||
|
.textNote textarea.textInput {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стили для файлов в режиме редактирования - делаем такими же сжатыми как в создании */
|
||||||
|
.textNote .file-preview-container .file-preview-item {
|
||||||
|
padding: 6px 10px;
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textNote .file-preview-container .file-preview-item .file-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textNote .file-preview-container .file-preview-item .file-name {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textNote .file-preview-container .file-preview-item .file-size {
|
||||||
|
font-size: 11px;
|
||||||
|
margin-top: 1px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textNote .file-preview-container .file-preview-item .remove-file-btn {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.note-preview-content h1,
|
.note-preview-content h1,
|
||||||
.note-preview-content h2,
|
.note-preview-content h2,
|
||||||
.note-preview-content h3,
|
.note-preview-content h3,
|
||||||
@ -2576,6 +2614,64 @@ textarea:focus {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.note-files-container {
|
||||||
|
margin: 10px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-file-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-file-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 6px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-file-link:hover {
|
||||||
|
background: #e9ecef;
|
||||||
|
border-color: var(--accent-color, #007bff);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-file-link .file-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
color: var(--accent-color, #007bff);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-file-link .file-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-file-link .file-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-file-link .file-size {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.note-image-item .remove-note-image-btn {
|
.note-image-item .remove-note-image-btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 5px;
|
top: 5px;
|
||||||
@ -3372,3 +3468,229 @@ textarea:focus {
|
|||||||
background: linear-gradient(45deg, #d4edda, #c3e6cb);
|
background: linear-gradient(45deg, #d4edda, #c3e6cb);
|
||||||
box-shadow: 0 0 0 2px rgba(40, 167, 69, 0.35);
|
box-shadow: 0 0 0 2px rgba(40, 167, 69, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Стили для файлов */
|
||||||
|
.file-preview-container {
|
||||||
|
margin: 15px 0;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f0f8ff;
|
||||||
|
border: 2px dashed #b0d4f1;
|
||||||
|
border-radius: 8px;
|
||||||
|
touch-action: manipulation;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
.file-preview-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
.file-preview-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.file-preview-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
gap: 12px;
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
.file-preview-item .file-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
color: var(--accent-color, #007bff);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.file-preview-item .file-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.file-preview-item .file-info-edit {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.file-preview-item .file-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.file-preview-item .file-size {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 1px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.file-info-edit .file-name {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.file-info-edit .file-size {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 1px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.file-preview-item .remove-file-btn {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
.file-preview-item .remove-file-btn:hover {
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
|
.clear-files-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-files-btn:hover {
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Темная тема для файлов */
|
||||||
|
[data-theme="dark"] .file-preview-container {
|
||||||
|
background: #2a3a4a;
|
||||||
|
border-color: #4a5a6a;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .file-preview-header {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .file-preview-item {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-color: var(--border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .file-preview-item .file-name {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .file-preview-item .file-size {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 11px;
|
||||||
|
margin-top: 1px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .file-info-edit .file-name {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .file-info-edit .file-size {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 11px;
|
||||||
|
margin-top: 1px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Темная тема для файлов в режиме редактирования */
|
||||||
|
[data-theme="dark"]
|
||||||
|
.textNote
|
||||||
|
.file-preview-container
|
||||||
|
.file-preview-item
|
||||||
|
.file-name {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"]
|
||||||
|
.textNote
|
||||||
|
.file-preview-container
|
||||||
|
.file-preview-item
|
||||||
|
.file-size {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 11px;
|
||||||
|
margin-top: 1px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .note-file-link {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-color: var(--border-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .note-file-link:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .note-file-link .file-name {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .note-file-link .file-size {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Темная тема для изображений */
|
||||||
|
[data-theme="dark"] .image-preview-container {
|
||||||
|
background: #2a3a4a;
|
||||||
|
border-color: #4a5a6a;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .image-preview-header {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .image-preview-item {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-color: var(--border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .image-preview-item .image-info {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .note-images-container {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .note-image-item {
|
||||||
|
border-color: var(--border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .note-image {
|
||||||
|
border-color: var(--border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .note-image:hover::after {
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|||||||
533
server.js
533
server.js
@ -102,6 +102,58 @@ const uploadNoteImages = multer({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Настройка multer для загрузки файлов заметок
|
||||||
|
const noteFileStorage = 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-file-" +
|
||||||
|
req.session.userId +
|
||||||
|
"-" +
|
||||||
|
uniqueSuffix +
|
||||||
|
path.extname(file.originalname)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploadNoteFiles = multer({
|
||||||
|
storage: noteFileStorage,
|
||||||
|
limits: { fileSize: 50 * 1024 * 1024 }, // 50MB максимум для файлов заметок
|
||||||
|
fileFilter: function (req, file, cb) {
|
||||||
|
// Разрешенные типы файлов
|
||||||
|
const allowedMimes = [
|
||||||
|
"application/pdf",
|
||||||
|
"application/msword",
|
||||||
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
"application/vnd.ms-excel",
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
"text/plain",
|
||||||
|
"application/zip",
|
||||||
|
"application/x-zip-compressed",
|
||||||
|
"application/x-rar-compressed",
|
||||||
|
"application/x-7z-compressed",
|
||||||
|
];
|
||||||
|
|
||||||
|
const allowedExtensions = /pdf|doc|docx|xls|xlsx|txt|zip|rar|7z/;
|
||||||
|
const extname = allowedExtensions.test(
|
||||||
|
path.extname(file.originalname).toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (allowedMimes.includes(file.mimetype) || extname) {
|
||||||
|
return cb(null, true);
|
||||||
|
}
|
||||||
|
cb(
|
||||||
|
new Error(
|
||||||
|
"Только файлы форматов pdf, doc, docx, xls, xlsx, txt, zip, rar, 7z разрешены!"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Middleware для безопасности
|
// Middleware для безопасности
|
||||||
app.use(
|
app.use(
|
||||||
helmet({
|
helmet({
|
||||||
@ -217,6 +269,20 @@ function createTables() {
|
|||||||
)
|
)
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const createNoteFilesTable = `
|
||||||
|
CREATE TABLE IF NOT EXISTS note_files (
|
||||||
|
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 = `
|
const createUsersTable = `
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@ -247,6 +313,14 @@ function createTables() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
db.run(createNoteFilesTable, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Ошибка создания таблицы файлов заметок:", err.message);
|
||||||
|
} else {
|
||||||
|
console.log("Таблица файлов заметок готова");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
db.run(createUsersTable, (err) => {
|
db.run(createUsersTable, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error("Ошибка создания таблицы пользователей:", err.message);
|
console.error("Ошибка создания таблицы пользователей:", err.message);
|
||||||
@ -268,6 +342,7 @@ function createIndexes() {
|
|||||||
"CREATE INDEX IF NOT EXISTS idx_notes_is_archived ON notes(is_archived)",
|
"CREATE INDEX IF NOT EXISTS idx_notes_is_archived ON notes(is_archived)",
|
||||||
"CREATE INDEX IF NOT EXISTS idx_notes_pinned_at ON notes(pinned_at)",
|
"CREATE INDEX IF NOT EXISTS idx_notes_pinned_at ON notes(pinned_at)",
|
||||||
"CREATE INDEX IF NOT EXISTS idx_note_images_note_id ON note_images(note_id)",
|
"CREATE INDEX IF NOT EXISTS idx_note_images_note_id ON note_images(note_id)",
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_note_files_note_id ON note_files(note_id)",
|
||||||
"CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)",
|
"CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)",
|
||||||
"CREATE INDEX IF NOT EXISTS idx_action_logs_user_id ON action_logs(user_id)",
|
"CREATE INDEX IF NOT EXISTS idx_action_logs_user_id ON action_logs(user_id)",
|
||||||
"CREATE INDEX IF NOT EXISTS idx_action_logs_created_at ON action_logs(created_at)",
|
"CREATE INDEX IF NOT EXISTS idx_action_logs_created_at ON action_logs(created_at)",
|
||||||
@ -866,9 +941,9 @@ app.get("/api/notes/search", requireApiAuth, (req, res) => {
|
|||||||
SELECT
|
SELECT
|
||||||
n.*,
|
n.*,
|
||||||
CASE
|
CASE
|
||||||
WHEN COUNT(ni.id) = 0 THEN '[]'
|
WHEN COUNT(DISTINCT ni.id) = 0 THEN '[]'
|
||||||
ELSE json_group_array(
|
ELSE json_group_array(
|
||||||
json_object(
|
DISTINCT json_object(
|
||||||
'id', ni.id,
|
'id', ni.id,
|
||||||
'filename', ni.filename,
|
'filename', ni.filename,
|
||||||
'original_name', ni.original_name,
|
'original_name', ni.original_name,
|
||||||
@ -877,10 +952,25 @@ app.get("/api/notes/search", requireApiAuth, (req, res) => {
|
|||||||
'mime_type', ni.mime_type,
|
'mime_type', ni.mime_type,
|
||||||
'created_at', ni.created_at
|
'created_at', ni.created_at
|
||||||
)
|
)
|
||||||
|
) FILTER (WHERE ni.id IS NOT NULL)
|
||||||
|
END as images,
|
||||||
|
CASE
|
||||||
|
WHEN COUNT(DISTINCT nf.id) = 0 THEN '[]'
|
||||||
|
ELSE json_group_array(
|
||||||
|
DISTINCT json_object(
|
||||||
|
'id', nf.id,
|
||||||
|
'filename', nf.filename,
|
||||||
|
'original_name', nf.original_name,
|
||||||
|
'file_path', nf.file_path,
|
||||||
|
'file_size', nf.file_size,
|
||||||
|
'mime_type', nf.mime_type,
|
||||||
|
'created_at', nf.created_at
|
||||||
)
|
)
|
||||||
END as images
|
) FILTER (WHERE nf.id IS NOT NULL)
|
||||||
|
END as files
|
||||||
FROM notes n
|
FROM notes n
|
||||||
LEFT JOIN note_images ni ON n.id = ni.note_id
|
LEFT JOIN note_images ni ON n.id = ni.note_id
|
||||||
|
LEFT JOIN note_files nf ON n.id = nf.note_id
|
||||||
${whereClause}
|
${whereClause}
|
||||||
GROUP BY n.id
|
GROUP BY n.id
|
||||||
ORDER BY n.is_pinned DESC, n.pinned_at DESC, n.created_at DESC
|
ORDER BY n.is_pinned DESC, n.pinned_at DESC, n.created_at DESC
|
||||||
@ -892,25 +982,26 @@ app.get("/api/notes/search", requireApiAuth, (req, res) => {
|
|||||||
return res.status(500).json({ error: "Ошибка сервера" });
|
return res.status(500).json({ error: "Ошибка сервера" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Парсим JSON строки изображений
|
// Парсим JSON строки изображений и файлов
|
||||||
const notesWithImages = rows.map((row) => ({
|
const notesWithImagesAndFiles = rows.map((row) => ({
|
||||||
...row,
|
...row,
|
||||||
images: row.images === "[]" ? [] : JSON.parse(row.images),
|
images: row.images === "[]" ? [] : JSON.parse(row.images),
|
||||||
|
files: row.files === "[]" ? [] : JSON.parse(row.files),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
res.json(notesWithImages);
|
res.json(notesWithImagesAndFiles);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// API для получения всех заметок с изображениями (исключая архивные)
|
// API для получения всех заметок с изображениями и файлами (исключая архивные)
|
||||||
app.get("/api/notes", requireApiAuth, (req, res) => {
|
app.get("/api/notes", requireApiAuth, (req, res) => {
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT
|
SELECT
|
||||||
n.*,
|
n.*,
|
||||||
CASE
|
CASE
|
||||||
WHEN COUNT(ni.id) = 0 THEN '[]'
|
WHEN COUNT(DISTINCT ni.id) = 0 THEN '[]'
|
||||||
ELSE json_group_array(
|
ELSE json_group_array(
|
||||||
json_object(
|
DISTINCT json_object(
|
||||||
'id', ni.id,
|
'id', ni.id,
|
||||||
'filename', ni.filename,
|
'filename', ni.filename,
|
||||||
'original_name', ni.original_name,
|
'original_name', ni.original_name,
|
||||||
@ -919,10 +1010,25 @@ app.get("/api/notes", requireApiAuth, (req, res) => {
|
|||||||
'mime_type', ni.mime_type,
|
'mime_type', ni.mime_type,
|
||||||
'created_at', ni.created_at
|
'created_at', ni.created_at
|
||||||
)
|
)
|
||||||
|
) FILTER (WHERE ni.id IS NOT NULL)
|
||||||
|
END as images,
|
||||||
|
CASE
|
||||||
|
WHEN COUNT(DISTINCT nf.id) = 0 THEN '[]'
|
||||||
|
ELSE json_group_array(
|
||||||
|
DISTINCT json_object(
|
||||||
|
'id', nf.id,
|
||||||
|
'filename', nf.filename,
|
||||||
|
'original_name', nf.original_name,
|
||||||
|
'file_path', nf.file_path,
|
||||||
|
'file_size', nf.file_size,
|
||||||
|
'mime_type', nf.mime_type,
|
||||||
|
'created_at', nf.created_at
|
||||||
)
|
)
|
||||||
END as images
|
) FILTER (WHERE nf.id IS NOT NULL)
|
||||||
|
END as files
|
||||||
FROM notes n
|
FROM notes n
|
||||||
LEFT JOIN note_images ni ON n.id = ni.note_id
|
LEFT JOIN note_images ni ON n.id = ni.note_id
|
||||||
|
LEFT JOIN note_files nf ON n.id = nf.note_id
|
||||||
WHERE n.user_id = ? AND n.is_archived = 0
|
WHERE n.user_id = ? AND n.is_archived = 0
|
||||||
GROUP BY n.id
|
GROUP BY n.id
|
||||||
ORDER BY n.is_pinned DESC, n.pinned_at DESC, n.created_at DESC
|
ORDER BY n.is_pinned DESC, n.pinned_at DESC, n.created_at DESC
|
||||||
@ -934,13 +1040,14 @@ app.get("/api/notes", requireApiAuth, (req, res) => {
|
|||||||
return res.status(500).json({ error: "Ошибка сервера" });
|
return res.status(500).json({ error: "Ошибка сервера" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Парсим JSON строки изображений
|
// Парсим JSON строки изображений и файлов
|
||||||
const notesWithImages = rows.map((row) => ({
|
const notesWithImagesAndFiles = rows.map((row) => ({
|
||||||
...row,
|
...row,
|
||||||
images: row.images === "[]" ? [] : JSON.parse(row.images),
|
images: row.images === "[]" ? [] : JSON.parse(row.images),
|
||||||
|
files: row.files === "[]" ? [] : JSON.parse(row.files),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
res.json(notesWithImages);
|
res.json(notesWithImagesAndFiles);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1063,6 +1170,21 @@ app.delete("/api/notes/:id", requireApiAuth, (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Удаляем файлы заметки
|
||||||
|
const getFilesSql = "SELECT file_path FROM note_files WHERE note_id = ?";
|
||||||
|
db.all(getFilesSql, [id], (err, files) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Ошибка получения файлов:", err.message);
|
||||||
|
} else {
|
||||||
|
// Удаляем файлы
|
||||||
|
files.forEach((file) => {
|
||||||
|
const filePath = path.join(__dirname, "public", file.file_path);
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Удаляем записи об изображениях из БД
|
// Удаляем записи об изображениях из БД
|
||||||
const deleteImagesSql = "DELETE FROM note_images WHERE note_id = ?";
|
const deleteImagesSql = "DELETE FROM note_images WHERE note_id = ?";
|
||||||
db.run(deleteImagesSql, [id], (err) => {
|
db.run(deleteImagesSql, [id], (err) => {
|
||||||
@ -1071,6 +1193,14 @@ app.delete("/api/notes/:id", requireApiAuth, (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Удаляем записи о файлах из БД
|
||||||
|
const deleteFilesSql = "DELETE FROM note_files WHERE note_id = ?";
|
||||||
|
db.run(deleteFilesSql, [id], (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Ошибка удаления файлов:", err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Удаляем саму заметку
|
// Удаляем саму заметку
|
||||||
const deleteSql = "DELETE FROM notes WHERE id = ?";
|
const deleteSql = "DELETE FROM notes WHERE id = ?";
|
||||||
db.run(deleteSql, id, function (err) {
|
db.run(deleteSql, id, function (err) {
|
||||||
@ -1095,6 +1225,7 @@ app.delete("/api/notes/:id", requireApiAuth, (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// API для загрузки изображений к заметке
|
// API для загрузки изображений к заметке
|
||||||
@ -1280,6 +1411,189 @@ app.delete("/api/notes/:noteId/images/:imageId", requireApiAuth, (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// API для загрузки файлов к заметке
|
||||||
|
app.post(
|
||||||
|
"/api/notes/:id/files",
|
||||||
|
requireAuth,
|
||||||
|
uploadNoteFiles.array("files", 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 (!row) {
|
||||||
|
return res.status(404).json({ error: "Заметка не найдена" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.user_id !== req.session.userId) {
|
||||||
|
return res.status(403).json({ error: "Нет доступа к этой заметке" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем информацию о файлах в БД
|
||||||
|
const insertSql = `
|
||||||
|
INSERT INTO note_files (note_id, filename, original_name, file_path, file_size, mime_type)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
`;
|
||||||
|
|
||||||
|
const uploadedFiles = [];
|
||||||
|
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 fileFullPath = path.join(__dirname, "public", filePath);
|
||||||
|
if (fs.existsSync(fileFullPath)) {
|
||||||
|
fs.unlinkSync(fileFullPath);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uploadedFiles.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 (uploadedFiles.length === 0) {
|
||||||
|
return res
|
||||||
|
.status(500)
|
||||||
|
.json({ error: "Не удалось загрузить файлы" });
|
||||||
|
}
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: `Загружено ${uploadedFiles.length} файлов`,
|
||||||
|
files: uploadedFiles,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// API для получения файлов заметки
|
||||||
|
app.get("/api/notes/:id/files", requireApiAuth, (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 getFilesSql = `
|
||||||
|
SELECT id, filename, original_name, file_path, file_size, mime_type, created_at
|
||||||
|
FROM note_files
|
||||||
|
WHERE note_id = ?
|
||||||
|
ORDER BY created_at ASC
|
||||||
|
`;
|
||||||
|
|
||||||
|
db.all(getFilesSql, [id], (err, files) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Ошибка получения файлов:", err.message);
|
||||||
|
return res.status(500).json({ error: "Ошибка сервера" });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(files);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// API для удаления файла заметки
|
||||||
|
app.delete("/api/notes/:noteId/files/:fileId", requireApiAuth, (req, res) => {
|
||||||
|
const { noteId, fileId } = 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 getFileSql =
|
||||||
|
"SELECT file_path FROM note_files WHERE id = ? AND note_id = ?";
|
||||||
|
db.get(getFileSql, [fileId, noteId], (err, file) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Ошибка получения файла:", err.message);
|
||||||
|
return res.status(500).json({ error: "Ошибка сервера" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return res.status(404).json({ error: "Файл не найден" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удаляем файл
|
||||||
|
const filePath = path.join(__dirname, "public", file.file_path);
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удаляем запись из БД
|
||||||
|
const deleteSql = "DELETE FROM note_files WHERE id = ? AND note_id = ?";
|
||||||
|
db.run(deleteSql, [fileId, 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: "Файл удален" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Страница личного кабинета
|
// Страница личного кабинета
|
||||||
app.get("/profile", requireAuth, (req, res) => {
|
app.get("/profile", requireAuth, (req, res) => {
|
||||||
// Получаем цвет пользователя для предотвращения FOUC
|
// Получаем цвет пользователя для предотвращения FOUC
|
||||||
@ -1648,9 +1962,9 @@ app.get("/api/notes/archived", requireApiAuth, (req, res) => {
|
|||||||
SELECT
|
SELECT
|
||||||
n.*,
|
n.*,
|
||||||
CASE
|
CASE
|
||||||
WHEN COUNT(ni.id) = 0 THEN '[]'
|
WHEN COUNT(DISTINCT ni.id) = 0 THEN '[]'
|
||||||
ELSE json_group_array(
|
ELSE json_group_array(
|
||||||
json_object(
|
DISTINCT json_object(
|
||||||
'id', ni.id,
|
'id', ni.id,
|
||||||
'filename', ni.filename,
|
'filename', ni.filename,
|
||||||
'original_name', ni.original_name,
|
'original_name', ni.original_name,
|
||||||
@ -1659,10 +1973,25 @@ app.get("/api/notes/archived", requireApiAuth, (req, res) => {
|
|||||||
'mime_type', ni.mime_type,
|
'mime_type', ni.mime_type,
|
||||||
'created_at', ni.created_at
|
'created_at', ni.created_at
|
||||||
)
|
)
|
||||||
|
) FILTER (WHERE ni.id IS NOT NULL)
|
||||||
|
END as images,
|
||||||
|
CASE
|
||||||
|
WHEN COUNT(DISTINCT nf.id) = 0 THEN '[]'
|
||||||
|
ELSE json_group_array(
|
||||||
|
DISTINCT json_object(
|
||||||
|
'id', nf.id,
|
||||||
|
'filename', nf.filename,
|
||||||
|
'original_name', nf.original_name,
|
||||||
|
'file_path', nf.file_path,
|
||||||
|
'file_size', nf.file_size,
|
||||||
|
'mime_type', nf.mime_type,
|
||||||
|
'created_at', nf.created_at
|
||||||
)
|
)
|
||||||
END as images
|
) FILTER (WHERE nf.id IS NOT NULL)
|
||||||
|
END as files
|
||||||
FROM notes n
|
FROM notes n
|
||||||
LEFT JOIN note_images ni ON n.id = ni.note_id
|
LEFT JOIN note_images ni ON n.id = ni.note_id
|
||||||
|
LEFT JOIN note_files nf ON n.id = nf.note_id
|
||||||
WHERE n.user_id = ? AND n.is_archived = 1
|
WHERE n.user_id = ? AND n.is_archived = 1
|
||||||
GROUP BY n.id
|
GROUP BY n.id
|
||||||
ORDER BY n.is_pinned DESC, n.pinned_at DESC, n.created_at DESC
|
ORDER BY n.is_pinned DESC, n.pinned_at DESC, n.created_at DESC
|
||||||
@ -1674,13 +2003,14 @@ app.get("/api/notes/archived", requireApiAuth, (req, res) => {
|
|||||||
return res.status(500).json({ error: "Ошибка сервера" });
|
return res.status(500).json({ error: "Ошибка сервера" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Парсим JSON строки изображений
|
// Парсим JSON строки изображений и файлов
|
||||||
const notesWithImages = rows.map((row) => ({
|
const notesWithImagesAndFiles = rows.map((row) => ({
|
||||||
...row,
|
...row,
|
||||||
images: row.images === "[]" ? [] : JSON.parse(row.images),
|
images: row.images === "[]" ? [] : JSON.parse(row.images),
|
||||||
|
files: row.files === "[]" ? [] : JSON.parse(row.files),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
res.json(notesWithImages);
|
res.json(notesWithImagesAndFiles);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1750,6 +2080,25 @@ app.delete("/api/notes/archived/all", requireApiAuth, async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Получаем и удаляем файлы заметок
|
||||||
|
const getFilesSql = `SELECT file_path FROM note_files WHERE note_id IN (${placeholders})`;
|
||||||
|
db.all(getFilesSql, noteIds, (err, files) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Ошибка получения файлов:", err.message);
|
||||||
|
} else {
|
||||||
|
// Удаляем файлы
|
||||||
|
files.forEach((file) => {
|
||||||
|
const filePath = path.join(__dirname, "public", file.file_path);
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Ошибка удаления файла:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Удаляем записи об изображениях
|
// Удаляем записи об изображениях
|
||||||
const deleteImagesSql = `DELETE FROM note_images WHERE note_id IN (${placeholders})`;
|
const deleteImagesSql = `DELETE FROM note_images WHERE note_id IN (${placeholders})`;
|
||||||
db.run(deleteImagesSql, noteIds, (err) => {
|
db.run(deleteImagesSql, noteIds, (err) => {
|
||||||
@ -1757,6 +2106,13 @@ app.delete("/api/notes/archived/all", requireApiAuth, async (req, res) => {
|
|||||||
console.error("Ошибка удаления изображений:", err.message);
|
console.error("Ошибка удаления изображений:", err.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Удаляем записи о файлах
|
||||||
|
const deleteFilesSql = `DELETE FROM note_files WHERE note_id IN (${placeholders})`;
|
||||||
|
db.run(deleteFilesSql, noteIds, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Ошибка удаления файлов:", err.message);
|
||||||
|
}
|
||||||
|
|
||||||
// Удаляем сами заметки
|
// Удаляем сами заметки
|
||||||
const deleteNotesSql =
|
const deleteNotesSql =
|
||||||
"DELETE FROM notes WHERE user_id = ? AND is_archived = 1";
|
"DELETE FROM notes WHERE user_id = ? AND is_archived = 1";
|
||||||
@ -1787,6 +2143,8 @@ app.delete("/api/notes/archived/all", requireApiAuth, async (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Ошибка при проверке пароля:", error);
|
console.error("Ошибка при проверке пароля:", error);
|
||||||
res.status(500).json({ error: "Ошибка сервера" });
|
res.status(500).json({ error: "Ошибка сервера" });
|
||||||
@ -1831,6 +2189,20 @@ app.delete("/api/notes/archived/:id", requireApiAuth, (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Удаляем файлы заметки
|
||||||
|
const getFilesSql = "SELECT file_path FROM note_files WHERE note_id = ?";
|
||||||
|
db.all(getFilesSql, [id], (err, files) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Ошибка получения файлов:", err.message);
|
||||||
|
} else {
|
||||||
|
files.forEach((file) => {
|
||||||
|
const filePath = path.join(__dirname, "public", file.file_path);
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Удаляем записи об изображениях
|
// Удаляем записи об изображениях
|
||||||
const deleteImagesSql = "DELETE FROM note_images WHERE note_id = ?";
|
const deleteImagesSql = "DELETE FROM note_images WHERE note_id = ?";
|
||||||
db.run(deleteImagesSql, [id], (err) => {
|
db.run(deleteImagesSql, [id], (err) => {
|
||||||
@ -1839,6 +2211,14 @@ app.delete("/api/notes/archived/:id", requireApiAuth, (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Удаляем записи о файлах
|
||||||
|
const deleteFilesSql = "DELETE FROM note_files WHERE note_id = ?";
|
||||||
|
db.run(deleteFilesSql, [id], (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Ошибка удаления файлов:", err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Удаляем саму заметку
|
// Удаляем саму заметку
|
||||||
const deleteSql = "DELETE FROM notes WHERE id = ?";
|
const deleteSql = "DELETE FROM notes WHERE id = ?";
|
||||||
db.run(deleteSql, id, function (err) {
|
db.run(deleteSql, id, function (err) {
|
||||||
@ -1860,6 +2240,7 @@ app.delete("/api/notes/archived/:id", requireApiAuth, (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// API для получения логов пользователя
|
// API для получения логов пользователя
|
||||||
@ -2214,7 +2595,7 @@ app.delete("/api/user/delete-account", requireApiAuth, async (req, res) => {
|
|||||||
return res.status(401).json({ error: "Неверный пароль" });
|
return res.status(401).json({ error: "Неверный пароль" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получаем аватарку пользователя перед удалением
|
// Получаем аватарку и все файлы пользователя перед удалением
|
||||||
const getAvatarSql = "SELECT avatar FROM users WHERE id = ?";
|
const getAvatarSql = "SELECT avatar FROM users WHERE id = ?";
|
||||||
db.get(getAvatarSql, [userId], (err, userData) => {
|
db.get(getAvatarSql, [userId], (err, userData) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
@ -2222,33 +2603,99 @@ app.delete("/api/user/delete-account", requireApiAuth, async (req, res) => {
|
|||||||
return res.status(500).json({ error: "Ошибка получения аватарки" });
|
return res.status(500).json({ error: "Ошибка получения аватарки" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Удаляем файл аватарки, если он существует
|
// Получаем все изображения заметок пользователя
|
||||||
|
const getImagesSql = `
|
||||||
|
SELECT ni.file_path
|
||||||
|
FROM note_images ni
|
||||||
|
JOIN notes n ON ni.note_id = n.id
|
||||||
|
WHERE n.user_id = ?
|
||||||
|
`;
|
||||||
|
db.all(getImagesSql, [userId], (err, images) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(
|
||||||
|
"Ошибка получения изображений пользователя:",
|
||||||
|
err.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем все файлы заметок пользователя
|
||||||
|
const getFilesSql = `
|
||||||
|
SELECT nf.file_path
|
||||||
|
FROM note_files nf
|
||||||
|
JOIN notes n ON nf.note_id = n.id
|
||||||
|
WHERE n.user_id = ?
|
||||||
|
`;
|
||||||
|
db.all(getFilesSql, [userId], (err, files) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(
|
||||||
|
"Ошибка получения файлов пользователя:",
|
||||||
|
err.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удаляем файл аватарки
|
||||||
if (userData && userData.avatar) {
|
if (userData && userData.avatar) {
|
||||||
const avatarPath = path.join(
|
const avatarPath = path.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
"public",
|
"public",
|
||||||
"uploads",
|
|
||||||
userData.avatar
|
userData.avatar
|
||||||
);
|
);
|
||||||
fs.unlink(avatarPath, (err) => {
|
if (fs.existsSync(avatarPath)) {
|
||||||
if (err && err.code !== "ENOENT") {
|
try {
|
||||||
|
fs.unlinkSync(avatarPath);
|
||||||
|
} catch (err) {
|
||||||
console.error("Ошибка удаления файла аватарки:", err.message);
|
console.error("Ошибка удаления файла аватарки:", err.message);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удаляем все изображения
|
||||||
|
if (images) {
|
||||||
|
images.forEach((image) => {
|
||||||
|
const imagePath = path.join(
|
||||||
|
__dirname,
|
||||||
|
"public",
|
||||||
|
image.file_path
|
||||||
|
);
|
||||||
|
if (fs.existsSync(imagePath)) {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(imagePath);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Ошибка удаления изображения:", err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Начинаем транзакцию для удаления всех данных пользователя
|
// Удаляем все файлы
|
||||||
|
if (files) {
|
||||||
|
files.forEach((file) => {
|
||||||
|
const filePath = path.join(__dirname, "public", file.file_path);
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Ошибка удаления файла:", err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Начинаем транзакцию для удаления всех данных из БД
|
||||||
db.serialize(() => {
|
db.serialize(() => {
|
||||||
db.run("BEGIN TRANSACTION");
|
db.run("BEGIN TRANSACTION");
|
||||||
|
|
||||||
// Удаляем все изображения заметок пользователя (через JOIN с notes)
|
// Удаляем все изображения заметок пользователя
|
||||||
const deleteImagesSql = `
|
const deleteImagesSql = `
|
||||||
DELETE FROM note_images
|
DELETE FROM note_images
|
||||||
WHERE note_id IN (SELECT id FROM notes WHERE user_id = ?)
|
WHERE note_id IN (SELECT id FROM notes WHERE user_id = ?)
|
||||||
`;
|
`;
|
||||||
db.run(deleteImagesSql, [userId], (err) => {
|
db.run(deleteImagesSql, [userId], (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error("Ошибка удаления изображений:", err.message);
|
console.error(
|
||||||
|
"Ошибка удаления изображений из БД:",
|
||||||
|
err.message
|
||||||
|
);
|
||||||
db.run("ROLLBACK");
|
db.run("ROLLBACK");
|
||||||
return res
|
return res
|
||||||
.status(500)
|
.status(500)
|
||||||
@ -2256,12 +2703,29 @@ app.delete("/api/user/delete-account", requireApiAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Удаляем все заметки пользователя (CASCADE удалит связанные изображения)
|
// Удаляем все файлы заметок пользователя
|
||||||
|
const deleteFilesSql = `
|
||||||
|
DELETE FROM note_files
|
||||||
|
WHERE note_id IN (SELECT id FROM notes WHERE user_id = ?)
|
||||||
|
`;
|
||||||
|
db.run(deleteFilesSql, [userId], (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Ошибка удаления файлов из БД:", err.message);
|
||||||
|
db.run("ROLLBACK");
|
||||||
|
return res
|
||||||
|
.status(500)
|
||||||
|
.json({ error: "Ошибка удаления файлов" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Удаляем все заметки пользователя
|
||||||
db.run("DELETE FROM notes WHERE user_id = ?", [userId], (err) => {
|
db.run("DELETE FROM notes WHERE user_id = ?", [userId], (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error("Ошибка удаления заметок:", err.message);
|
console.error("Ошибка удаления заметок:", err.message);
|
||||||
db.run("ROLLBACK");
|
db.run("ROLLBACK");
|
||||||
return res.status(500).json({ error: "Ошибка удаления заметок" });
|
return res
|
||||||
|
.status(500)
|
||||||
|
.json({ error: "Ошибка удаления заметок" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -2273,7 +2737,9 @@ app.delete("/api/user/delete-account", requireApiAuth, async (req, res) => {
|
|||||||
if (err) {
|
if (err) {
|
||||||
console.error("Ошибка удаления логов:", err.message);
|
console.error("Ошибка удаления логов:", err.message);
|
||||||
db.run("ROLLBACK");
|
db.run("ROLLBACK");
|
||||||
return res.status(500).json({ error: "Ошибка удаления логов" });
|
return res
|
||||||
|
.status(500)
|
||||||
|
.json({ error: "Ошибка удаления логов" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -2291,7 +2757,10 @@ app.delete("/api/user/delete-account", requireApiAuth, async (req, res) => {
|
|||||||
// Подтверждаем транзакцию
|
// Подтверждаем транзакцию
|
||||||
db.run("COMMIT", (err) => {
|
db.run("COMMIT", (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error("Ошибка подтверждения транзакции:", err.message);
|
console.error(
|
||||||
|
"Ошибка подтверждения транзакции:",
|
||||||
|
err.message
|
||||||
|
);
|
||||||
return res
|
return res
|
||||||
.status(500)
|
.status(500)
|
||||||
.json({ error: "Ошибка подтверждения транзакции" });
|
.json({ error: "Ошибка подтверждения транзакции" });
|
||||||
@ -2313,6 +2782,8 @@ app.delete("/api/user/delete-account", requireApiAuth, async (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Ошибка удаления аккаунта:", error);
|
console.error("Ошибка удаления аккаунта:", error);
|
||||||
res.status(500).json({ error: "Ошибка сервера" });
|
res.status(500).json({ error: "Ошибка сервера" });
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user