Добавлена поддержка PWA для улучшения пользовательского опыта

- Реализованы маршруты для обслуживания PWA файлов: manifest.json, sw.js и browserconfig.xml
- Добавлены мета-теги и иконки для PWA в HTML страницах
- Внедрена регистрация сервисного работника для кэширования и оффлайн-доступа
- Обновлены страницы входа, регистрации, профиля и заметок для поддержки PWA
This commit is contained in:
Fovway 2025-10-20 08:53:59 +07:00
parent 431d51c483
commit 4600dc61b7
10 changed files with 709 additions and 3 deletions

9
public/browserconfig.xml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/icons/icon-152x152.png"/>
<TileColor>#007bff</TileColor>
</tile>
</msapplication>
</browserconfig>

41
public/icon.svg Normal file
View File

@ -0,0 +1,41 @@
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<!-- Фон -->
<rect width="512" height="512" rx="80" fill="#007bff"/>
<!-- Основная иконка заметки -->
<rect x="80" y="100" width="280" height="360" rx="20" fill="white" stroke="#e3f2fd" stroke-width="4"/>
<!-- Заголовок заметки -->
<rect x="100" y="120" width="240" height="20" rx="10" fill="#e3f2fd"/>
<!-- Строки текста -->
<rect x="100" y="160" width="200" height="12" rx="6" fill="#e3f2fd"/>
<rect x="100" y="180" width="180" height="12" rx="6" fill="#e3f2fd"/>
<rect x="100" y="200" width="220" height="12" rx="6" fill="#e3f2fd"/>
<!-- Список -->
<circle cx="110" cy="240" r="4" fill="#007bff"/>
<rect x="125" y="236" width="120" height="8" rx="4" fill="#e3f2fd"/>
<circle cx="110" cy="260" r="4" fill="#007bff"/>
<rect x="125" y="256" width="100" height="8" rx="4" fill="#e3f2fd"/>
<circle cx="110" cy="280" r="4" fill="#007bff"/>
<rect x="125" y="276" width="140" height="8" rx="4" fill="#e3f2fd"/>
<!-- Код блок -->
<rect x="100" y="320" width="240" height="60" rx="8" fill="#f5f5f5" stroke="#e0e0e0" stroke-width="2"/>
<rect x="110" y="330" width="40" height="6" rx="3" fill="#007bff"/>
<rect x="110" y="340" width="60" height="6" rx="3" fill="#666"/>
<rect x="110" y="350" width="50" height="6" rx="3" fill="#666"/>
<rect x="110" y="360" width="30" height="6" rx="3" fill="#007bff"/>
<!-- Тег -->
<rect x="100" y="400" width="60" height="20" rx="10" fill="#e7f3ff" stroke="#007bff" stroke-width="2"/>
<text x="130" y="413" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" font-weight="bold" fill="#007bff">#tag</text>
<!-- Дополнительные элементы для живости -->
<circle cx="400" cy="150" r="8" fill="white" opacity="0.3"/>
<circle cx="450" cy="200" r="6" fill="white" opacity="0.2"/>
<circle cx="420" cy="300" r="10" fill="white" opacity="0.25"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -4,7 +4,30 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Вход в систему заметок</title>
<!-- PWA Meta Tags -->
<meta name="description" content="NoteJS - современная система заметок с поддержкой Markdown, изображений, тегов и календаря" />
<meta name="theme-color" content="#007bff" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="NoteJS" />
<meta name="msapplication-TileColor" content="#007bff" />
<meta name="msapplication-config" content="/browserconfig.xml" />
<!-- Icons -->
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link rel="icon" type="image/png" sizes="32x32" href="/icons/icon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/icons/icon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/icons/icon-192x192.png" />
<link rel="mask-icon" href="/icon.svg" color="#007bff" />
<!-- Manifest -->
<link rel="manifest" href="/manifest.json" />
<!-- Styles -->
<link rel="stylesheet" href="/style.css" />
<!-- Scripts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/iconify/2.0.0/iconify.min.js"></script>
</head>
<body>
@ -50,5 +73,66 @@
<p>Создатель: <span>Fovway</span></p>
</div>
<script src="/login.js"></script>
<!-- PWA Service Worker Registration -->
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then((registration) => {
console.log('SW зарегистрирован успешно:', registration.scope);
// Проверяем обновления
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// Новый контент доступен, можно показать уведомление
if (confirm('Доступна новая версия приложения. Обновить?')) {
newWorker.postMessage({ type: 'SKIP_WAITING' });
window.location.reload();
}
}
});
});
})
.catch((error) => {
console.log('Ошибка регистрации SW:', error);
});
});
}
// Обработка установки PWA
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
// Показываем кнопку установки
const installButton = document.createElement('button');
installButton.textContent = 'Установить приложение';
installButton.className = 'btnSave';
installButton.style.marginTop = '10px';
installButton.style.width = '100%';
installButton.addEventListener('click', () => {
deferredPrompt.prompt();
deferredPrompt.userChoice.then((choiceResult) => {
if (choiceResult.outcome === 'accepted') {
console.log('Пользователь установил приложение');
}
deferredPrompt = null;
installButton.remove();
});
});
document.querySelector('.auth-link').appendChild(installButton);
});
// Обработка успешной установки
window.addEventListener('appinstalled', () => {
console.log('PWA установлено успешно');
});
</script>
</body>
</html>

47
public/logo.svg Normal file
View File

@ -0,0 +1,47 @@
<svg width="400" height="120" viewBox="0 0 400 120" xmlns="http://www.w3.org/2000/svg">
<!-- Фон -->
<rect width="400" height="120" fill="white"/>
<!-- Иконка логотипа -->
<g transform="translate(20, 20)">
<!-- Основная иконка заметки -->
<rect x="0" y="0" width="60" height="80" rx="8" fill="#007bff" stroke="#0056b3" stroke-width="2"/>
<!-- Заголовок заметки -->
<rect x="8" y="8" width="44" height="6" rx="3" fill="white"/>
<!-- Строки текста -->
<rect x="8" y="20" width="36" height="4" rx="2" fill="white" opacity="0.8"/>
<rect x="8" y="28" width="32" height="4" rx="2" fill="white" opacity="0.8"/>
<rect x="8" y="36" width="40" height="4" rx="2" fill="white" opacity="0.8"/>
<!-- Список -->
<circle cx="12" cy="50" r="2" fill="white"/>
<rect x="18" y="49" width="20" height="2" rx="1" fill="white" opacity="0.8"/>
<circle cx="12" cy="58" r="2" fill="white"/>
<rect x="18" y="57" width="16" height="2" rx="1" fill="white" opacity="0.8"/>
<!-- Тег -->
<rect x="8" y="68" width="20" height="8" rx="4" fill="white" opacity="0.9"/>
<text x="18" y="73" text-anchor="middle" font-family="Arial, sans-serif" font-size="4" font-weight="bold" fill="#007bff">#</text>
</g>
<!-- Текст логотипа -->
<g transform="translate(100, 0)">
<!-- Название приложения -->
<text x="0" y="45" font-family="Arial, sans-serif" font-size="32" font-weight="bold" fill="#007bff">
NoteJS
</text>
<!-- Подзаголовок -->
<text x="0" y="65" font-family="Arial, sans-serif" font-size="14" fill="#666">
Система заметок
</text>
<!-- Дополнительные элементы -->
<circle cx="280" cy="25" r="3" fill="#007bff" opacity="0.3"/>
<circle cx="320" cy="35" r="2" fill="#007bff" opacity="0.2"/>
<circle cx="300" cy="55" r="4" fill="#007bff" opacity="0.25"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -3,8 +3,30 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Заметки</title>
<title>Заметки - NoteJS</title>
<!-- PWA Meta Tags -->
<meta name="description" content="NoteJS - современная система заметок с поддержкой Markdown, изображений, тегов и календаря" />
<meta name="theme-color" content="#007bff" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="NoteJS" />
<meta name="msapplication-TileColor" content="#007bff" />
<!-- Icons -->
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link rel="icon" type="image/png" sizes="32x32" href="/icons/icon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/icons/icon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/icons/icon-192x192.png" />
<link rel="mask-icon" href="/icon.svg" color="#007bff" />
<!-- Manifest -->
<link rel="manifest" href="/manifest.json" />
<!-- Styles -->
<link rel="stylesheet" href="/style.css?v=4" />
<!-- Scripts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/iconify/2.0.0/iconify.min.js"></script>
</head>
<body>
@ -240,6 +262,10 @@
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/11.1.0/marked.min.js"></script>
<script src="/app.js"></script>
<!-- PWA Script -->
<script src="/pwa.js"></script>
<script>
// Проверяем загрузку Iconify
document.addEventListener("DOMContentLoaded", function () {

View File

@ -3,8 +3,30 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Личный кабинет</title>
<title>Личный кабинет - NoteJS</title>
<!-- PWA Meta Tags -->
<meta name="description" content="NoteJS - современная система заметок с поддержкой Markdown, изображений, тегов и календаря" />
<meta name="theme-color" content="#007bff" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="NoteJS" />
<meta name="msapplication-TileColor" content="#007bff" />
<!-- Icons -->
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link rel="icon" type="image/png" sizes="32x32" href="/icons/icon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/icons/icon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/icons/icon-192x192.png" />
<link rel="mask-icon" href="/icon.svg" color="#007bff" />
<!-- Manifest -->
<link rel="manifest" href="/manifest.json" />
<!-- Styles -->
<link rel="stylesheet" href="/style.css?v=3" />
<!-- Scripts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/iconify/2.0.0/iconify.min.js"></script>
</head>
<body>
@ -132,5 +154,8 @@
</div>
<script src="/profile.js"></script>
<!-- PWA Script -->
<script src="/pwa.js"></script>
</body>
</html>

164
public/pwa.js Normal file
View File

@ -0,0 +1,164 @@
// PWA Service Worker Registration и установка
class PWAManager {
constructor() {
this.deferredPrompt = null;
this.init();
}
init() {
this.registerServiceWorker();
this.setupInstallPrompt();
this.setupAppInstalled();
}
// Регистрация Service Worker
registerServiceWorker() {
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then((registration) => {
console.log('SW зарегистрирован успешно:', registration.scope);
// Проверяем обновления
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
this.showUpdateNotification();
}
});
});
})
.catch((error) => {
console.log('Ошибка регистрации SW:', error);
});
});
}
}
// Показ уведомления об обновлении
showUpdateNotification() {
if (confirm('Доступна новая версия приложения. Обновить?')) {
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({ type: 'SKIP_WAITING' });
}
window.location.reload();
}
}
// Настройка промпта установки
setupInstallPrompt() {
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
this.deferredPrompt = e;
this.showInstallButton();
});
}
// Показ кнопки установки
showInstallButton() {
// Проверяем, не установлено ли уже приложение
if (window.matchMedia('(display-mode: standalone)').matches ||
window.navigator.standalone === true) {
return; // Приложение уже установлено
}
const installButton = this.createInstallButton();
this.addInstallButtonToPage(installButton);
}
// Создание кнопки установки
createInstallButton() {
const installButton = document.createElement('button');
installButton.textContent = '📱 Установить приложение';
installButton.className = 'btnSave';
installButton.style.marginTop = '10px';
installButton.style.width = '100%';
installButton.style.fontSize = '14px';
installButton.style.display = 'flex';
installButton.style.alignItems = 'center';
installButton.style.justifyContent = 'center';
installButton.style.gap = '8px';
installButton.addEventListener('click', () => {
this.installApp();
});
return installButton;
}
// Добавление кнопки на страницу
addInstallButtonToPage(installButton) {
// Ищем подходящее место для кнопки
const authLink = document.querySelector('.auth-link');
const footer = document.querySelector('.footer');
const container = document.querySelector('.container');
if (authLink) {
authLink.appendChild(installButton);
} else if (footer) {
footer.insertBefore(installButton, footer.firstChild);
} else if (container) {
container.appendChild(installButton);
}
}
// Установка приложения
installApp() {
if (this.deferredPrompt) {
this.deferredPrompt.prompt();
this.deferredPrompt.userChoice.then((choiceResult) => {
if (choiceResult.outcome === 'accepted') {
console.log('Пользователь установил приложение');
this.trackInstallation();
}
this.deferredPrompt = null;
this.removeInstallButton();
});
}
}
// Удаление кнопки установки
removeInstallButton() {
const installButton = document.querySelector('button[style*="Установить приложение"]');
if (installButton) {
installButton.remove();
}
}
// Отслеживание установки
trackInstallation() {
// Здесь можно добавить аналитику
console.log('PWA установлено успешно');
}
// Обработка успешной установки
setupAppInstalled() {
window.addEventListener('appinstalled', () => {
console.log('PWA установлено успешно');
this.removeInstallButton();
});
}
// Проверка статуса PWA
isPWAInstalled() {
return window.matchMedia('(display-mode: standalone)').matches ||
window.navigator.standalone === true;
}
// Получение информации о PWA
getPWAInfo() {
return {
isInstalled: this.isPWAInstalled(),
isOnline: navigator.onLine,
hasServiceWorker: 'serviceWorker' in navigator,
userAgent: navigator.userAgent
};
}
}
// Инициализация PWA Manager
const pwaManager = new PWAManager();
// Экспорт для использования в других скриптах
window.PWAManager = pwaManager;

View File

@ -3,8 +3,30 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Регистрация</title>
<title>Регистрация - NoteJS</title>
<!-- PWA Meta Tags -->
<meta name="description" content="NoteJS - современная система заметок с поддержкой Markdown, изображений, тегов и календаря" />
<meta name="theme-color" content="#007bff" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="NoteJS" />
<meta name="msapplication-TileColor" content="#007bff" />
<!-- Icons -->
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link rel="icon" type="image/png" sizes="32x32" href="/icons/icon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/icons/icon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/icons/icon-192x192.png" />
<link rel="mask-icon" href="/icon.svg" color="#007bff" />
<!-- Manifest -->
<link rel="manifest" href="/manifest.json" />
<!-- Styles -->
<link rel="stylesheet" href="/style.css" />
<!-- Scripts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/iconify/2.0.0/iconify.min.js"></script>
</head>
<body>
@ -61,5 +83,8 @@
<p>Создатель: <span>Fovway</span></p>
</div>
<script src="/register.js"></script>
<!-- PWA Script -->
<script src="/pwa.js"></script>
</body>
</html>

266
public/sw.js Normal file
View File

@ -0,0 +1,266 @@
// Service Worker для NoteJS
const CACHE_NAME = 'notejs-v1.0.0';
const STATIC_CACHE_NAME = 'notejs-static-v1.0.0';
const DYNAMIC_CACHE_NAME = 'notejs-dynamic-v1.0.0';
// Файлы для кэширования при установке
const STATIC_FILES = [
'/',
'/index.html',
'/login.html',
'/register.html',
'/notes.html',
'/profile.html',
'/style.css',
'/app.js',
'/login.js',
'/register.js',
'/profile.js',
'/icon.svg',
'/logo.svg',
'/manifest.json',
'/icons/icon-192x192.png',
'/icons/icon-512x512.png',
'https://cdnjs.cloudflare.com/ajax/libs/iconify/2.0.0/iconify.min.js'
];
// Файлы, которые не нужно кэшировать
const EXCLUDE_FROM_CACHE = [
'/api/',
'/uploads/',
'/database/'
];
// Установка Service Worker
self.addEventListener('install', (event) => {
console.log('[SW] Установка Service Worker');
event.waitUntil(
caches.open(STATIC_CACHE_NAME)
.then((cache) => {
console.log('[SW] Кэширование статических файлов');
return cache.addAll(STATIC_FILES);
})
.then(() => {
console.log('[SW] Статические файлы закэшированы');
return self.skipWaiting();
})
.catch((error) => {
console.error('[SW] Ошибка при кэшировании статических файлов:', error);
})
);
});
// Активация Service Worker
self.addEventListener('activate', (event) => {
console.log('[SW] Активация Service Worker');
event.waitUntil(
caches.keys()
.then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
// Удаляем старые кэши
if (cacheName !== STATIC_CACHE_NAME && cacheName !== DYNAMIC_CACHE_NAME) {
console.log('[SW] Удаление старого кэша:', cacheName);
return caches.delete(cacheName);
}
})
);
})
.then(() => {
console.log('[SW] Service Worker активирован');
return self.clients.claim();
})
);
});
// Перехват запросов
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// Пропускаем запросы к API и загрузкам
if (EXCLUDE_FROM_CACHE.some(pattern => url.pathname.startsWith(pattern))) {
return;
}
// Стратегия кэширования: Cache First для статических файлов, Network First для HTML
if (request.method === 'GET') {
event.respondWith(
handleRequest(request)
);
}
});
async function handleRequest(request) {
const url = new URL(request.url);
try {
// Для HTML файлов используем Network First стратегию
if (request.headers.get('accept')?.includes('text/html')) {
return await networkFirstStrategy(request);
}
// Для статических ресурсов используем Cache First стратегию
return await cacheFirstStrategy(request);
} catch (error) {
console.error('[SW] Ошибка при обработке запроса:', error);
// Fallback для HTML страниц
if (request.headers.get('accept')?.includes('text/html')) {
return await caches.match('/index.html');
}
throw error;
}
}
// Стратегия Cache First (для статических ресурсов)
async function cacheFirstStrategy(request) {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
console.log('[SW] Запрос из кэша:', request.url);
return cachedResponse;
}
console.log('[SW] Запрос к сети:', request.url);
const networkResponse = await fetch(request);
// Кэшируем успешные ответы
if (networkResponse.ok) {
const cache = await caches.open(DYNAMIC_CACHE_NAME);
cache.put(request, networkResponse.clone());
}
return networkResponse;
}
// Стратегия Network First (для HTML страниц)
async function networkFirstStrategy(request) {
try {
console.log('[SW] Запрос к сети (Network First):', request.url);
const networkResponse = await fetch(request);
// Кэшируем успешные ответы
if (networkResponse.ok) {
const cache = await caches.open(DYNAMIC_CACHE_NAME);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
console.log('[SW] Сеть недоступна, поиск в кэше:', request.url);
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// Fallback на главную страницу
if (request.headers.get('accept')?.includes('text/html')) {
return await caches.match('/index.html');
}
throw error;
}
}
// Обработка push уведомлений (для будущего использования)
self.addEventListener('push', (event) => {
console.log('[SW] Получено push уведомление');
const options = {
body: event.data ? event.data.text() : 'Новое уведомление от NoteJS',
icon: '/icons/icon-192x192.png',
badge: '/icons/icon-96x96.png',
vibrate: [100, 50, 100],
data: {
dateOfArrival: Date.now(),
primaryKey: 1
},
actions: [
{
action: 'explore',
title: 'Открыть приложение',
icon: '/icons/icon-96x96.png'
},
{
action: 'close',
title: 'Закрыть',
icon: '/icons/icon-96x96.png'
}
]
};
event.waitUntil(
self.registration.showNotification('NoteJS', options)
);
});
// Обработка кликов по уведомлениям
self.addEventListener('notificationclick', (event) => {
console.log('[SW] Клик по уведомлению:', event.action);
event.notification.close();
if (event.action === 'explore') {
event.waitUntil(
clients.openWindow('/')
);
}
});
// Синхронизация в фоне (для будущего использования)
self.addEventListener('sync', (event) => {
console.log('[SW] Фоновая синхронизация:', event.tag);
if (event.tag === 'background-sync') {
event.waitUntil(
doBackgroundSync()
);
}
});
async function doBackgroundSync() {
// Здесь можно добавить логику синхронизации данных
console.log('[SW] Выполнение фоновой синхронизации');
}
// Обработка сообщений от основного потока
self.addEventListener('message', (event) => {
console.log('[SW] Получено сообщение:', event.data);
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
if (event.data && event.data.type === 'GET_VERSION') {
event.ports[0].postMessage({ version: CACHE_NAME });
}
});
// Периодическая очистка кэша
self.addEventListener('periodicsync', (event) => {
if (event.tag === 'cache-cleanup') {
event.waitUntil(cleanupCache());
}
});
async function cleanupCache() {
const cacheNames = await caches.keys();
const oldCaches = cacheNames.filter(name =>
name !== STATIC_CACHE_NAME &&
name !== DYNAMIC_CACHE_NAME &&
name.startsWith('notejs-')
);
await Promise.all(
oldCaches.map(name => caches.delete(name))
);
console.log('[SW] Очистка старых кэшей завершена');
}

View File

@ -132,6 +132,25 @@ 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('/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.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());