diff --git a/dev-dist/sw.js b/dev-dist/sw.js index 5451ed4..04d17d9 100644 --- a/dev-dist/sw.js +++ b/dev-dist/sw.js @@ -81,34 +81,15 @@ define(['./workbox-9dc17825'], (function (workbox) { 'use strict'; "url": "registerSW.js", "revision": "3ca0b8505b4bec776b69afdba2768812" }, { - "url": "/index.html", - "revision": "0.ri1maclacqo" + "url": "index.html", + "revision": "0.fijdulj6fg" }], {}); workbox.cleanupOutdatedCaches(); - workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("/index.html"), { - allowlist: [/^\/$/], - denylist: [/^\/api/, /^\/_/] + workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { + allowlist: [/^\/$/] })); - workbox.registerRoute(({ - request - }) => request.destination === "document" || request.url.endsWith("/") || request.url.endsWith("/index.html"), new workbox.NetworkFirst({ - "cacheName": "html-cache", - "networkTimeoutSeconds": 3, - plugins: [new workbox.ExpirationPlugin({ - maxEntries: 10, - maxAgeSeconds: 86400 - })] - }), 'GET'); - workbox.registerRoute(/\.(?:js|css|woff|woff2|ttf|eot)$/, new workbox.CacheFirst({ - "cacheName": "static-resources-cache", - plugins: [new workbox.ExpirationPlugin({ - maxEntries: 200, - maxAgeSeconds: 31536000 - })] - }), 'GET'); workbox.registerRoute(/^https:\/\/api\./, new workbox.NetworkFirst({ "cacheName": "api-cache", - "networkTimeoutSeconds": 3, plugins: [new workbox.ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 3600 @@ -116,7 +97,7 @@ define(['./workbox-9dc17825'], (function (workbox) { 'use strict'; }), 'GET'); workbox.registerRoute(/\/api\//, new workbox.NetworkFirst({ "cacheName": "api-cache-local", - "networkTimeoutSeconds": 3, + "networkTimeoutSeconds": 10, plugins: [new workbox.ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 86400 diff --git a/src/App.tsx b/src/App.tsx index 6d6a875..9b7c16e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -19,12 +19,7 @@ const AppContent: React.FC = () => { <> - + } /> } /> diff --git a/src/api/axiosClient.ts b/src/api/axiosClient.ts index 92850a4..bd76eaa 100644 --- a/src/api/axiosClient.ts +++ b/src/api/axiosClient.ts @@ -34,31 +34,22 @@ 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) ); - // Разлогиниваем только если это НЕ запрос с проверкой пароля/статуса - // и только если есть ответ от сервера (не offline режим) - if (!isPasswordProtected && error.response) { + // Разлогиниваем только если это НЕ запрос с проверкой пароля + if (!isPasswordProtected) { localStorage.removeItem("isAuthenticated"); - localStorage.removeItem("userId"); - localStorage.removeItem("username"); - // Не делаем редирект, если нет интернета (error.response будет undefined) - // ProtectedRoute сам обработает это - if (navigator.onLine) { - window.location.href = "/"; - } + window.location.href = "/"; } } diff --git a/src/api/notesApi.ts b/src/api/notesApi.ts index 72c021c..93ff961 100644 --- a/src/api/notesApi.ts +++ b/src/api/notesApi.ts @@ -43,7 +43,7 @@ export const notesApi = { return data; }, - unarchive: async (id: number | string) => { + unarchive: async (id: number) => { const { data } = await axiosClient.put(`/notes/${id}/unarchive`); return data; }, @@ -89,7 +89,7 @@ export const notesApi = { return data; }, - deleteArchived: async (id: number | string) => { + deleteArchived: async (id: number) => { await axiosClient.delete(`/notes/archived/${id}`); }, diff --git a/src/api/userApi.ts b/src/api/userApi.ts index 06ffd33..3655742 100644 --- a/src/api/userApi.ts +++ b/src/api/userApi.ts @@ -12,8 +12,8 @@ export const userApi = { currentPassword?: string; newPassword?: string; accent_color?: string; - show_edit_date?: boolean | number; - colored_icons?: boolean | number; + show_edit_date?: boolean; + colored_icons?: boolean; } ) => { const { data } = await axiosClient.put("/user/profile", profile); diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx index 687cd17..0310ba3 100644 --- a/src/components/ProtectedRoute.tsx +++ b/src/components/ProtectedRoute.tsx @@ -4,7 +4,6 @@ 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, @@ -16,84 +15,26 @@ export const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ useEffect(() => { const checkAuth = async () => { try { - // Проверяем, есть ли интернет - 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) { + const authStatus = await authApi.checkStatus(); + if (authStatus.authenticated) { dispatch( setAuth({ - userId: parseInt(savedUserId, 10), - username: savedUsername, + userId: authStatus.userId!, + username: authStatus.username!, }) ); } else { dispatch(clearAuth()); } + } catch { + dispatch(clearAuth()); } finally { setIsChecking(false); } }; + // Всегда проверяем статус аутентификации при монтировании, + // независимо от начального состояния Redux (localStorage может быть устаревшим) checkAuth(); }, [dispatch]); diff --git a/src/components/calendar/MiniCalendar.tsx b/src/components/calendar/MiniCalendar.tsx index dade509..df75a5c 100644 --- a/src/components/calendar/MiniCalendar.tsx +++ b/src/components/calendar/MiniCalendar.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { format, startOfMonth, diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index db6b787..ab78c39 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -2,6 +2,7 @@ import React from "react"; import { MiniCalendar } from "../calendar/MiniCalendar"; import { SearchBar } from "../search/SearchBar"; import { TagsFilter } from "../search/TagsFilter"; +import { useAppSelector } from "../../store/hooks"; import { Note } from "../../types/note"; interface SidebarProps { diff --git a/src/components/notes/MarkdownToolbar.tsx b/src/components/notes/MarkdownToolbar.tsx index 38003b6..e6d7817 100644 --- a/src/components/notes/MarkdownToolbar.tsx +++ b/src/components/notes/MarkdownToolbar.tsx @@ -113,14 +113,7 @@ export const MarkdownToolbar: React.FC = ({ }; }, [isDragging]); - const buttons: Array<{ - id: string; - action?: () => void; - before?: string; - after?: string; - title?: string; - icon?: string; - }> = []; + const buttons = []; return (
= ({ }} title={btn.title} > - {btn.icon && } + ))} diff --git a/src/components/notes/NoteEditor.tsx b/src/components/notes/NoteEditor.tsx index 78fec79..f59d1f7 100644 --- a/src/components/notes/NoteEditor.tsx +++ b/src/components/notes/NoteEditor.tsx @@ -4,7 +4,7 @@ import { FloatingToolbar } from "./FloatingToolbar"; import { NotePreview } from "./NotePreview"; import { ImageUpload } from "./ImageUpload"; import { FileUpload } from "./FileUpload"; -import { useAppSelector } from "../../store/hooks"; +import { useAppSelector, useAppDispatch } from "../../store/hooks"; import { useNotification } from "../../hooks/useNotification"; import { offlineNotesApi } from "../../api/offlineNotesApi"; import { aiApi } from "../../api/aiApi"; @@ -31,6 +31,7 @@ export const NoteEditor: React.FC = ({ onSave }) => { const isPreviewMode = useAppSelector((state) => state.ui.isPreviewMode); const { showNotification } = useNotification(); const aiEnabled = useAppSelector((state) => state.profile.aiEnabled); + const dispatch = useAppDispatch(); const handleSave = async () => { if (!content.trim()) { @@ -250,27 +251,36 @@ export const NoteEditor: React.FC = ({ onSave }) => { // Определяем, есть ли уже такие маркеры на всех строках let allLinesHaveMarker = true; + let hasAnyMarker = false; for (const line of lines) { const trimmedLine = line.trimStart(); if (before === "- ") { // Для маркированного списка проверяем различные варианты - if (!trimmedLine.match(/^[-*+]\s/)) { + if (trimmedLine.match(/^[-*+]\s/)) { + hasAnyMarker = true; + } else { allLinesHaveMarker = false; } } else if (before === "1. ") { // Для нумерованного списка - if (!trimmedLine.match(/^\d+\.\s/)) { + if (trimmedLine.match(/^\d+\.\s/)) { + hasAnyMarker = true; + } else { allLinesHaveMarker = false; } } else if (before === "- [ ] ") { // Для чекбокса - if (!trimmedLine.match(/^-\s+\[[ xX]\]\s/)) { + if (trimmedLine.match(/^-\s+\[[ xX]\]\s/)) { + hasAnyMarker = true; + } else { allLinesHaveMarker = false; } } else if (before === "> ") { // Для цитаты - if (!trimmedLine.startsWith("> ")) { + if (trimmedLine.startsWith("> ")) { + hasAnyMarker = true; + } else { allLinesHaveMarker = false; } } @@ -520,12 +530,14 @@ export const NoteEditor: React.FC = ({ onSave }) => { const lines = text.split("\n"); // Определяем текущую строку + let currentLineIndex = 0; let currentLineStart = 0; let currentLine = ""; for (let i = 0; i < lines.length; i++) { const lineLength = lines[i].length; if (currentLineStart + lineLength >= start) { + currentLineIndex = i; currentLine = lines[i]; break; } @@ -656,6 +668,7 @@ export const NoteEditor: React.FC = ({ onSave }) => { const lineHeight = parseInt(styles.lineHeight) || 20; const paddingTop = parseInt(styles.paddingTop) || 0; const paddingLeft = parseInt(styles.paddingLeft) || 0; + const fontSize = parseInt(styles.fontSize) || 14; // Более точный расчет ширины символа // Создаем временный элемент для измерения diff --git a/src/components/notes/NoteItem.tsx b/src/components/notes/NoteItem.tsx index 9eeb4f2..233691e 100644 --- a/src/components/notes/NoteItem.tsx +++ b/src/components/notes/NoteItem.tsx @@ -31,7 +31,7 @@ interface NoteItemProps { export const NoteItem: React.FC = ({ note, - onDelete: _onDelete, + onDelete, onPin, onArchive, onReload, @@ -41,8 +41,8 @@ export const NoteItem: React.FC = ({ const [showArchiveModal, setShowArchiveModal] = useState(false); const [editImages, setEditImages] = useState([]); const [editFiles, setEditFiles] = useState([]); - const [deletedImageIds, setDeletedImageIds] = useState<(number | string)[]>([]); - const [deletedFileIds, setDeletedFileIds] = useState<(number | string)[]>([]); + const [deletedImageIds, setDeletedImageIds] = useState([]); + const [deletedFileIds, setDeletedFileIds] = useState([]); const [isAiLoading, setIsAiLoading] = useState(false); const [showFloatingToolbar, setShowFloatingToolbar] = useState(false); const [toolbarPosition, setToolbarPosition] = useState({ top: 0, left: 0 }); @@ -140,19 +140,19 @@ export const NoteItem: React.FC = ({ setLocalPreviewMode(false); }; - const handleDeleteExistingImage = (imageId: number | string) => { + const handleDeleteExistingImage = (imageId: number) => { setDeletedImageIds([...deletedImageIds, imageId]); }; - const handleDeleteExistingFile = (fileId: number | string) => { + const handleDeleteExistingFile = (fileId: number) => { setDeletedFileIds([...deletedFileIds, fileId]); }; - const handleRestoreImage = (imageId: number | string) => { + const handleRestoreImage = (imageId: number) => { setDeletedImageIds(deletedImageIds.filter((id) => id !== imageId)); }; - const handleRestoreFile = (fileId: number | string) => { + const handleRestoreFile = (fileId: number) => { setDeletedFileIds(deletedFileIds.filter((id) => id !== fileId)); }; @@ -337,27 +337,36 @@ export const NoteItem: React.FC = ({ // Определяем, есть ли уже такие маркеры на всех строках let allLinesHaveMarker = true; + let hasAnyMarker = false; for (const line of lines) { const trimmedLine = line.trimStart(); if (before === "- ") { // Для маркированного списка проверяем различные варианты - if (!trimmedLine.match(/^[-*+]\s/)) { + if (trimmedLine.match(/^[-*+]\s/)) { + hasAnyMarker = true; + } else { allLinesHaveMarker = false; } } else if (before === "1. ") { // Для нумерованного списка - if (!trimmedLine.match(/^\d+\.\s/)) { + if (trimmedLine.match(/^\d+\.\s/)) { + hasAnyMarker = true; + } else { allLinesHaveMarker = false; } } else if (before === "- [ ] ") { // Для чекбокса - if (!trimmedLine.match(/^-\s+\[[ xX]\]\s/)) { + if (trimmedLine.match(/^-\s+\[[ xX]\]\s/)) { + hasAnyMarker = true; + } else { allLinesHaveMarker = false; } } else if (before === "> ") { // Для цитаты - if (!trimmedLine.startsWith("> ")) { + if (trimmedLine.startsWith("> ")) { + hasAnyMarker = true; + } else { allLinesHaveMarker = false; } } @@ -619,6 +628,7 @@ export const NoteItem: React.FC = ({ const lineHeight = parseInt(styles.lineHeight) || 20; const paddingTop = parseInt(styles.paddingTop) || 0; const paddingLeft = parseInt(styles.paddingLeft) || 0; + const fontSize = parseInt(styles.fontSize) || 14; // Более точный расчет ширины символа // Создаем временный элемент для измерения @@ -741,12 +751,14 @@ export const NoteItem: React.FC = ({ const lines = text.split("\n"); // Определяем текущую строку + let currentLineIndex = 0; let currentLineStart = 0; let currentLine = ""; for (let i = 0; i < lines.length; i++) { const lineLength = lines[i].length; if (currentLineStart + lineLength >= start) { + currentLineIndex = i; currentLine = lines[i]; break; } @@ -1374,6 +1386,11 @@ export const NoteItem: React.FC = ({ {note.files .filter((file) => !deletedFileIds.includes(file.id)) .map((file) => { + const fileUrl = getFileUrl( + file.file_path, + note.id, + file.id + ); return (
void; } -export const NotesList = forwardRef((_props, ref) => { +export const NotesList = forwardRef((props, ref) => { const notes = useAppSelector((state) => state.notes.notes); const userId = useAppSelector((state) => state.auth.userId); const searchQuery = useAppSelector((state) => state.notes.searchQuery); diff --git a/src/components/search/SearchBar.tsx b/src/components/search/SearchBar.tsx index f5d6960..25d067e 100644 --- a/src/components/search/SearchBar.tsx +++ b/src/components/search/SearchBar.tsx @@ -6,7 +6,7 @@ import { setSearchQuery } from "../../store/slices/notesSlice"; export const SearchBar: React.FC = () => { const [query, setQuery] = useState(""); const dispatch = useAppDispatch(); - const timeoutRef = useRef | null>(null); + const timeoutRef = useRef(null); useEffect(() => { // Debounce для поиска diff --git a/src/hooks/useNotification.ts b/src/hooks/useNotification.ts index 2e455bc..f7e8422 100644 --- a/src/hooks/useNotification.ts +++ b/src/hooks/useNotification.ts @@ -10,8 +10,7 @@ export const useNotification = () => { message: string, type: "info" | "success" | "error" | "warning" = "info" ) => { - const id = `notification-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - dispatch(addNotification({ id, message, type })); + const id = dispatch(addNotification({ message, type })).payload.id; setTimeout(() => { dispatch(removeNotification(id)); }, 4000); diff --git a/src/main.tsx b/src/main.tsx index ea594df..bce2974 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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,46 +23,28 @@ async function initOfflineMode() { await dbManager.init(); console.log('[Init] IndexedDB initialized'); - // Устанавливаем начальное состояние на основе 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)); - }); + // Проверка состояния сети + const isOnline = await checkNetworkStatus(); + store.dispatch(setOfflineMode(!isOnline)); + console.log(`[Init] Network status: ${isOnline ? 'online' : 'offline'}`); // Установка listeners для событий сети networkListener.onOnline(async () => { console.log('[Network] Online event detected'); - // Небольшая задержка перед проверкой для стабилизации соединения - 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); + const isOnline = await checkNetworkStatus(); + store.dispatch(setOfflineMode(!isOnline)); + + if (isOnline) { + store.dispatch( + addNotification({ + message: 'Подключение восстановлено, начинаем синхронизацию...', + type: 'info', + }) + ); + + // Запуск синхронизации + await syncService.startSync(); + } }); networkListener.onOffline(() => { @@ -77,40 +59,29 @@ async function initOfflineMode() { }); // Обновление счетчика ожидающих синхронизацию - dbManager.getPendingSyncCount().then((pendingCount) => { - store.dispatch(setPendingSyncCount(pendingCount)); - - if (pendingCount > 0) { - console.log(`[Init] Found ${pendingCount} pending sync items`); - } + const pendingCount = await dbManager.getPendingSyncCount(); + store.dispatch(setPendingSyncCount(pendingCount)); + + if (pendingCount > 0) { + console.log(`[Init] Found ${pendingCount} pending sync items`); + } - // Автоматическая синхронизация при старте если есть что синхронизировать - // Проверяем статус сети перед синхронизацией - 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); - }); + // Автоматическая синхронизация при старте если есть что синхронизировать + if (isOnline && pendingCount > 0) { + console.log('[Init] Starting initial sync...'); + // Небольшая задержка для инициализации UI + setTimeout(() => { + syncService.startSync(); + }, 2000); + } 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( diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx index 970a44a..8a87862 100644 --- a/src/pages/ProfilePage.tsx +++ b/src/pages/ProfilePage.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useRef } from "react"; import { useNavigate } from "react-router-dom"; import { Icon } from "@iconify/react"; -import { useAppDispatch } from "../store/hooks"; +import { useAppSelector, useAppDispatch } from "../store/hooks"; import { userApi } from "../api/userApi"; import { authApi } from "../api/authApi"; import { clearAuth } from "../store/slices/authSlice"; @@ -16,6 +16,7 @@ const ProfilePage: React.FC = () => { const navigate = useNavigate(); const dispatch = useAppDispatch(); const { showNotification } = useNotification(); + const user = useAppSelector((state) => state.profile.user); const [username, setUsername] = useState(""); const [email, setEmail] = useState(""); diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 5d35bcb..157b8f2 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -13,6 +13,7 @@ import { setAccentColor } from "../utils/colorUtils"; import { useNotification } from "../hooks/useNotification"; import { Modal } from "../components/common/Modal"; import { ThemeToggle } from "../components/common/ThemeToggle"; +import { formatDateFromTimestamp } from "../utils/dateFormat"; import { parseMarkdown } from "../utils/markdown"; import { dbManager } from "../utils/indexedDB"; import { syncService } from "../services/syncService"; @@ -25,6 +26,7 @@ const SettingsPage: React.FC = () => { const dispatch = useAppDispatch(); const { showNotification } = useNotification(); const user = useAppSelector((state) => state.profile.user); + const accentColor = useAppSelector((state) => state.ui.accentColor); const [activeTab, setActiveTab] = useState(() => { // Восстанавливаем активную вкладку из localStorage при инициализации @@ -162,8 +164,8 @@ const SettingsPage: React.FC = () => { try { await userApi.updateProfile({ accent_color: selectedAccentColor, - show_edit_date: showEditDate ? 1 : 0, - colored_icons: coloredIcons ? 1 : 0, + show_edit_date: showEditDate, + colored_icons: coloredIcons, }); dispatch(setAccentColorAction(selectedAccentColor)); setAccentColor(selectedAccentColor); @@ -271,7 +273,7 @@ const SettingsPage: React.FC = () => { } }; - const handleRestoreNote = async (id: number | string) => { + const handleRestoreNote = async (id: number) => { try { await notesApi.unarchive(id); await loadArchivedNotes(); @@ -285,7 +287,7 @@ const SettingsPage: React.FC = () => { } }; - const handleDeletePermanent = async (id: number | string) => { + const handleDeletePermanent = async (id: number) => { try { await notesApi.deleteArchived(id); await loadArchivedNotes(); @@ -418,8 +420,8 @@ const SettingsPage: React.FC = () => { // Загружаем версию из IndexedDB try { - const userId = (user as any)?.id; - const localVer = userId && typeof userId === 'number' + const userId = user?.id; + const localVer = userId ? await dbManager.getDataVersionByUserId(userId) : await dbManager.getDataVersion(); setIndexedDBVersion(localVer); diff --git a/src/services/syncService.ts b/src/services/syncService.ts index c08f2f7..45e023c 100644 --- a/src/services/syncService.ts +++ b/src/services/syncService.ts @@ -5,11 +5,13 @@ import { Note, NoteImage, NoteFile } from '../types/note'; import { store } from '../store/index'; import { setSyncStatus, + removeNotification, addNotification, } from '../store/slices/uiSlice'; import { updateNote, setPendingSyncCount, + setOfflineMode, } from '../store/slices/notesSlice'; import { SyncQueueItem } from '../types/note'; @@ -18,7 +20,7 @@ const RETRY_DELAY_MS = 5000; class SyncService { private isSyncing = false; - private syncTimer: ReturnType | null = null; + private syncTimer: NodeJS.Timeout | null = null; private listeners: Array<() => void> = []; /** @@ -442,7 +444,7 @@ class SyncService { */ private async updateImageReferences( localNote: Note, - _serverNote: Note + serverNote: Note ): Promise { // Если нет изображений с base64, возвращаем как есть const hasBase64Images = localNote.images.some((img) => img.base64Data); @@ -459,7 +461,7 @@ class SyncService { */ private async updateFileReferences( localNote: Note, - _serverNote: Note + serverNote: Note ): Promise { // Если нет файлов с base64, возвращаем как есть const hasBase64Files = localNote.files.some((file) => file.base64Data); diff --git a/src/store/slices/authSlice.ts b/src/store/slices/authSlice.ts index 8742eec..138a0cb 100644 --- a/src/store/slices/authSlice.ts +++ b/src/store/slices/authSlice.ts @@ -9,7 +9,7 @@ interface AuthState { const initialState: AuthState = { isAuthenticated: localStorage.getItem("isAuthenticated") === "true", - userId: localStorage.getItem("userId") ? parseInt(localStorage.getItem("userId")!, 10) : null, + userId: null, username: localStorage.getItem("username") || null, loading: false, }; @@ -26,7 +26,6 @@ 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) => { @@ -34,7 +33,6 @@ const authSlice = createSlice({ state.userId = null; state.username = null; localStorage.removeItem("isAuthenticated"); - localStorage.removeItem("userId"); localStorage.removeItem("username"); }, }, diff --git a/src/store/slices/uiSlice.ts b/src/store/slices/uiSlice.ts index d7b1d40..980460b 100644 --- a/src/store/slices/uiSlice.ts +++ b/src/store/slices/uiSlice.ts @@ -50,11 +50,9 @@ const uiSlice = createSlice({ }, addNotification: ( state, - action: PayloadAction | Notification> + action: PayloadAction> ) => { - const id = ('id' in action.payload && action.payload.id) - ? action.payload.id - : `notification-${Date.now()}-${Math.random() + const id = `notification-${Date.now()}-${Math.random() .toString(36) .substr(2, 9)}`; state.notifications.push({ ...action.payload, id }); diff --git a/src/styles/style.css b/src/styles/style.css index e071cf4..32f9652 100644 --- a/src/styles/style.css +++ b/src/styles/style.css @@ -118,6 +118,7 @@ body { button { -webkit-tap-highlight-color: transparent; -moz-tap-highlight-color: transparent; + tap-highlight-color: transparent; outline: none; box-shadow: none; } @@ -136,6 +137,7 @@ a, div[role="button"] { -webkit-tap-highlight-color: transparent; -moz-tap-highlight-color: transparent; + tap-highlight-color: transparent; outline: none; box-shadow: none; } diff --git a/src/utils/filePaths.ts b/src/utils/filePaths.ts index 9962a4a..40b34c7 100644 --- a/src/utils/filePaths.ts +++ b/src/utils/filePaths.ts @@ -11,8 +11,8 @@ */ export function getImageUrl( filePath: string, - noteId: number | string, - imageId: number | string + noteId: number, + imageId: number ): string { // Если путь уже является полным URL (начинается с http:// или https://) if (filePath.startsWith("http://") || filePath.startsWith("https://")) { @@ -47,8 +47,8 @@ export function getImageUrl( */ export function getFileUrl( filePath: string, - noteId: number | string, - fileId: number | string + noteId: number, + fileId: number ): string { // Если путь уже является полным URL (начинается с http:// или https://) if (filePath.startsWith("http://") || filePath.startsWith("https://")) { diff --git a/src/utils/markdown.ts b/src/utils/markdown.ts index 409690f..6f5f4df 100644 --- a/src/utils/markdown.ts +++ b/src/utils/markdown.ts @@ -24,113 +24,50 @@ const spoilerExtension = { }; // Кастомный renderer для внешних ссылок и чекбоксов -const renderer = new marked.Renderer(); +const renderer: any = { + link(token: any) { + const href = token.href; + const title = token.title; + const text = token.text; -// Переопределяем link для внешних ссылок -const originalLink = renderer.link.bind(renderer); -renderer.link = function(token: any) { - const href = token.href; - const title = token.title; - const text = token.text; - - try { - const url = new URL(href, window.location.href); - const isExternal = url.origin !== window.location.origin; - - if (isExternal) { - return `${text}`; - } - } catch {} - - return originalLink(token); -}; - -// Переопределяем listitem для поддержки чекбоксов -renderer.listitem = function(token: any) { - const task = token.task; - const checked = token.checked; - - // Получаем токены для обработки - const tokens = token.tokens || []; - let text: string; - - // Блоковые типы токенов, которые нельзя обрабатывать через parseInline - const blockTypes = ['list', 'blockquote', 'code', 'heading', 'paragraph', 'hr', 'table']; - - // Обрабатываем токены вручную, избегая parseInline для блоковых элементов - if (tokens.length > 0) { try { - // Разделяем токены на inline и блоковые - const inlineTokens: any[] = []; - const blockTokens: any[] = []; - - tokens.forEach((t: any) => { - if (blockTypes.includes(t.type)) { - blockTokens.push(t); - } else { - inlineTokens.push(t); - } - }); - - // Обрабатываем inline токены только если они есть - let inlineText = ''; - if (inlineTokens.length > 0) { - try { - inlineText = this.parser.parseInline(inlineTokens); - } catch (inlineError) { - // Если ошибка при обработке inline, просто игнорируем их - console.warn('Error parsing inline tokens in listitem:', inlineError); - } - } - - // Обрабатываем блоковые токены через parser - let blockText = ''; - if (blockTokens.length > 0) { - try { - blockText = this.parser.parse(blockTokens); - } catch (blockError) { - // Если ошибка при обработке блоков, обрабатываем через стандартный renderer - console.warn('Error parsing block tokens in listitem:', blockError); - // Пытаемся обработать каждый блок отдельно - blockText = blockTokens.map((bt: any) => { - try { - return this.parser.parse([bt]); - } catch { - return ''; - } - }).join(''); - } - } - - text = inlineText + blockText; - - // Если после обработки текст пустой, используем fallback - if (!text || text.trim() === '') { - text = token.text || ''; - } - } catch (error) { - // Если общая ошибка, используем fallback - обрабатываем через стандартный parser - try { - text = this.parser.parse(tokens); - } catch (parseError) { - // Последний fallback - используем raw text - console.warn('Error parsing list item tokens:', parseError); - text = token.text || token.raw || ''; + const url = new URL(href, window.location.href); + const isExternal = url.origin !== window.location.origin; + + if (isExternal) { + return `${text}`; } + } catch {} + + return `${text}`; + }, + // Кастомный renderer для элементов списка с чекбоксами + listitem(token: any) { + 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 || ''; } - } else { - text = token.text || ''; - } - - // Если это задача (чекбокс), добавляем чекбокс - if (task) { - const checkbox = ``; - return `
  • ${checkbox} ${text}
  • \n`; - } - - return `
  • ${text}
  • \n`; + + if (task) { + const checkbox = ``; + return `
  • ${checkbox} ${text}
  • \n`; + } + return `
  • ${text}
  • \n`; + }, }; // Настройка marked diff --git a/src/utils/offlineManager.ts b/src/utils/offlineManager.ts index df5025c..5bfb948 100644 --- a/src/utils/offlineManager.ts +++ b/src/utils/offlineManager.ts @@ -39,8 +39,6 @@ export function waitForIndexedDB(): Promise { /** * Проверка состояния сети (более надежный метод) - * Не блокирует выполнение, всегда возвращает результат - * Различает "нет интернета" (offline) и "не авторизован" (401) */ export async function checkNetworkStatus(): Promise { // Простая проверка navigator.onLine @@ -51,11 +49,10 @@ export async function checkNetworkStatus(): Promise { // Дополнительная проверка через fetch с коротким таймаутом try { const controller = new AbortController(); - // Уменьшаем таймаут для более быстрого ответа - const timeoutId = setTimeout(() => controller.abort(), 1500); + const timeoutId = setTimeout(() => controller.abort(), 2000); // Используем /auth/status так как он всегда доступен при наличии сети - await fetch('/api/auth/status', { + const response = await fetch('/api/auth/status', { method: 'GET', signal: controller.signal, cache: 'no-cache', @@ -63,27 +60,10 @@ export async function checkNetworkStatus(): Promise { }); clearTimeout(timeoutId); - - // Если получили ответ (даже 401), значит интернет есть - // 401 означает "не авторизован", но НЕ "нет интернета" - // Любой ответ (даже ошибка) означает, что сеть работает - return true; + return response.ok; } catch (error) { - // Если запрос не удался из-за таймаута или сетевой ошибки - // (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; - } - } - // В остальных случаях считаем, что интернет есть - // (возможно, просто ошибка авторизации или другая проблема) + // Если запрос не удался, но navigator.onLine = true, считаем что онлайн + // (возможно, просто таймаут или другая проблема) return navigator.onLine; } } diff --git a/vite.config.ts b/vite.config.ts index 5e1039e..d7e1ff5 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -12,10 +12,7 @@ export default defineConfig({ "icon.svg", "icons/icon-192x192.png", "icons/icon-512x512.png", - "manifest.json", ], - // Включаем стратегию для offline работы - strategies: "generateSW", manifest: { name: "NoteJS - Система заметок", short_name: "NoteJS", @@ -91,41 +88,8 @@ export default defineConfig({ ], }, workbox: { - // Кэшируем все статические ресурсы для offline работы - globPatterns: ["**/*.{js,css,html,ico,png,svg,woff,woff2,ttf,eot,json}"], - // Стратегия для главной страницы - CacheFirst для offline работы - navigateFallback: "/index.html", - navigateFallbackDenylist: [/^\/api/, /^\/_/], - // Кэширование для offline работы приложения + globPatterns: ["**/*.{js,css,html,ico,png,svg,woff,woff2,ttf,eot}"], 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", @@ -135,7 +99,6 @@ export default defineConfig({ maxEntries: 50, maxAgeSeconds: 60 * 60, // 1 hour }, - networkTimeoutSeconds: 3, }, }, { @@ -147,7 +110,7 @@ export default defineConfig({ maxEntries: 100, maxAgeSeconds: 24 * 60 * 60, // 24 hours }, - networkTimeoutSeconds: 3, + networkTimeoutSeconds: 10, }, }, {