From 143338bc2bc17059a63030cb967995a5fa0c9e6a Mon Sep 17 00:00:00 2001 From: Fovway Date: Sun, 2 Nov 2025 23:35:22 +0700 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20=D1=84?= =?UTF-8?q?=D0=B8=D0=BB=D1=8C=D1=82=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BC=D0=B5=D1=82=D0=BE=D0=BA=20=D0=BF=D0=BE=20=D1=82?= =?UTF-8?q?=D0=B5=D0=B3=D0=B0=D0=BC=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=BE=D0=BD=D0=B5=D0=B7=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D1=81=D0=B8=D0=BC=D0=BE=D0=B3=D0=BE=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B8=D1=81=D0=BA=D0=B0.=20=D0=A4=D0=B8=D0=BB=D1=8C=D1=82?= =?UTF-8?q?=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BF=D0=BE=20=D1=82=D0=B5?= =?UTF-8?q?=D0=B3=D0=B0=D0=BC=20=D1=82=D0=B5=D0=BF=D0=B5=D1=80=D1=8C=20?= =?UTF-8?q?=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D1=8F=D0=B5=D1=82=D1=81?= =?UTF-8?q?=D1=8F=20=D0=BD=D0=B0=20=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B5,=20=D1=87=D1=82=D0=BE=D0=B1=D1=8B=20=D0=B8=D0=B7=D0=B1?= =?UTF-8?q?=D0=B5=D0=B6=D0=B0=D1=82=D1=8C=20=D0=BF=D1=80=D0=BE=D0=B1=D0=BB?= =?UTF-8?q?=D0=B5=D0=BC=20=D1=81=20=D0=BA=D0=B8=D1=80=D0=B8=D0=BB=D0=BB?= =?UTF-8?q?=D0=B8=D1=86=D0=B5=D0=B9=20=D0=B2=20SQLite.=20=D0=A2=D0=B0?= =?UTF-8?q?=D0=BA=D0=B6=D0=B5=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=20=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D1=82=D0=B5=D0=B3=D0=BE=D0=B2=20=D0=B2=20=D0=BD?= =?UTF-8?q?=D0=B8=D0=B6=D0=BD=D0=B5=D0=BC=20=D1=80=D0=B5=D0=B3=D0=B8=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=B5=20=D0=B4=D0=BB=D1=8F=20=D0=B5=D0=B4=D0=B8?= =?UTF-8?q?=D0=BD=D0=BE=D0=BE=D0=B1=D1=80=D0=B0=D0=B7=D0=B8=D1=8F=20=D0=B8?= =?UTF-8?q?=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD=D0=B0=20=D0=BE?= =?UTF-8?q?=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B0=20=D1=83=D0=BD?= =?UTF-8?q?=D0=B8=D0=BA=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D1=85=20=D1=82=D0=B5?= =?UTF-8?q?=D0=B3=D0=BE=D0=B2=20=D0=B2=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D1=82=D0=B5=20TagsFilter.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/server.js | 12 +++++++----- dev-dist/sw.js | 2 +- src/components/notes/NoteItem.tsx | 3 ++- src/components/notes/NotesList.tsx | 11 +++++++++++ src/components/search/TagsFilter.tsx | 26 +++++++++++++++++++++----- src/utils/markdown.ts | 5 +++-- 6 files changed, 45 insertions(+), 14 deletions(-) diff --git a/backend/server.js b/backend/server.js index a8e1ec1..d35a9f8 100644 --- a/backend/server.js +++ b/backend/server.js @@ -803,11 +803,13 @@ app.get("/api/notes/search", requireApiAuth, (req, res) => { params.push(`%${q.trim()}%`); } - // Поиск по тегу - if (tag && tag.trim()) { - whereClause += " AND n.content LIKE ?"; - params.push(`%#${tag.trim()}%`); - } + // Поиск по тегу (регистронезависимый) + // SQLite LOWER() плохо работает с кириллицей, поэтому фильтрация по тегу выполняется на клиенте + // Здесь не фильтруем по тегу, чтобы получить все заметки для последующей фильтрации на клиенте + // Если есть другие фильтры (дата, поиск), они применяются здесь + // if (tag && tag.trim()) { + // // Фильтрация по тегу выполняется на клиенте + // } // Поиск по дате (используем created_at вместо date) if (date && date.trim()) { diff --git a/dev-dist/sw.js b/dev-dist/sw.js index 0fc6f17..0786127 100644 --- a/dev-dist/sw.js +++ b/dev-dist/sw.js @@ -85,7 +85,7 @@ define(['./workbox-8cfb3eb5'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "index.html", - "revision": "0.h9luj2l3mv8" + "revision": "0.tihabijh3s" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/src/components/notes/NoteItem.tsx b/src/components/notes/NoteItem.tsx index bdfd45e..09b57f0 100644 --- a/src/components/notes/NoteItem.tsx +++ b/src/components/notes/NoteItem.tsx @@ -978,7 +978,8 @@ export const NoteItem: React.FC = ({ const handleTagClick = (e: React.MouseEvent, tag: string) => { e.stopPropagation(); - dispatch(setSelectedTag(tag)); + // Всегда сохраняем тег в нижнем регистре для единообразия + dispatch(setSelectedTag(tag.toLowerCase())); }; const toggleExpand = () => { diff --git a/src/components/notes/NotesList.tsx b/src/components/notes/NotesList.tsx index 60e63f5..b69630e 100644 --- a/src/components/notes/NotesList.tsx +++ b/src/components/notes/NotesList.tsx @@ -4,6 +4,7 @@ import { useAppSelector, useAppDispatch } from "../../store/hooks"; import { notesApi } from "../../api/notesApi"; import { setNotes, setAllNotes } from "../../store/slices/notesSlice"; import { useNotification } from "../../hooks/useNotification"; +import { extractTags } from "../../utils/markdown"; export interface NotesListRef { reloadNotes: () => void; @@ -41,6 +42,16 @@ export const NotesList = forwardRef((props, ref) => { if (userId) { notesData = notesData.filter((note) => note.user_id === userId); } + + // Точная фильтрация по тегу на клиенте (регистронезависимо) + // SQLite LOWER() плохо работает с кириллицей, поэтому фильтруем здесь + if (selectedTag) { + const selectedTagLower = selectedTag.toLowerCase(); + notesData = notesData.filter((note) => { + const noteTags = extractTags(note.content); + return noteTags.some(tag => tag.toLowerCase() === selectedTagLower); + }); + } } else { // Если нет фильтров, используем все заметки notesData = filteredAllData; diff --git a/src/components/search/TagsFilter.tsx b/src/components/search/TagsFilter.tsx index 97d71c6..3d7d2c1 100644 --- a/src/components/search/TagsFilter.tsx +++ b/src/components/search/TagsFilter.tsx @@ -14,17 +14,30 @@ export const TagsFilter: React.FC = ({ notes = [] }) => { const dispatch = useAppDispatch(); // Получаем все уникальные теги из заметок + // Группируем регистронезависимо, но сохраняем оригинальный регистр первого вхождения const getAllTags = () => { const tagCounts: Record = {}; + const tagVariants: Record = {}; // Хранит оригинальный регистр для каждого тега notes.forEach((note) => { const tags = extractTags(note.content); tags.forEach((tag) => { - tagCounts[tag] = (tagCounts[tag] || 0) + 1; + const tagKey = tag.toLowerCase(); + tagCounts[tagKey] = (tagCounts[tagKey] || 0) + 1; + // Сохраняем первый вариант с оригинальным регистром + if (!tagVariants[tagKey]) { + tagVariants[tagKey] = tag; + } }); }); - return tagCounts; + // Преобразуем обратно в Record с оригинальными тегами + const result: Record = {}; + Object.keys(tagCounts).forEach((tagKey) => { + result[tagVariants[tagKey]] = tagCounts[tagKey]; + }); + + return result; }; const tagCounts = getAllTags(); @@ -32,10 +45,12 @@ export const TagsFilter: React.FC = ({ notes = [] }) => { const handleTagClick = (tag: string, event: React.MouseEvent) => { event.preventDefault(); - if (selectedTag === tag) { + // Всегда сохраняем тег в нижнем регистре для единообразия + const tagLower = tag.toLowerCase(); + if (selectedTag?.toLowerCase() === tagLower) { dispatch(setSelectedTag(null)); } else { - dispatch(setSelectedTag(tag)); + dispatch(setSelectedTag(tagLower)); } // Сбрасываем фокус после клика для предотвращения сохранения состояния :active (event.currentTarget as HTMLElement).blur(); @@ -68,7 +83,7 @@ export const TagsFilter: React.FC = ({ notes = [] }) => {
{sortedTags.map((tag) => { const count = tagCounts[tag]; - const isActive = selectedTag === tag; + const isActive = selectedTag?.toLowerCase() === tag.toLowerCase(); return ( = ({ notes = [] }) => {
); }; + diff --git a/src/utils/markdown.ts b/src/utils/markdown.ts index e5ec3d8..83b2075 100644 --- a/src/utils/markdown.ts +++ b/src/utils/markdown.ts @@ -109,8 +109,9 @@ export const extractTags = (content: string): string[] => { } } - const tag = match[1].toLowerCase(); - if (!tags.includes(tag)) { + const tag = match[1]; + // Проверяем, есть ли уже тег с таким же именем (регистронезависимо) + if (!tags.some(t => t.toLowerCase() === tag.toLowerCase())) { tags.push(tag); } }