diff --git a/MOBILE-UPLOAD-TESTING.md b/MOBILE-UPLOAD-TESTING.md deleted file mode 100644 index 0f7d1fc..0000000 --- a/MOBILE-UPLOAD-TESTING.md +++ /dev/null @@ -1,92 +0,0 @@ -# 📱 Тестирование загрузки изображений на мобильных устройствах - -## Проблема -Пользователи не могли загружать картинки в заметки с мобильных телефонов. - -## Внесенные исправления - -### 1. Улучшения JavaScript (app.js) -- ✅ Добавлена поддержка touch событий для кнопки загрузки изображений -- ✅ Улучшена обработка выбора файлов с проверкой размера и типа -- ✅ Добавлена защита от дублирования файлов -- ✅ Улучшена функция обновления превью с обработкой ошибок -- ✅ Добавлены индикаторы загрузки для мобильных устройств -- ✅ Улучшена функция сохранения заметок с уведомлениями - -### 2. Улучшения CSS (style.css) -- ✅ Добавлены стили для touch устройств (touch-action, -webkit-tap-highlight-color) -- ✅ Увеличена минимальная высота кнопок для удобства touch (44px+) -- ✅ Улучшены размеры кнопок удаления изображений -- ✅ Добавлены специальные стили для мобильных устройств в медиа-запросах - -### 3. Создана тестовая страница -- ✅ `/test-mobile-upload.html` - специальная страница для тестирования загрузки на мобильных - -## Как протестировать - -### На мобильном устройстве: -1. Откройте приложение в мобильном браузере -2. Перейдите на страницу заметок -3. Нажмите на кнопку загрузки изображений (📷) -4. Выберите одно или несколько изображений -5. Проверьте превью изображений -6. Нажмите "Сохранить" -7. Убедитесь, что изображения загрузились и отображаются в заметке - -### Альтернативный способ тестирования: -1. Откройте `/test-mobile-upload.html` на мобильном устройстве -2. Эта страница содержит специальные тесты для проверки загрузки файлов -3. Проверьте все функции загрузки и отображения - -## Основные улучшения для мобильных устройств - -### Touch Events -- Добавлена поддержка `touchend` событий -- Улучшена обработка touch для кнопок - -### Размеры элементов -- Минимальная высота кнопок: 44px (рекомендация Apple/Google) -- Увеличены размеры кнопок удаления изображений -- Улучшены отступы и размеры для touch - -### Визуальная обратная связь -- Индикаторы загрузки для мобильных устройств -- Уведомления об успешном сохранении -- Обработка ошибок с понятными сообщениями - -### Производительность -- Проверка размера файлов (максимум 10MB) -- Защита от дублирования файлов -- Обработка ошибок чтения файлов - -## Поддерживаемые форматы изображений -- JPEG (.jpg, .jpeg) -- PNG (.png) -- GIF (.gif) -- WebP (.webp) - -## Ограничения -- Максимальный размер файла: 10MB -- Максимальное количество файлов за раз: 10 -- Поддерживаются только изображения - -## Браузеры -Протестировано на: -- ✅ Chrome Mobile (Android) -- ✅ Safari Mobile (iOS) -- ✅ Firefox Mobile -- ✅ Samsung Internet - -## Если проблемы остаются - -1. **Проверьте консоль браузера** на наличие ошибок JavaScript -2. **Убедитесь, что у вас стабильное интернет-соединение** -3. **Попробуйте уменьшить размер изображений** (сжать перед загрузкой) -4. **Проверьте, что браузер поддерживает File API** (современные браузеры поддерживают) -5. **Попробуйте перезагрузить страницу** и повторить попытку - -## Отладка -Для отладки используйте: -- `/test-mobile-upload.html` - специальная тестовая страница -- Консоль разработчика в мобильном браузере -- Информация об устройстве и браузере на тестовой странице diff --git a/PWA-TESTING.md b/PWA-TESTING.md deleted file mode 100644 index db52886..0000000 --- a/PWA-TESTING.md +++ /dev/null @@ -1,269 +0,0 @@ -# 🚀 Тестирование PWA для NoteJS - -## Что было сделано - -✅ **Созданы файлы PWA:** - -- `manifest.json` - манифест приложения -- `sw.js` - сервис-воркер для кэширования -- `pwa.js` - JavaScript класс для управления PWA -- `icon.svg` - SVG иконка приложения -- `logo.svg` - логотип приложения -- `icons/` - PNG иконки различных размеров -- `browserconfig.xml` - конфигурация для Windows - -✅ **Обновлены HTML страницы:** - -- Добавлены PWA мета-теги -- Подключены иконки и манифест -- Добавлен скрипт регистрации Service Worker - -✅ **Настроен сервер:** - -- Правильные заголовки для PWA файлов -- Поддержка кэширования - -## Как протестировать - -### 1. Откройте диагностическую страницу PWA - -``` -http://localhost:3000/pwa-debug.html -``` - -### 2. Откройте тестовую страницу для мобильных устройств - -``` -http://localhost:3000/mobile-pwa-test.html -``` - -### 3. Откройте тестовую страницу для Brave браузера - -``` -http://localhost:3000/brave-pwa-test.html -``` - -### 4. Откройте обычную тестовую страницу - -``` -http://localhost:3000/test-pwa.html -``` - -### 5. Проверьте требования PWA - -На мобильной тестовой странице автоматически проверяются все требования: - -- ✅ HTTPS или localhost -- ✅ Service Worker -- ✅ Manifest -- ✅ Иконки -- ❌ Уже установлено (должно быть красным, если не установлено) - -### 6. Установка приложения - -- Если все проверки пройдены, появится кнопка "Установить приложение" -- Нажмите на неё для установки PWA -- Следуйте инструкциям браузера или используйте инструкции на странице - -### 7. Проверка в разных браузерах - -#### Chrome/Edge: - -- Откройте DevTools (F12) -- Перейдите в Application → Manifest -- Проверьте, что манифест загружается без ошибок -- В Application → Service Workers проверьте статус SW - -#### Firefox: - -- Откройте DevTools (F12) -- Перейдите в Application → Manifest -- Проверьте манифест - -#### Brave: - -- Откройте `http://localhost:3000/brave-pwa-test.html` для диагностики -- Проверьте настройки PWA в браузере -- Используйте меню браузера для установки - -#### Safari (iOS): - -- Откройте сайт в Safari -- Нажмите кнопку "Поделиться" -- Выберите "На экран Домой" -- Приложение установится как PWA - -#### ПК/Десктоп (Chrome, Edge, Firefox): - -- Откройте сайт в браузере -- Нажмите на иконку установки в адресной строке (если доступна) -- Или используйте меню браузера → "Установить приложение" -- Или используйте меню браузера → "Создать ярлык" - -## Новые улучшения для мобильных устройств - -✅ **Улучшенный manifest.json:** - -- Добавлены все необходимые размеры иконок (72x72, 96x96, 128x128, 144x144, 152x152, 192x192, 384x384, 512x512) -- Добавлены maskable иконки для Android -- Добавлены категории и скриншоты -- Улучшена совместимость с мобильными устройствами - -✅ **Улучшенные мета-теги:** - -- Добавлены все необходимые apple-touch-icon размеры -- Улучшена поддержка iOS Safari -- Добавлены мета-теги для Windows -- Настроен правильный статус-бар для iOS - -✅ **Улучшенный Service Worker:** - -- Кэширование всех иконок -- Улучшенная обработка ошибок -- Fallback для различных типов ресурсов -- Лучшая поддержка мобильных устройств - -✅ **Улучшенный PWA Manager:** - -- Определение мобильного Safari -- Разные инструкции для разных браузеров -- Улучшенная проверка установки PWA -- Поддержка различных режимов отображения -- **Кнопка установки показывается только на мобильных устройствах** -- Принудительная проверка возможности установки -- Специальные инструкции для Android и iOS -- Диагностическая страница для отладки PWA - -## Возможные проблемы и решения - -### 1. Кнопка установки не появляется - -**Причины:** - -- Приложение уже установлено -- Браузер не поддерживает PWA -- Не выполнены требования PWA -- **Вы используете ПК/десктоп (кнопка скрыта для ПК)** -- **Проблемы с Brave браузером** - -**Решение:** - -- Проверьте статус на мобильной тестовой странице -- Убедитесь, что используете HTTPS или localhost -- Проверьте консоль браузера на ошибки -- Для iOS Safari используйте инструкции "Добавить на главный экран" -- **На ПК используйте меню браузера для установки PWA** -- **Для Brave: проверьте настройки PWA в браузере** - -### 1.1. Проблемы с Brave браузером - -**Причины:** - -- Brave может блокировать PWA по умолчанию -- Неправильная конфигурация manifest.json -- Проблемы с Service Worker в Brave - -**Решение:** - -- Откройте `http://localhost:3000/brave-pwa-test.html` для диагностики -- В Brave перейдите в **Настройки → Дополнительно → Сайты и разрешения → PWA** -- Убедитесь, что PWA включены -- Попробуйте установить через меню браузера (три точки → "Установить приложение") -- Обновите manifest.json с новыми полями для Brave совместимости - -### 2. Service Worker не регистрируется - -**Причины:** - -- Ошибки в коде SW -- Проблемы с кэшированием файлов - -**Решение:** - -- Откройте DevTools → Application → Service Workers -- Проверьте ошибки в консоли -- Попробуйте очистить кэш - -### 3. Ошибка "Download error or resource isn't a valid image" - -**Причины:** - -- PNG иконки повреждены или имеют неправильный размер -- Иконки не являются валидными PNG файлами - -**Решение:** - -- ✅ **ИСПРАВЛЕНО**: Созданы правильные PNG иконки с помощью pngjs -- Проверьте, что иконки имеют правильные размеры (192x192, 512x512) -- Убедитесь, что файлы иконок валидные PNG - -### 4. Предупреждение о deprecated meta tag - -**Причины:** - -- Использование устаревшего `apple-mobile-web-app-capable` - -**Решение:** - -- ✅ **ИСПРАВЛЕНО**: Добавлен современный `mobile-web-app-capable` -- Оба тега теперь присутствуют для совместимости - -## Отладка - -### Консоль браузера - -Откройте DevTools (F12) и проверьте консоль на ошибки: - -```javascript -// Проверить статус PWA -window.debugPWA(); - -// Проверить Service Worker -navigator.serviceWorker.getRegistrations().then(console.log); - -// Проверить возможность установки -window.checkInstallability(); - -// Принудительная попытка установки -window.forceInstall(); -``` - -### Диагностическая страница - -Используйте `http://localhost:3000/pwa-debug.html` для: - -- Детальной диагностики всех требований PWA -- Проверки загрузки манифеста и Service Worker -- Отображения информации о браузере и устройстве -- Просмотра ошибок консоли -- Тестирования установки PWA - -### Lighthouse - -Запустите аудит Lighthouse в Chrome DevTools: - -1. Откройте DevTools (F12) -2. Перейдите в Lighthouse -3. Выберите "Progressive Web App" -4. Нажмите "Generate report" - -## Файлы для проверки - -- ✅ `http://localhost:3000/manifest.json` - должен возвращать JSON -- ✅ `http://localhost:3000/sw.js` - должен возвращать JavaScript -- ✅ `http://localhost:3000/icons/icon-192x192.png` - должен возвращать PNG -- ✅ `http://localhost:3000/icons/icon-512x512.png` - должен возвращать PNG -- ✅ `http://localhost:3000/pwa-debug.html` - диагностическая страница PWA -- ✅ `http://localhost:3000/mobile-pwa-test.html` - мобильная тестовая страница -- ✅ `http://localhost:3000/brave-pwa-test.html` - тестовая страница для Brave браузера -- ✅ `http://localhost:3000/test-pwa.html` - обычная тестовая страница - -## Следующие шаги - -После успешного тестирования: - -1. Удалите тестовую страницу `test-pwa.html` -2. Настройте HTTPS для продакшена -3. Добавьте реальные скриншоты в манифест -4. Создайте качественные иконки с помощью дизайнера -5. Настройте push-уведомления (опционально) diff --git a/public/app.js b/public/app.js index 4dbc509..b0cbf95 100644 --- a/public/app.js +++ b/public/app.js @@ -36,6 +36,47 @@ let searchResults = []; let notesCache = null; // Кэш для заметок let lastLoadTime = 0; // Время последней загрузки +// Lazy loading для изображений +function initLazyLoading() { + // Проверяем поддержку Intersection Observer API + if ("IntersectionObserver" in window) { + const imageObserver = new IntersectionObserver( + (entries, observer) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + const img = entry.target; + // Если у изображения есть data-src, загружаем его + if (img.dataset.src) { + img.src = img.dataset.src; + img.removeAttribute("data-src"); + } + img.classList.remove("lazy"); + observer.unobserve(img); + } + }); + }, + { + rootMargin: "50px 0px", // Загружать изображения за 50px до появления в viewport + threshold: 0.01, + } + ); + + // Наблюдаем за всеми изображениями с классом lazy + document.querySelectorAll("img.lazy").forEach((img) => { + imageObserver.observe(img); + }); + } else { + // Fallback для старых браузеров - просто показываем все изображения + document.querySelectorAll("img.lazy").forEach((img) => { + if (img.dataset.src) { + img.src = img.dataset.src; + img.removeAttribute("data-src"); + } + img.classList.remove("lazy"); + }); + } +} + // Функция для получения текущей даты и времени function getFormattedDateTime() { let now = new Date(); @@ -151,7 +192,10 @@ function renderTags() { // Добавляем обработчики кликов для тегов tagsContainer.querySelectorAll(".tag").forEach((tagElement) => { - tagElement.addEventListener("click", async (event) => await handleTagClick(event)); + tagElement.addEventListener( + "click", + async (event) => await handleTagClick(event) + ); }); } @@ -343,20 +387,21 @@ imageBtn.addEventListener("touchend", function (event) { imageInput.addEventListener("change", function (event) { const files = Array.from(event.target.files); let addedCount = 0; - - files.forEach(file => { - if (file.type.startsWith('image/')) { + + files.forEach((file) => { + if (file.type.startsWith("image/")) { // Проверяем размер файла (максимум 10MB) if (file.size > 10 * 1024 * 1024) { alert(`Файл "${file.name}" слишком большой. Максимальный размер: 10MB`); return; } - + // Проверяем, не добавлен ли уже этот файл - const isDuplicate = selectedImages.some(existingFile => - existingFile.name === file.name && existingFile.size === file.size + const isDuplicate = selectedImages.some( + (existingFile) => + existingFile.name === file.name && existingFile.size === file.size ); - + if (!isDuplicate) { selectedImages.push(file); addedCount++; @@ -365,7 +410,7 @@ imageInput.addEventListener("change", function (event) { alert(`Файл "${file.name}" не является изображением`); } }); - + if (addedCount > 0) { updateImagePreview(); // Показываем уведомление о добавленных файлах @@ -375,9 +420,9 @@ imageInput.addEventListener("change", function (event) { console.log(`Добавлено ${addedCount} изображений`); } } - + // Очищаем input для возможности повторного выбора тех же файлов - event.target.value = ''; + event.target.value = ""; }); // Обработчик очистки всех изображений @@ -420,19 +465,20 @@ function updateImagePreview() { reader.onload = function (e) { const previewItem = document.createElement("div"); previewItem.className = "image-preview-item"; - + // Форматируем размер файла const fileSize = (file.size / 1024 / 1024).toFixed(2); - const fileName = file.name.length > 20 ? file.name.substring(0, 20) + '...' : file.name; - + const fileName = + file.name.length > 20 ? file.name.substring(0, 20) + "..." : file.name; + previewItem.innerHTML = ` - Preview + Preview
${fileName}
${fileSize} MB
`; - + imagePreviewList.appendChild(previewItem); - + // Обработчик удаления изображения const removeBtn = previewItem.querySelector(".remove-image-btn"); removeBtn.addEventListener("click", function (event) { @@ -441,7 +487,7 @@ function updateImagePreview() { selectedImages.splice(index, 1); updateImagePreview(); }); - + // Дополнительный обработчик для touch событий removeBtn.addEventListener("touchend", function (event) { event.preventDefault(); @@ -450,25 +496,25 @@ function updateImagePreview() { updateImagePreview(); }); }; - - reader.onerror = function() { - console.error('Ошибка чтения файла:', file.name); + + reader.onerror = function () { + console.error("Ошибка чтения файла:", file.name); alert(`Ошибка чтения файла: ${file.name}`); }; - + reader.readAsDataURL(file); }); } // Функция для отображения изображения в модальном окне function showImageModal(imageSrc) { - console.log('showImageModal called with:', imageSrc); + console.log("showImageModal called with:", imageSrc); try { modalImage.src = imageSrc; imageModal.style.display = "block"; - console.log('Modal opened successfully'); + console.log("Modal opened successfully"); } catch (error) { - console.error('Error in showImageModal:', error); + console.error("Error in showImageModal:", error); } } @@ -479,20 +525,23 @@ async function uploadImages(noteId) { } const formData = new FormData(); - selectedImages.forEach(file => { + selectedImages.forEach((file) => { formData.append("images", 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; - + 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-upload-loading'; + const loadingDiv = document.createElement("div"); + loadingDiv.id = "mobile-upload-loading"; loadingDiv.style.cssText = ` position: fixed; top: 50%; @@ -523,23 +572,23 @@ async function uploadImages(noteId) { } const result = await response.json(); - + // Удаляем индикатор загрузки - const loadingDiv = document.getElementById('mobile-upload-loading'); + const loadingDiv = document.getElementById("mobile-upload-loading"); if (loadingDiv) { loadingDiv.remove(); } - + return result.images || []; } catch (error) { console.error("Ошибка загрузки изображений:", error); - + // Удаляем индикатор загрузки в случае ошибки - const loadingDiv = document.getElementById('mobile-upload-loading'); + const loadingDiv = document.getElementById("mobile-upload-loading"); if (loadingDiv) { loadingDiv.remove(); } - + // Показываем ошибку пользователю alert(`Ошибка загрузки изображений: ${error.message}`); return []; @@ -582,9 +631,9 @@ async function deleteNoteImage(noteId, imageId) { async function loadNotes(forceReload = false) { const now = Date.now(); const CACHE_DURATION = 30000; // 30 секунд кэширования - + // Используем кэш, если он не устарел и не требуется принудительная перезагрузка - if (!forceReload && notesCache && (now - lastLoadTime) < CACHE_DURATION) { + if (!forceReload && notesCache && now - lastLoadTime < CACHE_DURATION) { allNotes = notesCache; await renderNotes(notesCache); renderCalendar(); @@ -772,18 +821,18 @@ async function renderNotes(notes) { // Используем изображения, которые уже пришли с заметкой const noteImages = Array.isArray(note.images) ? note.images : []; let imagesHtml = ""; - + if (noteImages.length > 0) { - imagesHtml = '
'; - noteImages.forEach(image => { - imagesHtml += ` + imagesHtml = '
'; + noteImages.forEach((image) => { + imagesHtml += `
- ${image.original_name} + ${image.original_name}
`; - }); - imagesHtml += '
'; + }); + imagesHtml += "
"; } const noteHtml = ` @@ -818,36 +867,39 @@ async function renderNotes(notes) { // Обрабатываем длинные заметки handleLongNotes(); + + // Инициализируем lazy loading для новых изображений + initLazyLoading(); } // Функция для обработки длинных заметок function handleLongNotes() { const MAX_HEIGHT = 300; // Максимальная высота в пикселях - + document.querySelectorAll(".textNote").forEach((noteElement) => { // Проверяем высоту контента const contentHeight = noteElement.scrollHeight; - + if (contentHeight > MAX_HEIGHT) { // Добавляем класс для сворачивания noteElement.classList.add("collapsed"); - + // Создаем кнопку "Показать все" const showMoreBtn = document.createElement("button"); showMoreBtn.classList.add("show-more-btn"); showMoreBtn.textContent = "Показать полностью"; showMoreBtn.setAttribute("data-expanded", "false"); - + // Вставляем кнопку после заметки noteElement.parentElement.insertBefore( showMoreBtn, noteElement.nextSibling ); - + // Обработчик клика на кнопку showMoreBtn.addEventListener("click", function () { const isExpanded = this.getAttribute("data-expanded") === "true"; - + if (isExpanded) { // Сворачиваем noteElement.classList.add("collapsed"); @@ -899,7 +951,7 @@ function addNoteEventListeners() { // Разворачиваем заметку при редактировании noteContent.classList.remove("collapsed"); - + // Скрываем кнопку "Показать полностью" если она есть const showMoreBtn = noteContainer.querySelector(".show-more-btn"); if (showMoreBtn) { @@ -990,7 +1042,7 @@ function addNoteEventListeners() { saveButtonContainer.appendChild(saveHint); // Функция обновления превью изображений для режима редактирования - const updateEditImagePreview = function() { + const updateEditImagePreview = function () { if (editSelectedImages.length === 0) { editImagePreviewContainer.style.display = "none"; return; @@ -1004,15 +1056,15 @@ function addNoteEventListeners() { reader.onload = function (e) { const previewItem = document.createElement("div"); previewItem.className = "image-preview-item"; - + previewItem.innerHTML = ` - Preview + Preview
${file.name}
`; - + editImagePreviewList.appendChild(previewItem); - + // Обработчик удаления изображения const removeBtn = previewItem.querySelector(".remove-image-btn"); removeBtn.addEventListener("click", function () { @@ -1025,13 +1077,13 @@ function addNoteEventListeners() { }; // Функция загрузки изображений для режима редактирования - const uploadEditImages = async function(noteId) { + const uploadEditImages = async function (noteId) { if (editSelectedImages.length === 0) { return []; } const formData = new FormData(); - editSelectedImages.forEach(file => { + editSelectedImages.forEach((file) => { formData.append("images", file); }); @@ -1099,8 +1151,8 @@ function addNoteEventListeners() { // Обработчики для загрузки изображений в режиме редактирования editImageInput.addEventListener("change", function (event) { const files = Array.from(event.target.files); - files.forEach(file => { - if (file.type.startsWith('image/')) { + files.forEach((file) => { + if (file.type.startsWith("image/")) { editSelectedImages.push(file); } }); @@ -1108,7 +1160,9 @@ function addNoteEventListeners() { }); // Обработчик очистки всех изображений в режиме редактирования - const editClearImagesBtn = editImagePreviewHeader.querySelector(`#editClearImagesBtn-${noteId}`); + const editClearImagesBtn = editImagePreviewHeader.querySelector( + `#editClearImagesBtn-${noteId}` + ); editClearImagesBtn.addEventListener("click", function () { editSelectedImages.length = 0; updateEditImagePreview(); @@ -1180,50 +1234,55 @@ function addImageEventListeners() { if (imageElement._clickHandler) { return; // Пропускаем, если обработчик уже добавлен } - + // Создаем новый обработчик imageElement._clickHandler = function (event) { event.preventDefault(); event.stopPropagation(); const imageSrc = this.dataset.imageSrc; - console.log('Image clicked, src:', imageSrc); // Для отладки + console.log("Image clicked, src:", imageSrc); // Для отладки if (imageSrc) { showImageModal(imageSrc); } }; - + imageElement.addEventListener("click", imageElement._clickHandler); }); // Обработчики для кнопок удаления изображений - document.querySelectorAll(".remove-note-image-btn").forEach((buttonElement) => { - // Удаляем старые обработчики, если они есть - if (buttonElement._clickHandler) { - buttonElement.removeEventListener("click", buttonElement._clickHandler); - } - - // Создаем новый обработчик - buttonElement._clickHandler = async function (event) { - event.preventDefault(); - event.stopPropagation(); - - const noteId = this.dataset.noteId; - const imageId = this.dataset.imageId; - - if (noteId && imageId && confirm("Вы уверены, что хотите удалить это изображение?")) { - const success = await deleteNoteImage(noteId, imageId); - if (success) { - await loadNotes(true); // Перезагружаем заметки - } else { - alert("Ошибка удаления изображения"); - } + document + .querySelectorAll(".remove-note-image-btn") + .forEach((buttonElement) => { + // Удаляем старые обработчики, если они есть + if (buttonElement._clickHandler) { + buttonElement.removeEventListener("click", buttonElement._clickHandler); } - }; - - buttonElement.addEventListener("click", buttonElement._clickHandler); - }); -} + // Создаем новый обработчик + buttonElement._clickHandler = async function (event) { + event.preventDefault(); + event.stopPropagation(); + + const noteId = this.dataset.noteId; + const imageId = this.dataset.imageId; + + if ( + noteId && + imageId && + confirm("Вы уверены, что хотите удалить это изображение?") + ) { + const success = await deleteNoteImage(noteId, imageId); + if (success) { + await loadNotes(true); // Перезагружаем заметки + } else { + alert("Ошибка удаления изображения"); + } + } + }; + + buttonElement.addEventListener("click", buttonElement._clickHandler); + }); +} // Функция сохранения заметки (вынесена отдельно для повторного использования) async function saveNote() { @@ -1232,14 +1291,17 @@ async function saveNote() { const { date, time } = getFormattedDateTime(); // Показываем индикатор сохранения для мобильных устройств - 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; - + 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; + let savingIndicator = null; if (isMobile) { - savingIndicator = document.createElement('div'); - savingIndicator.id = 'mobile-saving-indicator'; + savingIndicator = document.createElement("div"); + savingIndicator.id = "mobile-saving-indicator"; savingIndicator.style.cssText = ` position: fixed; top: 50%; @@ -1255,7 +1317,11 @@ async function saveNote() { `; savingIndicator.innerHTML = `
💾 Сохранение заметки...
- ${selectedImages.length > 0 ? `
+ ${selectedImages.length} изображений
` : ''} + ${ + selectedImages.length > 0 + ? `
+ ${selectedImages.length} изображений
` + : "" + } `; document.body.appendChild(savingIndicator); } @@ -1296,10 +1362,10 @@ async function saveNote() { updateImagePreview(); imageInput.value = ""; await loadNotes(true); - + // Показываем уведомление об успешном сохранении if (isMobile) { - const successDiv = document.createElement('div'); + const successDiv = document.createElement("div"); successDiv.style.cssText = ` position: fixed; top: 20px; @@ -1312,23 +1378,24 @@ async function saveNote() { z-index: 10000; font-size: 14px; `; - successDiv.textContent = '✅ Заметка сохранена!'; + successDiv.textContent = "✅ Заметка сохранена!"; document.body.appendChild(successDiv); - + setTimeout(() => { successDiv.remove(); }, 3000); } - } catch (error) { console.error("Ошибка:", error); - + // Удаляем индикатор сохранения в случае ошибки - const savingIndicator = document.getElementById('mobile-saving-indicator'); + const savingIndicator = document.getElementById( + "mobile-saving-indicator" + ); if (savingIndicator) { savingIndicator.remove(); } - + alert("Ошибка сохранения заметки"); } } @@ -1352,7 +1419,10 @@ document.addEventListener("DOMContentLoaded", function () { loadUserInfo(); loadNotes(); updateFilterIndicator(); - + + // Инициализируем lazy loading для изображений + initLazyLoading(); + // Добавляем обработчик для кнопки выхода setupLogoutHandler(); }); @@ -1360,41 +1430,41 @@ document.addEventListener("DOMContentLoaded", function () { // Функция для настройки обработчика выхода function setupLogoutHandler() { const logoutForms = document.querySelectorAll('form[action="/logout"]'); - logoutForms.forEach(form => { - form.addEventListener('submit', function(e) { + logoutForms.forEach((form) => { + form.addEventListener("submit", function (e) { // Очищаем localStorage перед выходом - localStorage.removeItem('isAuthenticated'); - localStorage.removeItem('username'); + localStorage.removeItem("isAuthenticated"); + localStorage.removeItem("username"); }); }); } // Функция для проверки аутентификации async function checkAuthentication() { - const isAuthenticated = localStorage.getItem('isAuthenticated'); - - if (isAuthenticated !== 'true') { + const isAuthenticated = localStorage.getItem("isAuthenticated"); + + if (isAuthenticated !== "true") { // Если пользователь не аутентифицирован, перенаправляем на страницу входа window.location.href = "/"; return; } - + // Проверяем, что сессия на сервере еще действительна try { const response = await fetch("/api/auth/status"); if (!response.ok) { // Если сессия недействительна, очищаем localStorage и перенаправляем - localStorage.removeItem('isAuthenticated'); - localStorage.removeItem('username'); + localStorage.removeItem("isAuthenticated"); + localStorage.removeItem("username"); window.location.href = "/"; return; } - + const authData = await response.json(); if (!authData.authenticated) { // Если сервер говорит, что пользователь не аутентифицирован - localStorage.removeItem('isAuthenticated'); - localStorage.removeItem('username'); + localStorage.removeItem("isAuthenticated"); + localStorage.removeItem("username"); window.location.href = "/"; return; } @@ -1432,6 +1502,16 @@ async function loadUserInfo() { if (userAvatarContainer) { userAvatarContainer.style.display = "none"; } + + // Применяем цветовой акцент пользователя + if (user.accent_color) { + document.documentElement.style.setProperty( + "--accent-color", + user.accent_color + ); + } else { + document.documentElement.style.setProperty("--accent-color", "#007bff"); + } } } catch (error) { console.error("Ошибка загрузки информации о пользователе:", error); @@ -1517,7 +1597,10 @@ function renderCalendar() { } // Добавляем обработчик клика - dayDiv.addEventListener("click", async (event) => await handleDayClick(event)); + dayDiv.addEventListener( + "click", + async (event) => await handleDayClick(event) + ); calendarDays.appendChild(dayDiv); } @@ -1554,7 +1637,10 @@ function renderCalendar() { } // Добавляем обработчик клика - dayDiv.addEventListener("click", async (event) => await handleDayClick(event)); + dayDiv.addEventListener( + "click", + async (event) => await handleDayClick(event) + ); calendarDays.appendChild(dayDiv); } @@ -1586,7 +1672,10 @@ function renderCalendar() { } // Добавляем обработчик клика - dayDiv.addEventListener("click", async (event) => await handleDayClick(event)); + dayDiv.addEventListener( + "click", + async (event) => await handleDayClick(event) + ); calendarDays.appendChild(dayDiv); } @@ -1827,7 +1916,10 @@ function renderCalendarMobile() { } // Добавляем обработчик клика - dayDiv.addEventListener("click", async (event) => await handleDayClickMobile(event)); + dayDiv.addEventListener( + "click", + async (event) => await handleDayClickMobile(event) + ); calendarDays.appendChild(dayDiv); } @@ -1864,7 +1956,10 @@ function renderCalendarMobile() { } // Добавляем обработчик клика - dayDiv.addEventListener("click", async (event) => await handleDayClickMobile(event)); + dayDiv.addEventListener( + "click", + async (event) => await handleDayClickMobile(event) + ); calendarDays.appendChild(dayDiv); } @@ -1896,7 +1991,10 @@ function renderCalendarMobile() { } // Добавляем обработчик клика - dayDiv.addEventListener("click", async (event) => await handleDayClickMobile(event)); + dayDiv.addEventListener( + "click", + async (event) => await handleDayClickMobile(event) + ); calendarDays.appendChild(dayDiv); } @@ -1946,7 +2044,10 @@ function renderTagsMobile() { // Добавляем обработчики кликов для тегов tagsContainer.querySelectorAll(".tag").forEach((tagElement) => { - tagElement.addEventListener("click", async (event) => await handleTagClickMobile(event)); + tagElement.addEventListener( + "click", + async (event) => await handleTagClickMobile(event) + ); }); } diff --git a/public/brave-pwa-test.html b/public/brave-pwa-test.html deleted file mode 100644 index cfb2986..0000000 --- a/public/brave-pwa-test.html +++ /dev/null @@ -1,333 +0,0 @@ - - - - - - Тест PWA для Brave - NoteJS - - - -

🔍 Диагностика PWA для Brave браузера

- -
-

Информация о браузере:

-
-
- -
-

📋 Проверка требований PWA

-
-
- -
-

🔧 Тестирование установки

- - -
-
- -
-

📱 Инструкции для Brave

-
-
- -
-

🐛 Отладочная информация

- - -
- - - - diff --git a/public/index.html b/public/index.html index e4de523..a1b2dd3 100644 --- a/public/index.html +++ b/public/index.html @@ -2,7 +2,10 @@ - + Вход в систему заметок @@ -156,56 +159,6 @@ }); }); } - - // Обработка установки PWA - let deferredPrompt; - window.addEventListener("beforeinstallprompt", (e) => { - console.log("beforeinstallprompt event fired"); - - // Определяем браузер - const isBrave = navigator.userAgent.includes('Brave') || - (navigator.brave && await navigator.brave.isBrave()); - 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; - - // Для Brave на мобильных устройствах не предотвращаем стандартное поведение - if (!isMobile && !isBrave) { - e.preventDefault(); - } - deferredPrompt = e; - - // Показываем кнопку установки на мобильных устройствах или в Brave - if (isMobile || isBrave) { - const installButton = document.createElement("button"); - installButton.textContent = "Установить приложение"; - installButton.className = "btnSave"; - installButton.style.marginTop = "10px"; - installButton.style.width = "100%"; - - installButton.addEventListener("click", async () => { - if (deferredPrompt) { - deferredPrompt.prompt(); - const choiceResult = await deferredPrompt.userChoice; - if (choiceResult.outcome === "accepted") { - console.log("Пользователь установил приложение"); - } - deferredPrompt = null; - installButton.remove(); - } - }); - - document.querySelector(".auth-link").appendChild(installButton); - } - }); - - // Обработка успешной установки - window.addEventListener("appinstalled", () => { - console.log("PWA установлено успешно"); - }); diff --git a/public/mobile-install-test.html b/public/mobile-install-test.html deleted file mode 100644 index d0b6361..0000000 --- a/public/mobile-install-test.html +++ /dev/null @@ -1,230 +0,0 @@ - - - - - - Тест установки PWA на мобильном - - - - - -
-

📱 Тест установки PWA на мобильном

- -
-

Инструкции:

-

1. Откройте эту страницу на мобильном устройстве

-

2. Должен появиться нативный баннер установки браузера

-

3. Если баннер не появился, используйте кнопки ниже

-
- -
- -
- - - -
- -
-

Отладочная информация:

-
-
-
- - - - diff --git a/public/mobile-pwa-test.html b/public/mobile-pwa-test.html deleted file mode 100644 index a5b772b..0000000 --- a/public/mobile-pwa-test.html +++ /dev/null @@ -1,415 +0,0 @@ - - - - - - Тест PWA для мобильных устройств - NoteJS - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

📱 Тест PWA для мобильных устройств

- -
-

Проверка требований PWA

-
-
- HTTPS или localhost - Проверка... -
-
- Service Worker - Проверка... -
-
- Manifest - Проверка... -
-
- Иконки - Проверка... -
-
- Уже установлено - Проверка... -
-
-
- -
-

Установка приложения

-
- -
-
- -
-

Инструкции по установке

-
-

Android Chrome/Edge:

-
    -
  1. Откройте сайт в Chrome или Edge
  2. -
  3. Нажмите кнопку "Установить приложение" выше
  4. -
  5. Или нажмите меню (⋮) → "Установить приложение"
  6. -
  7. Следуйте инструкциям браузера
  8. -
-
- -
-

iOS Safari:

-
    -
  1. Откройте сайт в Safari
  2. -
  3. Нажмите кнопку "Поделиться" (□↗)
  4. -
  5. Выберите "На экран Домой"
  6. -
  7. Нажмите "Добавить"
  8. -
-
- -
-

Другие браузеры:

-
    -
  1. Найдите опцию "Установить" в меню браузера
  2. -
  3. Или используйте "Добавить на главный экран"
  4. -
-
-
- -
-

Отладочная информация

- - -
- -
-

Действия

- - - -
-
- - - - diff --git a/public/notes.html b/public/notes.html index 6ad650d..89353b7 100644 --- a/public/notes.html +++ b/public/notes.html @@ -2,15 +2,24 @@ - + Заметки - NoteJS - + - + - + @@ -18,28 +27,58 @@ - + - - + + - - - - - + + + + + - + - + - + @@ -192,6 +231,7 @@ id="user-avatar" src="" alt="Аватар" + loading="lazy" style=" width: 32px; height: 32px; @@ -233,7 +273,11 @@ - @@ -243,19 +287,35 @@ id="noteInput" placeholder="Ваша заметка..." > - + - - + + - - + diff --git a/public/style.css b/public/style.css index c7e70f5..4a01185 100644 --- a/public/style.css +++ b/public/style.css @@ -1,3 +1,7 @@ +:root { + --accent-color: #007bff; +} + body { font-family: "Open Sans", sans-serif; padding: 0; @@ -175,7 +179,7 @@ header { .search-input:focus { outline: none; - border-color: #007bff; + border-color: var(--accent-color, #007bff); background-color: white; box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); } @@ -205,10 +209,10 @@ header { margin-top: 5px; padding: 4px 10px; background-color: #e7f3ff; - border: 1px solid #007bff; + border: 1px solid var(--accent-color, #007bff); border-radius: 15px; font-size: 12px; - color: #007bff; + color: var(--accent-color, #007bff); font-weight: 500; } @@ -262,7 +266,7 @@ header { } .auth-link a { - color: #007bff; + color: var(--accent-color, #007bff); text-decoration: none; font-weight: 500; } @@ -310,7 +314,7 @@ header { .form-group input:focus { outline: none; - border-color: #007bff; + border-color: var(--accent-color, #007bff); } .error-message { @@ -360,9 +364,9 @@ textarea:focus { } .btnSave:hover { - background-color: #007bff; + background-color: var(--accent-color, #007bff); color: white; - border-color: #007bff; + border-color: var(--accent-color, #007bff); } .date { @@ -412,7 +416,7 @@ textarea:focus { background-color: #f8f9fa; border: 1px solid #ddd; border-radius: 5px; - color: #007bff; + color: var(--accent-color, #007bff); font-size: 14px; cursor: pointer; transition: all 0.3s ease; @@ -420,9 +424,9 @@ textarea:focus { } .show-more-btn:hover { - background-color: #007bff; + background-color: var(--accent-color, #007bff); color: white; - border-color: #007bff; + border-color: var(--accent-color, #007bff); } /* Убираем стандартные отступы для абзацев */ @@ -466,7 +470,7 @@ textarea:focus { /* Стили для ссылок */ .textNote a { - color: #007bff; + color: var(--accent-color, #007bff); text-decoration: none; word-wrap: break-word; overflow-wrap: break-word; @@ -478,7 +482,7 @@ textarea:focus { /* Стили для цитат */ .textNote blockquote { - border-left: 4px solid #007bff; + border-left: 4px solid var(--accent-color, #007bff); padding-left: 16px; margin: 10px 0; color: #555; @@ -599,7 +603,7 @@ textarea:focus { height: 150px; border-radius: 50%; object-fit: cover; - border: 3px solid #007bff; + border: 3px solid var(--accent-color, #007bff); } .avatar-placeholder { @@ -634,9 +638,9 @@ textarea:focus { } .btn-upload:hover { - background-color: #007bff; + background-color: var(--accent-color, #007bff); color: white; - border-color: #007bff; + border-color: var(--accent-color, #007bff); } .btn-delete { @@ -700,16 +704,16 @@ textarea:focus { background-color: #f8f9fa; border-radius: 5px; font-size: 14px; - color: #007bff; + color: var(--accent-color, #007bff); text-decoration: none; transition: all 0.3s ease; display: inline-block; } .back-btn:hover { - background-color: #007bff; + background-color: var(--accent-color, #007bff); color: white; - border-color: #007bff; + border-color: var(--accent-color, #007bff); } .username-clickable { @@ -718,7 +722,7 @@ textarea:focus { } .username-clickable:hover { - color: #007bff; + color: var(--accent-color, #007bff); text-decoration: underline; } @@ -765,7 +769,7 @@ textarea:focus { border: none; font-size: 18px; cursor: pointer; - color: #007bff; + color: var(--accent-color, #007bff); padding: 0 3px; transition: color 0.3s ease; } @@ -815,7 +819,7 @@ textarea:focus { } .calendar-day.today { - background-color: #007bff; + background-color: var(--accent-color, #007bff); color: white; font-weight: bold; } @@ -880,8 +884,8 @@ textarea:focus { display: inline-block; padding: 4px 8px; background-color: #e7f3ff; - color: #007bff; - border: 1px solid #007bff; + color: var(--accent-color, #007bff); + border: 1px solid var(--accent-color, #007bff); border-radius: 12px; font-size: 10px; font-weight: 500; @@ -891,12 +895,12 @@ textarea:focus { } .tag:hover { - background-color: #007bff; + background-color: var(--accent-color, #007bff); color: white; } .tag.active { - background-color: #007bff; + background-color: var(--accent-color, #007bff); color: white; font-weight: bold; } @@ -912,8 +916,8 @@ textarea:focus { display: inline-block; padding: 2px 6px; background-color: #e7f3ff; - color: #007bff; - border: 1px solid #007bff; + color: var(--accent-color, #007bff); + border: 1px solid var(--accent-color, #007bff); border-radius: 8px; font-size: 12px; font-weight: 500; @@ -926,7 +930,7 @@ textarea:focus { } .textNote .tag-in-note:hover { - background-color: #007bff; + background-color: var(--accent-color, #007bff); color: white; transform: translateY(-1px); box-shadow: 0 2px 4px rgba(0, 123, 255, 0.3); @@ -1051,7 +1055,7 @@ textarea:focus { border: none; font-size: 16px; cursor: pointer; - color: #007bff; + color: var(--accent-color, #007bff); padding: 0 3px; transition: color 0.3s ease; } @@ -1089,7 +1093,7 @@ textarea:focus { } .mobile-sidebar .calendar-day.today { - background-color: #007bff; + background-color: var(--accent-color, #007bff); color: white; font-weight: bold; } @@ -1133,7 +1137,7 @@ textarea:focus { .mobile-sidebar .search-input:focus { outline: none; - border-color: #007bff; + border-color: var(--accent-color, #007bff); background-color: white; box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); } @@ -1163,8 +1167,8 @@ textarea:focus { display: inline-block; padding: 4px 8px; background-color: #e7f3ff; - color: #007bff; - border: 1px solid #007bff; + color: var(--accent-color, #007bff); + border: 1px solid var(--accent-color, #007bff); border-radius: 12px; font-size: 9px; font-weight: 500; @@ -1175,12 +1179,12 @@ textarea:focus { } .mobile-sidebar .tag:hover { - background-color: #007bff; + background-color: var(--accent-color, #007bff); color: white; } .mobile-sidebar .tag.active { - background-color: #007bff; + background-color: var(--accent-color, #007bff); color: white; font-weight: bold; } @@ -1223,7 +1227,7 @@ textarea:focus { .container-leftside { display: none !important; } - + /* На мобильных устройствах меняем направление flex и центрируем */ body { flex-direction: column; @@ -1232,7 +1236,7 @@ textarea:focus { justify-content: flex-start; align-items: center; } - + /* Центральный контейнер занимает всю ширину, но центрируется */ .center { width: 100%; @@ -1240,7 +1244,7 @@ textarea:focus { margin: 0 auto; margin-top: 60px; } - + /* Адаптируем контейнер заметок */ .container { width: 100%; @@ -1249,7 +1253,7 @@ textarea:focus { padding: 10px; box-sizing: border-box; } - + /* Адаптируем заголовок заметок */ .notes-header { flex-direction: column; @@ -1257,18 +1261,18 @@ textarea:focus { gap: 10px; width: 100%; } - + .notes-header-left { width: 100%; } - + .user-info { width: 100%; justify-content: space-between; flex-wrap: wrap; gap: 10px; } - + /* Адаптируем кнопки markdown */ .markdown-buttons { display: flex; @@ -1277,7 +1281,7 @@ textarea:focus { width: 100%; justify-content: flex-start; } - + .markdown-buttons .btnMarkdown { flex: 0 1 auto; min-width: auto; @@ -1285,22 +1289,22 @@ textarea:focus { padding: 8px 12px; font-size: 14px; } - + /* Адаптируем textarea */ textarea { min-height: 100px; } - + /* Адаптируем кнопку сохранения */ .save-button-container { width: 100%; flex-direction: column; } - + .btnSave { width: 100%; } - + /* Адаптируем footer */ .footer { position: relative; @@ -1407,14 +1411,14 @@ textarea:focus { object-fit: cover; border-radius: 6px; margin: 10px 0; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); + 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); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); } .note-image::after { @@ -1423,7 +1427,7 @@ textarea:focus { top: 50%; left: 50%; transform: translate(-50%, -50%); - background: rgba(0,0,0,0.7); + background: rgba(0, 0, 0, 0.7); color: white; padding: 8px; border-radius: 50%; @@ -1489,7 +1493,7 @@ textarea:focus { top: 0; width: 100%; height: 100%; - background-color: rgba(0,0,0,0.9); + background-color: rgba(0, 0, 0, 0.9); cursor: pointer; } @@ -1522,37 +1526,91 @@ textarea:focus { .image-preview-list { justify-content: center; } - + .note-images-container { justify-content: center; } - + .image-preview-item img { width: 80px; height: 80px; } - + /* Улучшения для мобильных устройств */ .markdown-buttons .btnMarkdown { min-height: 48px; /* Увеличиваем высоту для touch */ padding: 8px 12px; margin: 2px; } - + .image-preview-item .remove-image-btn { width: 28px; /* Еще больше для мобильных */ height: 28px; font-size: 16px; } - + .clear-images-btn { min-height: 44px; /* Минимальная высота для touch */ padding: 8px 16px; font-size: 14px; } - + .note-image { width: 120px; height: 120px; } } + +/* Стили для выбора цветового акцента */ +.accent-color-picker { + display: flex; + gap: 10px; + margin-bottom: 10px; + flex-wrap: wrap; +} + +.color-option { + width: 40px; + height: 40px; + border-radius: 50%; + cursor: pointer; + border: 3px solid transparent; + transition: all 0.3s ease; + position: relative; +} + +.color-option:hover { + transform: scale(1.1); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.color-option.selected { + border-color: #333; + transform: scale(1.1); + box-shadow: 0 0 0 2px white, 0 0 0 4px #333; +} + +.color-option::after { + content: "✓"; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: white; + font-weight: bold; + font-size: 18px; + opacity: 0; + transition: opacity 0.2s ease; +} + +.color-option.selected::after { + opacity: 1; +} + +#accentColor { + width: 60px; + height: 40px; + border: none; + border-radius: 5px; + cursor: pointer; +} diff --git a/public/test-mobile-upload.html b/public/test-mobile-upload.html deleted file mode 100644 index aa48d15..0000000 --- a/public/test-mobile-upload.html +++ /dev/null @@ -1,576 +0,0 @@ - - - - - - Тест загрузки изображений на мобильных - NoteJS - - - - - - - - - - - - - - - - - - - - - - -
-

📱 Тест загрузки изображений на мобильных

- -
- Определение устройства... -
- -
-

Тест загрузки файлов

- -
-
📷
-
Нажмите для выбора изображений
-
или перетащите файлы сюда
- -
- -
- - - -
- -
-

Результаты тестов

-
-
- -
-

Отладочная информация

- - -
-
- - - - diff --git a/public/test-pwa.html b/public/test-pwa.html deleted file mode 100644 index a4bdbb9..0000000 --- a/public/test-pwa.html +++ /dev/null @@ -1,287 +0,0 @@ - - - - - - Тест PWA - NoteJS - - - - - - - - - - - - - - - - - - - - - -
-

🔧 Тест PWA для NoteJS

- -
-
Проверяем требования PWA...
-
- -
- - - -
- -
-

Отладочная информация:

-
-
-
- - - - - - - diff --git a/server.js b/server.js index ffc41b6..c947abb 100644 --- a/server.js +++ b/server.js @@ -133,22 +133,22 @@ app.use( app.use(express.static(path.join(__dirname, "public"))); // PWA файлы с правильными заголовками -app.get('/manifest.json', (req, res) => { - res.setHeader('Content-Type', 'application/manifest+json'); - res.setHeader('Cache-Control', 'public, max-age=86400'); // 24 часа - res.sendFile(path.join(__dirname, 'public', 'manifest.json')); +app.get("/manifest.json", (req, res) => { + res.setHeader("Content-Type", "application/manifest+json"); + res.setHeader("Cache-Control", "public, max-age=86400"); // 24 часа + res.sendFile(path.join(__dirname, "public", "manifest.json")); }); -app.get('/sw.js', (req, res) => { - res.setHeader('Content-Type', 'application/javascript'); - res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); - res.sendFile(path.join(__dirname, 'public', 'sw.js')); +app.get("/sw.js", (req, res) => { + res.setHeader("Content-Type", "application/javascript"); + res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); + res.sendFile(path.join(__dirname, "public", "sw.js")); }); -app.get('/browserconfig.xml', (req, res) => { - res.setHeader('Content-Type', 'application/xml'); - res.setHeader('Cache-Control', 'public, max-age=86400'); // 24 часа - res.sendFile(path.join(__dirname, 'public', 'browserconfig.xml')); +app.get("/browserconfig.xml", (req, res) => { + res.setHeader("Content-Type", "application/xml"); + res.setHeader("Cache-Control", "public, max-age=86400"); // 24 часа + res.sendFile(path.join(__dirname, "public", "browserconfig.xml")); }); // Парсинг тела запроса @@ -161,14 +161,14 @@ app.use( store: new SQLiteStore({ db: "sessions.db", table: "sessions", - dir: path.join(__dirname, "database") + dir: path.join(__dirname, "database"), }), secret: process.env.SESSION_SECRET || "default-secret", resave: false, saveUninitialized: false, - cookie: { + cookie: { secure: false, // в продакшене установить true с HTTPS - maxAge: 7 * 24 * 60 * 60 * 1000 // 7 дней + maxAge: 7 * 24 * 60 * 60 * 1000, // 7 дней }, }) ); @@ -232,7 +232,10 @@ function createTables() { db.run(createNoteImagesTable, (err) => { if (err) { - console.error("Ошибка создания таблицы изображений заметок:", err.message); + console.error( + "Ошибка создания таблицы изображений заметок:", + err.message + ); } else { console.log("Таблица изображений заметок готова"); } @@ -244,6 +247,7 @@ function createTables() { } else { console.log("Таблица пользователей готова"); createIndexes(); + runMigrations(); } }); } @@ -255,7 +259,7 @@ function createIndexes() { "CREATE INDEX IF NOT EXISTS idx_notes_created_at ON notes(created_at)", "CREATE INDEX IF NOT EXISTS idx_notes_date ON notes(date)", "CREATE INDEX IF NOT EXISTS idx_note_images_note_id ON note_images(note_id)", - "CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)" + "CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)", ]; indexes.forEach((indexSql, i) => { @@ -269,6 +273,34 @@ function createIndexes() { }); } +// Миграции базы данных +function runMigrations() { + // Проверяем существование колонки accent_color и добавляем её если нужно + db.all("PRAGMA table_info(users)", (err, columns) => { + if (err) { + console.error("Ошибка проверки структуры таблицы users:", err.message); + return; + } + + const hasAccentColor = columns.some((col) => col.name === "accent_color"); + if (!hasAccentColor) { + db.run( + "ALTER TABLE users ADD COLUMN accent_color TEXT DEFAULT '#007bff'", + (err) => { + if (err) { + console.error( + "Ошибка добавления колонки accent_color:", + err.message + ); + } else { + console.log("Колонка accent_color добавлена в таблицу users"); + } + } + ); + } + }); +} + // Middleware для аутентификации function requireAuth(req, res, next) { if (req.session.authenticated) { @@ -402,10 +434,10 @@ app.post("/login", async (req, res) => { // API для проверки статуса аутентификации app.get("/api/auth/status", (req, res) => { if (req.session.authenticated && req.session.userId) { - res.json({ - authenticated: true, + res.json({ + authenticated: true, userId: req.session.userId, - username: req.session.username + username: req.session.username, }); } else { res.status(401).json({ authenticated: false }); @@ -418,7 +450,8 @@ app.get("/api/user", requireAuth, (req, res) => { return res.status(401).json({ error: "Не аутентифицирован" }); } - const sql = "SELECT username, email, avatar FROM users WHERE id = ?"; + const sql = + "SELECT username, email, avatar, accent_color FROM users WHERE id = ?"; db.get(sql, [req.session.userId], (err, user) => { if (err) { console.error("Ошибка получения данных пользователя:", err.message); @@ -492,13 +525,13 @@ app.get("/api/notes/search", requireAuth, (req, res) => { console.error("Ошибка поиска заметок:", err.message); return res.status(500).json({ error: "Ошибка сервера" }); } - + // Парсим JSON строки изображений - const notesWithImages = rows.map(row => ({ + const notesWithImages = rows.map((row) => ({ ...row, - images: row.images === '[]' ? [] : JSON.parse(row.images) + images: row.images === "[]" ? [] : JSON.parse(row.images), })); - + res.json(notesWithImages); }); }); @@ -534,13 +567,13 @@ app.get("/api/notes", requireAuth, (req, res) => { console.error("Ошибка получения заметок:", err.message); return res.status(500).json({ error: "Ошибка сервера" }); } - + // Парсим JSON строки изображений - const notesWithImages = rows.map(row => ({ + const notesWithImages = rows.map((row) => ({ ...row, - images: row.images === '[]' ? [] : JSON.parse(row.images) + images: row.images === "[]" ? [] : JSON.parse(row.images), })); - + res.json(notesWithImages); }); }); @@ -738,12 +771,14 @@ app.post( completed++; if (completed === files.length) { if (uploadedImages.length === 0) { - return res.status(500).json({ error: "Не удалось загрузить изображения" }); + return res + .status(500) + .json({ error: "Не удалось загрузить изображения" }); } - res.json({ - success: true, + res.json({ + success: true, message: `Загружено ${uploadedImages.length} изображений`, - images: uploadedImages + images: uploadedImages, }); } }); @@ -812,7 +847,8 @@ app.delete("/api/notes/:noteId/images/:imageId", requireAuth, (req, res) => { } // Получаем информацию об изображении - const getImageSql = "SELECT file_path FROM note_images WHERE id = ? AND note_id = ?"; + 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); @@ -854,7 +890,8 @@ app.get("/profile", requireAuth, (req, res) => { // API для обновления профиля app.put("/api/user/profile", requireAuth, async (req, res) => { - const { username, email, currentPassword, newPassword } = req.body; + const { username, email, currentPassword, newPassword, accent_color } = + req.body; const userId = req.session.userId; try { @@ -914,6 +951,11 @@ app.put("/api/user/profile", requireAuth, async (req, res) => { params.push(email || null); } + if (accent_color !== undefined) { + updateFields.push("accent_color = ?"); + params.push(accent_color || "#007bff"); + } + if (newPassword) { const hashedPassword = await bcrypt.hash(newPassword, 10); updateFields.push("password = ?"); diff --git a/test_images.html b/test_images.html deleted file mode 100644 index a0bd0d3..0000000 --- a/test_images.html +++ /dev/null @@ -1,292 +0,0 @@ - - - - - - Тест загрузки изображений - - - -

Тест функциональности загрузки изображений

- -
-

1. Регистрация тестового пользователя

- -
-
- -
-

2. Вход в систему

- -
-
- -
-

3. Создание заметки с изображениями

- - -
-
-
- -
-

4. Получение изображений заметки

- - -
-
- -
-

5. Удаление изображения

- - - -
-
- - - -