Compare commits

...

2 Commits

12 changed files with 502 additions and 76 deletions

View File

@ -82,7 +82,7 @@ define(['./workbox-9dc17825'], (function (workbox) { 'use strict';
"revision": "3ca0b8505b4bec776b69afdba2768812"
}, {
"url": "index.html",
"revision": "0.nsn25edhihg"
"revision": "0.o51qplqi6t"
}], {});
workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

View File

@ -34,22 +34,31 @@ axiosClient.interceptors.response.use(
if (error.response?.status === 401) {
// Список URL, где 401 означает неправильный пароль, а не истечение сессии
// или где не нужно делать редирект (например, проверка статуса)
const passwordProtectedUrls = [
"/login", // Страница входа
"/register", // Страница регистрации
"/notes/archived/all", // Удаление всех архивных заметок
"/user/delete-account", // Удаление аккаунта
"/auth/status", // Проверка статуса (не нужно делать редирект при ошибке)
];
// Проверяем, является ли это запросом с проверкой пароля
// Проверяем, является ли это запросом с проверкой пароля или статуса
const isPasswordProtected = passwordProtectedUrls.some((url) =>
error.config?.url?.includes(url)
);
// Разлогиниваем только если это НЕ запрос с проверкой пароля
if (!isPasswordProtected) {
// Разлогиниваем только если это НЕ запрос с проверкой пароля/статуса
// и только если есть ответ от сервера (не offline режим)
if (!isPasswordProtected && error.response) {
localStorage.removeItem("isAuthenticated");
window.location.href = "/";
localStorage.removeItem("userId");
localStorage.removeItem("username");
// Не делаем редирект, если нет интернета (error.response будет undefined)
// ProtectedRoute сам обработает это
if (navigator.onLine) {
window.location.href = "/";
}
}
}

View File

@ -4,6 +4,7 @@ import { useAppSelector, useAppDispatch } from "../store/hooks";
import { setAuth, clearAuth } from "../store/slices/authSlice";
import { authApi } from "../api/authApi";
import { LoadingOverlay } from "./common/LoadingOverlay";
import { checkNetworkStatus } from "../utils/offlineManager";
export const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({
children,
@ -15,26 +16,84 @@ export const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({
useEffect(() => {
const checkAuth = async () => {
try {
const authStatus = await authApi.checkStatus();
if (authStatus.authenticated) {
// Проверяем, есть ли интернет
const isOnline = await checkNetworkStatus();
if (!isOnline) {
// Offline режим - используем данные из localStorage
const savedAuth = localStorage.getItem("isAuthenticated") === "true";
const savedUserId = localStorage.getItem("userId");
const savedUsername = localStorage.getItem("username");
if (savedAuth && savedUserId && savedUsername) {
// Восстанавливаем авторизацию из localStorage
dispatch(
setAuth({
userId: parseInt(savedUserId, 10),
username: savedUsername,
})
);
console.log('[ProtectedRoute] Offline mode: restored auth from localStorage');
} else {
// Нет сохраненных данных - не авторизован
dispatch(clearAuth());
}
} else {
// Online режим - проверяем через API
try {
const authStatus = await authApi.checkStatus();
if (authStatus.authenticated) {
dispatch(
setAuth({
userId: authStatus.userId!,
username: authStatus.username!,
})
);
} else {
dispatch(clearAuth());
}
} catch (apiError) {
// Если API запрос не удался, но есть данные в localStorage
// Используем их для offline работы
const savedAuth = localStorage.getItem("isAuthenticated") === "true";
const savedUserId = localStorage.getItem("userId");
const savedUsername = localStorage.getItem("username");
if (savedAuth && savedUserId && savedUsername) {
console.log('[ProtectedRoute] API failed, using cached auth');
dispatch(
setAuth({
userId: parseInt(savedUserId, 10),
username: savedUsername,
})
);
} else {
dispatch(clearAuth());
}
}
}
} catch (error) {
console.error('[ProtectedRoute] Auth check error:', error);
// В случае ошибки проверяем localStorage
const savedAuth = localStorage.getItem("isAuthenticated") === "true";
const savedUserId = localStorage.getItem("userId");
const savedUsername = localStorage.getItem("username");
if (savedAuth && savedUserId && savedUsername) {
dispatch(
setAuth({
userId: authStatus.userId!,
username: authStatus.username!,
userId: parseInt(savedUserId, 10),
username: savedUsername,
})
);
} else {
dispatch(clearAuth());
}
} catch {
dispatch(clearAuth());
} finally {
setIsChecking(false);
}
};
// Всегда проверяем статус аутентификации при монтировании,
// независимо от начального состояния Redux (localStorage может быть устаревшим)
checkAuth();
}, [dispatch]);

View File

@ -296,6 +296,7 @@ export const FloatingToolbar: React.FC<FloatingToolbarProps> = ({
{hasSelection && (
<>
<div className="floating-toolbar-separator" />
<button
className="floating-toolbar-btn"
onClick={handleCopy}
@ -367,22 +368,6 @@ export const FloatingToolbar: React.FC<FloatingToolbarProps> = ({
>
<Icon icon="mdi:eye-off" />
</button>
<button
className="floating-toolbar-btn"
onClick={() => handleFormat("`", "`")}
title="Код"
>
<Icon icon="mdi:code-tags" />
</button>
<button
className="floating-toolbar-btn"
onClick={() => handleFormat("> ", "")}
title="Цитата"
>
<Icon icon="mdi:format-quote-close" />
</button>
</>
)}
</div>

View File

@ -239,6 +239,128 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
const end = textarea.selectionEnd;
const selectedText = content.substring(start, end);
// Определяем маркеры списков и цитат, которые обрабатываются построчно
const listMarkers = ["- ", "1. ", "- [ ] ", "> "];
const isListMarker = listMarkers.includes(before);
// Если это маркер списка и выделено несколько строк, обрабатываем построчно
if (isListMarker && selectedText.includes("\n")) {
const lines = selectedText.split("\n");
const beforeText = content.substring(0, start);
const afterText = content.substring(end);
// Определяем, есть ли уже такие маркеры на всех строках
let allLinesHaveMarker = true;
let hasAnyMarker = false;
for (const line of lines) {
const trimmedLine = line.trimStart();
if (before === "- ") {
// Для маркированного списка проверяем различные варианты
if (trimmedLine.match(/^[-*+]\s/)) {
hasAnyMarker = true;
} else {
allLinesHaveMarker = false;
}
} else if (before === "1. ") {
// Для нумерованного списка
if (trimmedLine.match(/^\d+\.\s/)) {
hasAnyMarker = true;
} else {
allLinesHaveMarker = false;
}
} else if (before === "- [ ] ") {
// Для чекбокса
if (trimmedLine.match(/^-\s+\[[ xX]\]\s/)) {
hasAnyMarker = true;
} else {
allLinesHaveMarker = false;
}
} else if (before === "> ") {
// Для цитаты
if (trimmedLine.startsWith("> ")) {
hasAnyMarker = true;
} else {
allLinesHaveMarker = false;
}
}
}
// Если все строки уже имеют маркер, удаляем их (переключение)
// Если некоторые имеют, но не все - добавляем к тем, у которых нет
const processedLines = lines.map((line, index) => {
const trimmedLine = line.trimStart();
const leadingSpaces = line.substring(0, line.length - trimmedLine.length);
let shouldToggle = false;
if (before === "- ") {
const match = trimmedLine.match(/^([-*+])\s/);
if (match) {
shouldToggle = true;
}
} else if (before === "1. ") {
if (trimmedLine.match(/^\d+\.\s/)) {
shouldToggle = true;
}
} else if (before === "- [ ] ") {
if (trimmedLine.match(/^-\s+\[[ xX]\]\s/)) {
shouldToggle = true;
}
} else if (before === "> ") {
if (trimmedLine.startsWith("> ")) {
shouldToggle = true;
}
}
if (shouldToggle && allLinesHaveMarker) {
// Удаляем маркер
if (before === "- ") {
const match = trimmedLine.match(/^([-*+])\s(.*)$/);
return match ? leadingSpaces + match[2] : line;
} else if (before === "1. ") {
const match = trimmedLine.match(/^\d+\.\s(.*)$/);
return match ? leadingSpaces + match[1] : line;
} else if (before === "- [ ] ") {
const match = trimmedLine.match(/^-\s+\[[ xX]\]\s(.*)$/);
return match ? leadingSpaces + match[1] : line;
} else if (before === "> ") {
return trimmedLine.startsWith("> ")
? leadingSpaces + trimmedLine.substring(2)
: line;
}
} else if (!shouldToggle || !allLinesHaveMarker) {
// Добавляем маркер
if (before === "1. ") {
// Для нумерованного списка добавляем правильный номер
const number = index + 1;
return leadingSpaces + `${number}. ` + trimmedLine;
} else {
return leadingSpaces + before + trimmedLine;
}
}
return line;
});
const newSelectedText = processedLines.join("\n");
const newText = beforeText + newSelectedText + afterText;
// Вычисляем новую позицию курсора
const newStart = start;
const newEnd = start + newSelectedText.length;
setContent(newText);
setTimeout(() => {
textarea.focus();
textarea.setSelectionRange(newStart, newEnd);
const formats = getActiveFormats();
setActiveFormats(formats);
}, 0);
return;
}
const tagLength = before.length;
// Проверяем область вокруг выделения (расширяем для проверки тегов)

View File

@ -325,6 +325,128 @@ export const NoteItem: React.FC<NoteItemProps> = ({
const end = textarea.selectionEnd;
const selectedText = editContent.substring(start, end);
// Определяем маркеры списков и цитат, которые обрабатываются построчно
const listMarkers = ["- ", "1. ", "- [ ] ", "> "];
const isListMarker = listMarkers.includes(before);
// Если это маркер списка и выделено несколько строк, обрабатываем построчно
if (isListMarker && selectedText.includes("\n")) {
const lines = selectedText.split("\n");
const beforeText = editContent.substring(0, start);
const afterText = editContent.substring(end);
// Определяем, есть ли уже такие маркеры на всех строках
let allLinesHaveMarker = true;
let hasAnyMarker = false;
for (const line of lines) {
const trimmedLine = line.trimStart();
if (before === "- ") {
// Для маркированного списка проверяем различные варианты
if (trimmedLine.match(/^[-*+]\s/)) {
hasAnyMarker = true;
} else {
allLinesHaveMarker = false;
}
} else if (before === "1. ") {
// Для нумерованного списка
if (trimmedLine.match(/^\d+\.\s/)) {
hasAnyMarker = true;
} else {
allLinesHaveMarker = false;
}
} else if (before === "- [ ] ") {
// Для чекбокса
if (trimmedLine.match(/^-\s+\[[ xX]\]\s/)) {
hasAnyMarker = true;
} else {
allLinesHaveMarker = false;
}
} else if (before === "> ") {
// Для цитаты
if (trimmedLine.startsWith("> ")) {
hasAnyMarker = true;
} else {
allLinesHaveMarker = false;
}
}
}
// Если все строки уже имеют маркер, удаляем их (переключение)
// Если некоторые имеют, но не все - добавляем к тем, у которых нет
const processedLines = lines.map((line, index) => {
const trimmedLine = line.trimStart();
const leadingSpaces = line.substring(0, line.length - trimmedLine.length);
let shouldToggle = false;
if (before === "- ") {
const match = trimmedLine.match(/^([-*+])\s/);
if (match) {
shouldToggle = true;
}
} else if (before === "1. ") {
if (trimmedLine.match(/^\d+\.\s/)) {
shouldToggle = true;
}
} else if (before === "- [ ] ") {
if (trimmedLine.match(/^-\s+\[[ xX]\]\s/)) {
shouldToggle = true;
}
} else if (before === "> ") {
if (trimmedLine.startsWith("> ")) {
shouldToggle = true;
}
}
if (shouldToggle && allLinesHaveMarker) {
// Удаляем маркер
if (before === "- ") {
const match = trimmedLine.match(/^([-*+])\s(.*)$/);
return match ? leadingSpaces + match[2] : line;
} else if (before === "1. ") {
const match = trimmedLine.match(/^\d+\.\s(.*)$/);
return match ? leadingSpaces + match[1] : line;
} else if (before === "- [ ] ") {
const match = trimmedLine.match(/^-\s+\[[ xX]\]\s(.*)$/);
return match ? leadingSpaces + match[1] : line;
} else if (before === "> ") {
return trimmedLine.startsWith("> ")
? leadingSpaces + trimmedLine.substring(2)
: line;
}
} else if (!shouldToggle || !allLinesHaveMarker) {
// Добавляем маркер
if (before === "1. ") {
// Для нумерованного списка добавляем правильный номер
const number = index + 1;
return leadingSpaces + `${number}. ` + trimmedLine;
} else {
return leadingSpaces + before + trimmedLine;
}
}
return line;
});
const newSelectedText = processedLines.join("\n");
const newText = beforeText + newSelectedText + afterText;
// Вычисляем новую позицию курсора
const newStart = start;
const newEnd = start + newSelectedText.length;
setEditContent(newText);
setTimeout(() => {
textarea.focus();
textarea.setSelectionRange(newStart, newEnd);
const formats = getActiveFormats();
setActiveFormats(formats);
}, 0);
return;
}
const tagLength = before.length;
// Проверяем область вокруг выделения (расширяем для проверки тегов)

View File

@ -14,7 +14,7 @@ import { addNotification } from "./store/slices/uiSlice";
// Регистрация PWA (vite-plugin-pwa автоматически внедряет регистрацию через injectRegister: "auto")
// Инициализация offline функционала
// Инициализация offline функционала (неблокирующая)
async function initOfflineMode() {
try {
console.log('[Init] Initializing offline mode...');
@ -23,28 +23,46 @@ async function initOfflineMode() {
await dbManager.init();
console.log('[Init] IndexedDB initialized');
// Проверка состояния сети
const isOnline = await checkNetworkStatus();
store.dispatch(setOfflineMode(!isOnline));
console.log(`[Init] Network status: ${isOnline ? 'online' : 'offline'}`);
// Устанавливаем начальное состояние на основе navigator.onLine (мгновенно, без блокировки)
const initialOnlineState = navigator.onLine;
store.dispatch(setOfflineMode(!initialOnlineState));
console.log(`[Init] Initial network status (navigator.onLine): ${initialOnlineState ? 'online' : 'offline'}`);
// Проверка состояния сети в фоне (не блокирует загрузку приложения)
checkNetworkStatus().then((isOnline) => {
store.dispatch(setOfflineMode(!isOnline));
console.log(`[Init] Network status (after check): ${isOnline ? 'online' : 'offline'}`);
}).catch((error) => {
console.warn('[Init] Network check failed, using navigator.onLine:', error);
// В случае ошибки используем navigator.onLine
store.dispatch(setOfflineMode(!navigator.onLine));
});
// Установка listeners для событий сети
networkListener.onOnline(async () => {
console.log('[Network] Online event detected');
const isOnline = await checkNetworkStatus();
store.dispatch(setOfflineMode(!isOnline));
if (isOnline) {
store.dispatch(
addNotification({
message: 'Подключение восстановлено, начинаем синхронизацию...',
type: 'info',
})
);
// Запуск синхронизации
await syncService.startSync();
}
// Небольшая задержка перед проверкой для стабилизации соединения
setTimeout(async () => {
try {
const isOnline = await checkNetworkStatus();
store.dispatch(setOfflineMode(!isOnline));
if (isOnline) {
store.dispatch(
addNotification({
message: 'Подключение восстановлено, начинаем синхронизацию...',
type: 'info',
})
);
// Запуск синхронизации
await syncService.startSync();
}
} catch (error) {
console.error('[Network] Error checking network status:', error);
store.dispatch(setOfflineMode(!navigator.onLine));
}
}, 500);
});
networkListener.onOffline(() => {
@ -59,29 +77,40 @@ async function initOfflineMode() {
});
// Обновление счетчика ожидающих синхронизацию
const pendingCount = await dbManager.getPendingSyncCount();
store.dispatch(setPendingSyncCount(pendingCount));
if (pendingCount > 0) {
console.log(`[Init] Found ${pendingCount} pending sync items`);
}
dbManager.getPendingSyncCount().then((pendingCount) => {
store.dispatch(setPendingSyncCount(pendingCount));
if (pendingCount > 0) {
console.log(`[Init] Found ${pendingCount} pending sync items`);
}
// Автоматическая синхронизация при старте если есть что синхронизировать
if (isOnline && pendingCount > 0) {
console.log('[Init] Starting initial sync...');
// Небольшая задержка для инициализации UI
setTimeout(() => {
syncService.startSync();
}, 2000);
}
// Автоматическая синхронизация при старте если есть что синхронизировать
// Проверяем статус сети перед синхронизацией
checkNetworkStatus().then((isOnline) => {
if (isOnline && pendingCount > 0) {
console.log('[Init] Starting initial sync...');
// Небольшая задержка для инициализации UI
setTimeout(() => {
syncService.startSync();
}, 2000);
}
}).catch(() => {
// Если проверка сети не удалась, не запускаем синхронизацию
console.log('[Init] Skipping initial sync due to network check failure');
});
}).catch((error) => {
console.error('[Init] Error getting pending sync count:', error);
});
console.log('[Init] Offline mode initialized successfully');
} catch (error) {
console.error('[Init] Error initializing offline mode:', error);
// Не блокируем запуск приложения даже при ошибке
store.dispatch(setOfflineMode(!navigator.onLine));
}
}
// Запуск инициализации
// Запуск инициализации (не блокирует рендеринг React)
initOfflineMode();
ReactDOM.createRoot(document.getElementById("root")!).render(

View File

@ -9,7 +9,7 @@ interface AuthState {
const initialState: AuthState = {
isAuthenticated: localStorage.getItem("isAuthenticated") === "true",
userId: null,
userId: localStorage.getItem("userId") ? parseInt(localStorage.getItem("userId")!, 10) : null,
username: localStorage.getItem("username") || null,
loading: false,
};
@ -26,6 +26,7 @@ const authSlice = createSlice({
state.userId = action.payload.userId;
state.username = action.payload.username;
localStorage.setItem("isAuthenticated", "true");
localStorage.setItem("userId", action.payload.userId.toString());
localStorage.setItem("username", action.payload.username);
},
clearAuth: (state) => {
@ -33,6 +34,7 @@ const authSlice = createSlice({
state.userId = null;
state.username = null;
localStorage.removeItem("isAuthenticated");
localStorage.removeItem("userId");
localStorage.removeItem("username");
},
},

View File

@ -1612,22 +1612,29 @@ textarea:focus {
border-radius: 0 0 5px 5px;
}
/* Плавающая панель форматирования */
/* Плавающая панель форматирования - улучшенная версия */
.floating-toolbar-wrapper {
overflow-x: auto;
overflow-y: hidden;
/* Скрываем скроллбар */
scrollbar-width: none;
/* Плавная прокрутка */
scroll-behavior: smooth;
/* Максимальная ширина с учетом отступов */
max-width: calc(100vw - 20px);
/* Предотвращаем выбор текста при перетаскивании */
user-select: none;
-webkit-user-select: none;
/* Улучшаем производительность */
will-change: transform;
}
.floating-toolbar-wrapper::-webkit-scrollbar {
display: none;
}
/* Мобильная версия */
.floating-toolbar-wrapper.mobile {
max-width: calc(100vw - 32px);
}
.floating-toolbar {
display: flex;
gap: 4px;
@ -1638,7 +1645,6 @@ textarea:focus {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
animation: fadeIn 0.2s ease-out;
align-items: center;
/* Предотвращаем сжатие кнопок */
flex-shrink: 0;
min-width: fit-content;
width: max-content;
@ -1669,6 +1675,28 @@ textarea:focus {
transition: all 0.2s ease;
min-width: 32px;
min-height: 32px;
/* Улучшение для touch */
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
/* Увеличенные кнопки на мобильных */
@media (max-width: 768px) {
.floating-toolbar-wrapper.mobile .floating-toolbar-btn {
min-width: 44px;
min-height: 44px;
padding: 8px 12px;
font-size: 18px;
}
.floating-toolbar-wrapper.mobile .floating-toolbar {
padding: 8px;
gap: 6px;
}
.floating-toolbar-wrapper.mobile .floating-toolbar-btn .iconify {
font-size: 20px;
}
}
.floating-toolbar-btn:hover {

View File

@ -45,9 +45,22 @@ const renderer: any = {
},
// Кастомный renderer для элементов списка с чекбоксами
listitem(token: any) {
const text = token.text;
const task = token.task;
const checked = token.checked;
// Используем tokens для правильной обработки форматирования внутри элементов списка
// token.tokens содержит массив токенов для вложенного содержимого
const tokens = token.tokens || [];
let text: string;
if (tokens.length > 0) {
// Используем this.parser.parseInline для правильной обработки вложенного форматирования
// this указывает на экземпляр Parser в контексте renderer
text = this.parser.parseInline(tokens);
} else {
// Fallback на token.text, если tokens отсутствуют
text = token.text || '';
}
if (task) {
const checkbox = `<input type="checkbox" ${checked ? "checked" : ""} />`;

View File

@ -39,6 +39,8 @@ export function waitForIndexedDB(): Promise<IDBDatabase> {
/**
* Проверка состояния сети (более надежный метод)
* Не блокирует выполнение, всегда возвращает результат
* Различает "нет интернета" (offline) и "не авторизован" (401)
*/
export async function checkNetworkStatus(): Promise<boolean> {
// Простая проверка navigator.onLine
@ -49,7 +51,8 @@ export async function checkNetworkStatus(): Promise<boolean> {
// Дополнительная проверка через fetch с коротким таймаутом
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 2000);
// Уменьшаем таймаут для более быстрого ответа
const timeoutId = setTimeout(() => controller.abort(), 1500);
// Используем /auth/status так как он всегда доступен при наличии сети
const response = await fetch('/api/auth/status', {
@ -60,10 +63,27 @@ export async function checkNetworkStatus(): Promise<boolean> {
});
clearTimeout(timeoutId);
return response.ok;
// Если получили ответ (даже 401), значит интернет есть
// 401 означает "не авторизован", но НЕ "нет интернета"
// Любой ответ (даже ошибка) означает, что сеть работает
return true;
} catch (error) {
// Если запрос не удался, но navigator.onLine = true, считаем что онлайн
// (возможно, просто таймаут или другая проблема)
// Если запрос не удался из-за таймаута или сетевой ошибки
// (AbortError, NetworkError, TypeError и т.д.)
// Это означает, что интернета действительно нет
if (error instanceof Error) {
// AbortError означает таймаут - нет интернета
if (error.name === 'AbortError') {
return false;
}
// TypeError обычно означает, что запрос не может быть выполнен
if (error.name === 'TypeError' && error.message.includes('fetch')) {
return false;
}
}
// В остальных случаях считаем, что интернет есть
// (возможно, просто ошибка авторизации или другая проблема)
return navigator.onLine;
}
}

View File

@ -12,7 +12,10 @@ export default defineConfig({
"icon.svg",
"icons/icon-192x192.png",
"icons/icon-512x512.png",
"manifest.json",
],
// Включаем стратегию для offline работы
strategies: "generateSW",
manifest: {
name: "NoteJS - Система заметок",
short_name: "NoteJS",
@ -88,8 +91,41 @@ export default defineConfig({
],
},
workbox: {
globPatterns: ["**/*.{js,css,html,ico,png,svg,woff,woff2,ttf,eot}"],
// Кэшируем все статические ресурсы для offline работы
globPatterns: ["**/*.{js,css,html,ico,png,svg,woff,woff2,ttf,eot,json}"],
// Стратегия для главной страницы - CacheFirst для offline работы
navigateFallback: "/index.html",
navigateFallbackDenylist: [/^\/api/, /^\/_/],
// Кэширование для offline работы приложения
runtimeCaching: [
{
// Стратегия для корневого пути и index.html
urlPattern: ({ request }) =>
request.destination === 'document' ||
request.url.endsWith('/') ||
request.url.endsWith('/index.html'),
handler: "NetworkFirst",
options: {
cacheName: "html-cache",
expiration: {
maxEntries: 10,
maxAgeSeconds: 24 * 60 * 60, // 24 hours
},
networkTimeoutSeconds: 3,
},
},
{
// Статические ресурсы (JS, CSS) - кэшируем для offline
urlPattern: /\.(?:js|css|woff|woff2|ttf|eot)$/,
handler: "CacheFirst",
options: {
cacheName: "static-resources-cache",
expiration: {
maxEntries: 200,
maxAgeSeconds: 365 * 24 * 60 * 60, // 1 year
},
},
},
{
urlPattern: /^https:\/\/api\./,
handler: "NetworkFirst",
@ -99,6 +135,7 @@ export default defineConfig({
maxEntries: 50,
maxAgeSeconds: 60 * 60, // 1 hour
},
networkTimeoutSeconds: 3,
},
},
{
@ -110,7 +147,7 @@ export default defineConfig({
maxEntries: 100,
maxAgeSeconds: 24 * 60 * 60, // 24 hours
},
networkTimeoutSeconds: 10,
networkTimeoutSeconds: 3,
},
},
{