Обновлены API для работы с заметками и пользователями, добавлена поддержка идентификаторов в виде строк. Изменены параметры в компонентах для улучшения обработки данных. Оптимизированы стили и логика компонентов для повышения производительности и улучшения пользовательского опыта.
This commit is contained in:
parent
4b3bc3e024
commit
5b3e41d1b6
@ -82,7 +82,7 @@ define(['./workbox-9dc17825'], (function (workbox) { 'use strict';
|
||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||
}, {
|
||||
"url": "/index.html",
|
||||
"revision": "0.6dag44kodlo"
|
||||
"revision": "0.ri1maclacqo"
|
||||
}], {});
|
||||
workbox.cleanupOutdatedCaches();
|
||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("/index.html"), {
|
||||
|
||||
@ -43,7 +43,7 @@ export const notesApi = {
|
||||
return data;
|
||||
},
|
||||
|
||||
unarchive: async (id: number) => {
|
||||
unarchive: async (id: number | string) => {
|
||||
const { data } = await axiosClient.put(`/notes/${id}/unarchive`);
|
||||
return data;
|
||||
},
|
||||
@ -89,7 +89,7 @@ export const notesApi = {
|
||||
return data;
|
||||
},
|
||||
|
||||
deleteArchived: async (id: number) => {
|
||||
deleteArchived: async (id: number | string) => {
|
||||
await axiosClient.delete(`/notes/archived/${id}`);
|
||||
},
|
||||
|
||||
|
||||
@ -12,8 +12,8 @@ export const userApi = {
|
||||
currentPassword?: string;
|
||||
newPassword?: string;
|
||||
accent_color?: string;
|
||||
show_edit_date?: boolean;
|
||||
colored_icons?: boolean;
|
||||
show_edit_date?: boolean | number;
|
||||
colored_icons?: boolean | number;
|
||||
}
|
||||
) => {
|
||||
const { data } = await axiosClient.put("/user/profile", profile);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
format,
|
||||
startOfMonth,
|
||||
|
||||
@ -2,7 +2,6 @@ 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 {
|
||||
|
||||
@ -113,7 +113,14 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
|
||||
};
|
||||
}, [isDragging]);
|
||||
|
||||
const buttons = [];
|
||||
const buttons: Array<{
|
||||
id: string;
|
||||
action?: () => void;
|
||||
before?: string;
|
||||
after?: string;
|
||||
title?: string;
|
||||
icon?: string;
|
||||
}> = [];
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -142,7 +149,7 @@ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
|
||||
}}
|
||||
title={btn.title}
|
||||
>
|
||||
<Icon icon={btn.icon} />
|
||||
{btn.icon && <Icon icon={btn.icon} />}
|
||||
</button>
|
||||
))}
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import { FloatingToolbar } from "./FloatingToolbar";
|
||||
import { NotePreview } from "./NotePreview";
|
||||
import { ImageUpload } from "./ImageUpload";
|
||||
import { FileUpload } from "./FileUpload";
|
||||
import { useAppSelector, useAppDispatch } from "../../store/hooks";
|
||||
import { useAppSelector } from "../../store/hooks";
|
||||
import { useNotification } from "../../hooks/useNotification";
|
||||
import { offlineNotesApi } from "../../api/offlineNotesApi";
|
||||
import { aiApi } from "../../api/aiApi";
|
||||
@ -31,7 +31,6 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ 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()) {
|
||||
@ -251,36 +250,27 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
||||
|
||||
// Определяем, есть ли уже такие маркеры на всех строках
|
||||
let allLinesHaveMarker = true;
|
||||
let hasAnyMarker = false;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trimStart();
|
||||
if (before === "- ") {
|
||||
// Для маркированного списка проверяем различные варианты
|
||||
if (trimmedLine.match(/^[-*+]\s/)) {
|
||||
hasAnyMarker = true;
|
||||
} else {
|
||||
if (!trimmedLine.match(/^[-*+]\s/)) {
|
||||
allLinesHaveMarker = false;
|
||||
}
|
||||
} else if (before === "1. ") {
|
||||
// Для нумерованного списка
|
||||
if (trimmedLine.match(/^\d+\.\s/)) {
|
||||
hasAnyMarker = true;
|
||||
} else {
|
||||
if (!trimmedLine.match(/^\d+\.\s/)) {
|
||||
allLinesHaveMarker = false;
|
||||
}
|
||||
} else if (before === "- [ ] ") {
|
||||
// Для чекбокса
|
||||
if (trimmedLine.match(/^-\s+\[[ xX]\]\s/)) {
|
||||
hasAnyMarker = true;
|
||||
} else {
|
||||
if (!trimmedLine.match(/^-\s+\[[ xX]\]\s/)) {
|
||||
allLinesHaveMarker = false;
|
||||
}
|
||||
} else if (before === "> ") {
|
||||
// Для цитаты
|
||||
if (trimmedLine.startsWith("> ")) {
|
||||
hasAnyMarker = true;
|
||||
} else {
|
||||
if (!trimmedLine.startsWith("> ")) {
|
||||
allLinesHaveMarker = false;
|
||||
}
|
||||
}
|
||||
@ -530,14 +520,12 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ 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;
|
||||
}
|
||||
@ -668,7 +656,6 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ 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;
|
||||
|
||||
// Более точный расчет ширины символа
|
||||
// Создаем временный элемент для измерения
|
||||
|
||||
@ -31,7 +31,7 @@ interface NoteItemProps {
|
||||
|
||||
export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
note,
|
||||
onDelete,
|
||||
onDelete: _onDelete,
|
||||
onPin,
|
||||
onArchive,
|
||||
onReload,
|
||||
@ -41,8 +41,8 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
const [showArchiveModal, setShowArchiveModal] = useState(false);
|
||||
const [editImages, setEditImages] = useState<File[]>([]);
|
||||
const [editFiles, setEditFiles] = useState<File[]>([]);
|
||||
const [deletedImageIds, setDeletedImageIds] = useState<number[]>([]);
|
||||
const [deletedFileIds, setDeletedFileIds] = useState<number[]>([]);
|
||||
const [deletedImageIds, setDeletedImageIds] = useState<(number | string)[]>([]);
|
||||
const [deletedFileIds, setDeletedFileIds] = useState<(number | string)[]>([]);
|
||||
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<NoteItemProps> = ({
|
||||
setLocalPreviewMode(false);
|
||||
};
|
||||
|
||||
const handleDeleteExistingImage = (imageId: number) => {
|
||||
const handleDeleteExistingImage = (imageId: number | string) => {
|
||||
setDeletedImageIds([...deletedImageIds, imageId]);
|
||||
};
|
||||
|
||||
const handleDeleteExistingFile = (fileId: number) => {
|
||||
const handleDeleteExistingFile = (fileId: number | string) => {
|
||||
setDeletedFileIds([...deletedFileIds, fileId]);
|
||||
};
|
||||
|
||||
const handleRestoreImage = (imageId: number) => {
|
||||
const handleRestoreImage = (imageId: number | string) => {
|
||||
setDeletedImageIds(deletedImageIds.filter((id) => id !== imageId));
|
||||
};
|
||||
|
||||
const handleRestoreFile = (fileId: number) => {
|
||||
const handleRestoreFile = (fileId: number | string) => {
|
||||
setDeletedFileIds(deletedFileIds.filter((id) => id !== fileId));
|
||||
};
|
||||
|
||||
@ -337,36 +337,27 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
|
||||
// Определяем, есть ли уже такие маркеры на всех строках
|
||||
let allLinesHaveMarker = true;
|
||||
let hasAnyMarker = false;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trimStart();
|
||||
if (before === "- ") {
|
||||
// Для маркированного списка проверяем различные варианты
|
||||
if (trimmedLine.match(/^[-*+]\s/)) {
|
||||
hasAnyMarker = true;
|
||||
} else {
|
||||
if (!trimmedLine.match(/^[-*+]\s/)) {
|
||||
allLinesHaveMarker = false;
|
||||
}
|
||||
} else if (before === "1. ") {
|
||||
// Для нумерованного списка
|
||||
if (trimmedLine.match(/^\d+\.\s/)) {
|
||||
hasAnyMarker = true;
|
||||
} else {
|
||||
if (!trimmedLine.match(/^\d+\.\s/)) {
|
||||
allLinesHaveMarker = false;
|
||||
}
|
||||
} else if (before === "- [ ] ") {
|
||||
// Для чекбокса
|
||||
if (trimmedLine.match(/^-\s+\[[ xX]\]\s/)) {
|
||||
hasAnyMarker = true;
|
||||
} else {
|
||||
if (!trimmedLine.match(/^-\s+\[[ xX]\]\s/)) {
|
||||
allLinesHaveMarker = false;
|
||||
}
|
||||
} else if (before === "> ") {
|
||||
// Для цитаты
|
||||
if (trimmedLine.startsWith("> ")) {
|
||||
hasAnyMarker = true;
|
||||
} else {
|
||||
if (!trimmedLine.startsWith("> ")) {
|
||||
allLinesHaveMarker = false;
|
||||
}
|
||||
}
|
||||
@ -628,7 +619,6 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
const lineHeight = parseInt(styles.lineHeight) || 20;
|
||||
const paddingTop = parseInt(styles.paddingTop) || 0;
|
||||
const paddingLeft = parseInt(styles.paddingLeft) || 0;
|
||||
const fontSize = parseInt(styles.fontSize) || 14;
|
||||
|
||||
// Более точный расчет ширины символа
|
||||
// Создаем временный элемент для измерения
|
||||
@ -751,14 +741,12 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
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;
|
||||
}
|
||||
@ -1386,11 +1374,6 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
||||
{note.files
|
||||
.filter((file) => !deletedFileIds.includes(file.id))
|
||||
.map((file) => {
|
||||
const fileUrl = getFileUrl(
|
||||
file.file_path,
|
||||
note.id,
|
||||
file.id
|
||||
);
|
||||
return (
|
||||
<div key={file.id} className="file-preview-item">
|
||||
<Icon
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useImperativeHandle, forwardRef } from "react";
|
||||
import { useEffect, useImperativeHandle, forwardRef } from "react";
|
||||
import { NoteItem } from "./NoteItem";
|
||||
import { useAppSelector, useAppDispatch } from "../../store/hooks";
|
||||
import { offlineNotesApi } from "../../api/offlineNotesApi";
|
||||
@ -10,7 +10,7 @@ export interface NotesListRef {
|
||||
reloadNotes: () => void;
|
||||
}
|
||||
|
||||
export const NotesList = forwardRef<NotesListRef>((props, ref) => {
|
||||
export const NotesList = forwardRef<NotesListRef>((_props, ref) => {
|
||||
const notes = useAppSelector((state) => state.notes.notes);
|
||||
const userId = useAppSelector((state) => state.auth.userId);
|
||||
const searchQuery = useAppSelector((state) => state.notes.searchQuery);
|
||||
|
||||
@ -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<NodeJS.Timeout | null>(null);
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Debounce для поиска
|
||||
|
||||
@ -10,7 +10,8 @@ export const useNotification = () => {
|
||||
message: string,
|
||||
type: "info" | "success" | "error" | "warning" = "info"
|
||||
) => {
|
||||
const id = dispatch(addNotification({ message, type })).payload.id;
|
||||
const id = `notification-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
dispatch(addNotification({ id, message, type }));
|
||||
setTimeout(() => {
|
||||
dispatch(removeNotification(id));
|
||||
}, 4000);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Icon } from "@iconify/react";
|
||||
import { useAppSelector, useAppDispatch } from "../store/hooks";
|
||||
import { useAppDispatch } from "../store/hooks";
|
||||
import { userApi } from "../api/userApi";
|
||||
import { authApi } from "../api/authApi";
|
||||
import { clearAuth } from "../store/slices/authSlice";
|
||||
@ -16,7 +16,6 @@ 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("");
|
||||
|
||||
@ -13,7 +13,6 @@ 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";
|
||||
@ -26,7 +25,6 @@ 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<SettingsTab>(() => {
|
||||
// Восстанавливаем активную вкладку из localStorage при инициализации
|
||||
@ -164,8 +162,8 @@ const SettingsPage: React.FC = () => {
|
||||
try {
|
||||
await userApi.updateProfile({
|
||||
accent_color: selectedAccentColor,
|
||||
show_edit_date: showEditDate,
|
||||
colored_icons: coloredIcons,
|
||||
show_edit_date: showEditDate ? 1 : 0,
|
||||
colored_icons: coloredIcons ? 1 : 0,
|
||||
});
|
||||
dispatch(setAccentColorAction(selectedAccentColor));
|
||||
setAccentColor(selectedAccentColor);
|
||||
@ -273,7 +271,7 @@ const SettingsPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestoreNote = async (id: number) => {
|
||||
const handleRestoreNote = async (id: number | string) => {
|
||||
try {
|
||||
await notesApi.unarchive(id);
|
||||
await loadArchivedNotes();
|
||||
@ -287,7 +285,7 @@ const SettingsPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeletePermanent = async (id: number) => {
|
||||
const handleDeletePermanent = async (id: number | string) => {
|
||||
try {
|
||||
await notesApi.deleteArchived(id);
|
||||
await loadArchivedNotes();
|
||||
@ -420,8 +418,8 @@ const SettingsPage: React.FC = () => {
|
||||
|
||||
// Загружаем версию из IndexedDB
|
||||
try {
|
||||
const userId = user?.id;
|
||||
const localVer = userId
|
||||
const userId = (user as any)?.id;
|
||||
const localVer = userId && typeof userId === 'number'
|
||||
? await dbManager.getDataVersionByUserId(userId)
|
||||
: await dbManager.getDataVersion();
|
||||
setIndexedDBVersion(localVer);
|
||||
|
||||
@ -5,13 +5,11 @@ 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';
|
||||
|
||||
@ -20,7 +18,7 @@ const RETRY_DELAY_MS = 5000;
|
||||
|
||||
class SyncService {
|
||||
private isSyncing = false;
|
||||
private syncTimer: NodeJS.Timeout | null = null;
|
||||
private syncTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private listeners: Array<() => void> = [];
|
||||
|
||||
/**
|
||||
@ -444,7 +442,7 @@ class SyncService {
|
||||
*/
|
||||
private async updateImageReferences(
|
||||
localNote: Note,
|
||||
serverNote: Note
|
||||
_serverNote: Note
|
||||
): Promise<NoteImage[]> {
|
||||
// Если нет изображений с base64, возвращаем как есть
|
||||
const hasBase64Images = localNote.images.some((img) => img.base64Data);
|
||||
@ -461,7 +459,7 @@ class SyncService {
|
||||
*/
|
||||
private async updateFileReferences(
|
||||
localNote: Note,
|
||||
serverNote: Note
|
||||
_serverNote: Note
|
||||
): Promise<NoteFile[]> {
|
||||
// Если нет файлов с base64, возвращаем как есть
|
||||
const hasBase64Files = localNote.files.some((file) => file.base64Data);
|
||||
|
||||
@ -50,9 +50,11 @@ const uiSlice = createSlice({
|
||||
},
|
||||
addNotification: (
|
||||
state,
|
||||
action: PayloadAction<Omit<Notification, "id">>
|
||||
action: PayloadAction<Omit<Notification, "id"> | Notification>
|
||||
) => {
|
||||
const id = `notification-${Date.now()}-${Math.random()
|
||||
const id = ('id' in action.payload && action.payload.id)
|
||||
? action.payload.id
|
||||
: `notification-${Date.now()}-${Math.random()
|
||||
.toString(36)
|
||||
.substr(2, 9)}`;
|
||||
state.notifications.push({ ...action.payload, id });
|
||||
|
||||
@ -11,8 +11,8 @@
|
||||
*/
|
||||
export function getImageUrl(
|
||||
filePath: string,
|
||||
noteId: number,
|
||||
imageId: number
|
||||
noteId: number | string,
|
||||
imageId: number | string
|
||||
): 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,
|
||||
fileId: number
|
||||
noteId: number | string,
|
||||
fileId: number | string
|
||||
): string {
|
||||
// Если путь уже является полным URL (начинается с http:// или https://)
|
||||
if (filePath.startsWith("http://") || filePath.startsWith("https://")) {
|
||||
|
||||
@ -55,7 +55,7 @@ export async function checkNetworkStatus(): Promise<boolean> {
|
||||
const timeoutId = setTimeout(() => controller.abort(), 1500);
|
||||
|
||||
// Используем /auth/status так как он всегда доступен при наличии сети
|
||||
const response = await fetch('/api/auth/status', {
|
||||
await fetch('/api/auth/status', {
|
||||
method: 'GET',
|
||||
signal: controller.signal,
|
||||
cache: 'no-cache',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user