feat: add search functionality to the notebook (#1318)
This commit is contained in:
@@ -344,5 +344,7 @@
|
||||
"Search in {{count}} Book(s)..._two": "بحث في كتابين...",
|
||||
"Search in {{count}} Book(s)..._few": "بحث في {{count}} كتب...",
|
||||
"Search in {{count}} Book(s)..._many": "بحث في {{count}} كتابًا...",
|
||||
"Search in {{count}} Book(s)..._other": "بحث في {{count}} من الكتب..."
|
||||
"Search in {{count}} Book(s)..._other": "بحث في {{count}} من الكتب...",
|
||||
"No notes match your search": "لا توجد ملاحظات تطابق بحثك",
|
||||
"Search notes and excerpts...": "بحث في الملاحظات والمقتطفات..."
|
||||
}
|
||||
|
||||
@@ -336,5 +336,7 @@
|
||||
"Enable only if you trust the file.": "Nur aktivieren, wenn Sie der Datei vertrauen.",
|
||||
"Sort TOC by Page": "TOC nach Seite sortieren",
|
||||
"Search in {{count}} Book(s)..._one": "Suche in {{count}} Buch...",
|
||||
"Search in {{count}} Book(s)..._other": "Suche in {{count}} Büchern..."
|
||||
"Search in {{count}} Book(s)..._other": "Suche in {{count}} Büchern...",
|
||||
"No notes match your search": "Noch keine Notizen gefunden",
|
||||
"Search notes and excerpts...": "Notizen und Auszüge suchen..."
|
||||
}
|
||||
|
||||
@@ -336,5 +336,7 @@
|
||||
"Enable only if you trust the file.": "Ενεργοποιήστε μόνο εάν εμπιστεύεστε το αρχείο.",
|
||||
"Sort TOC by Page": "Ταξινόμηση TOC κατά σελίδα",
|
||||
"Search in {{count}} Book(s)..._one": "Αναζήτηση σε {{count}} Βιβλίο...",
|
||||
"Search in {{count}} Book(s)..._other": "Αναζήτηση σε {{count}} Βιβλία..."
|
||||
"Search in {{count}} Book(s)..._other": "Αναζήτηση σε {{count}} Βιβλία...",
|
||||
"No notes match your search": "Καμία σημείωση δεν βρέθηκε",
|
||||
"Search notes and excerpts...": "Αναζήτηση σημειώσεων..."
|
||||
}
|
||||
|
||||
@@ -338,5 +338,7 @@
|
||||
"Sort TOC by Page": "Ordenar TOC por página",
|
||||
"Search in {{count}} Book(s)..._one": "Buscar en {{count}} libro...",
|
||||
"Search in {{count}} Book(s)..._many": "Buscar en {{count}} libros...",
|
||||
"Search in {{count}} Book(s)..._other": "Buscar en {{count}} libros..."
|
||||
"Search in {{count}} Book(s)..._other": "Buscar en {{count}} libros...",
|
||||
"No notes match your search": "No se encontraron notas",
|
||||
"Search notes and excerpts...": "Buscar notas..."
|
||||
}
|
||||
|
||||
@@ -338,5 +338,7 @@
|
||||
"Sort TOC by Page": "Tris la table des matières par page",
|
||||
"Search in {{count}} Book(s)..._one": "Rechercher dans {{count}} Livre...",
|
||||
"Search in {{count}} Book(s)..._many": "Rechercher dans {{count}} Livres...",
|
||||
"Search in {{count}} Book(s)..._other": "Rechercher dans {{count}} Livres..."
|
||||
"Search in {{count}} Book(s)..._other": "Rechercher dans {{count}} Livres...",
|
||||
"No notes match your search": "Aucune note trouvée",
|
||||
"Search notes and excerpts...": "Rechercher des notes..."
|
||||
}
|
||||
|
||||
@@ -336,5 +336,7 @@
|
||||
"Enable only if you trust the file.": "केवल तभी सक्षम करें जब आप फ़ाइल पर भरोसा करते हों।",
|
||||
"Sort TOC by Page": "सामग्री को पृष्ठ द्वारा क्रमबद्ध करें",
|
||||
"Search in {{count}} Book(s)..._one": "{{count}} पुस्तक में खोजें...",
|
||||
"Search in {{count}} Book(s)..._other": "{{count}} पुस्तकों में खोजें..."
|
||||
"Search in {{count}} Book(s)..._other": "{{count}} पुस्तकों में खोजें...",
|
||||
"No notes match your search": "कोई नोट्स नहीं मिले",
|
||||
"Search notes and excerpts...": "नोट्स खोजें..."
|
||||
}
|
||||
|
||||
@@ -334,5 +334,7 @@
|
||||
"Allow JavaScript": "Izinkan JavaScript",
|
||||
"Enable only if you trust the file.": "Aktifkan hanya jika Anda mempercayai file tersebut.",
|
||||
"Sort TOC by Page": "Urutkan TOC berdasarkan Halaman",
|
||||
"Search in {{count}} Book(s)..._other": "Cari di {{count}} Buku..."
|
||||
"Search in {{count}} Book(s)..._other": "Cari di {{count}} Buku...",
|
||||
"No notes match your search": "Tidak ada catatan ditemukan",
|
||||
"Search notes and excerpts...": "Cari catatan..."
|
||||
}
|
||||
|
||||
@@ -338,5 +338,7 @@
|
||||
"Sort TOC by Page": "Ordina sommario per pagina",
|
||||
"Search in {{count}} Book(s)..._one": "Cerca in {{count}} libro...",
|
||||
"Search in {{count}} Book(s)..._many": "Cerca in {{count}} libri...",
|
||||
"Search in {{count}} Book(s)..._other": "Cerca in {{count}} libri..."
|
||||
"Search in {{count}} Book(s)..._other": "Cerca in {{count}} libri...",
|
||||
"No notes match your search": "Nessuna nota trovata",
|
||||
"Search notes and excerpts...": "Cerca nelle note..."
|
||||
}
|
||||
|
||||
@@ -334,5 +334,7 @@
|
||||
"Allow JavaScript": "JavaScriptを許可",
|
||||
"Enable only if you trust the file.": "ファイルを信頼する場合にのみ有効にしてください。",
|
||||
"Sort TOC by Page": "目次をページで並べ替え",
|
||||
"Search in {{count}} Book(s)..._other": "{{count}}冊の本を検索..."
|
||||
"Search in {{count}} Book(s)..._other": "{{count}}冊の本を検索...",
|
||||
"No notes match your search": "ノートが見つかりません",
|
||||
"Search notes and excerpts...": "ノートを検索..."
|
||||
}
|
||||
|
||||
@@ -334,5 +334,7 @@
|
||||
"Allow JavaScript": "JavaScript 허용",
|
||||
"Enable only if you trust the file.": "파일을 신뢰하는 경우에만 활성화하세요.",
|
||||
"Sort TOC by Page": "목차를 페이지별로 정렬",
|
||||
"Search in {{count}} Book(s)..._other": "{{count}}권의 책에서 검색..."
|
||||
"Search in {{count}} Book(s)..._other": "{{count}}권의 책에서 검색...",
|
||||
"No notes match your search": "일치하는 노트가 없습니다",
|
||||
"Search notes and excerpts...": "노트 검색..."
|
||||
}
|
||||
|
||||
@@ -336,5 +336,7 @@
|
||||
"Enable only if you trust the file.": "Schakel alleen in als u het bestand vertrouwt.",
|
||||
"Sort TOC by Page": "__STRING_NOT_TRANSLATED__",
|
||||
"Search in {{count}} Book(s)..._one": "Zoeken in {{count}} Boek...",
|
||||
"Search in {{count}} Book(s)..._other": "Zoeken in {{count}} Boeken..."
|
||||
"Search in {{count}} Book(s)..._other": "Zoeken in {{count}} Boeken...",
|
||||
"No notes match your search": "Geen notities gevonden",
|
||||
"Search notes and excerpts...": "Zoek notities..."
|
||||
}
|
||||
|
||||
@@ -340,5 +340,7 @@
|
||||
"Search in {{count}} Book(s)..._one": "Szukaj w {{count}} książce...",
|
||||
"Search in {{count}} Book(s)..._few": "Szukaj w {{count}} książkach...",
|
||||
"Search in {{count}} Book(s)..._many": "Szukaj w {{count}} książkach...",
|
||||
"Search in {{count}} Book(s)..._other": "Szukaj w {{count}} książkach..."
|
||||
"Search in {{count}} Book(s)..._other": "Szukaj w {{count}} książkach...",
|
||||
"No notes match your search": "Nie znaleziono notatek",
|
||||
"Search notes and excerpts...": "Szukaj notatek..."
|
||||
}
|
||||
|
||||
@@ -338,5 +338,7 @@
|
||||
"Sort TOC by Page": "Ordenar TOC por Página",
|
||||
"Search in {{count}} Book(s)..._one": "Pesquisar em {{count}} Livro...",
|
||||
"Search in {{count}} Book(s)..._many": "Pesquisar em {{count}} Livros...",
|
||||
"Search in {{count}} Book(s)..._other": "Pesquisar em {{count}} Livros..."
|
||||
"Search in {{count}} Book(s)..._other": "Pesquisar em {{count}} Livros...",
|
||||
"No notes match your search": "Nenhuma nota encontrada",
|
||||
"Search notes and excerpts...": "Buscar notas..."
|
||||
}
|
||||
|
||||
@@ -340,5 +340,7 @@
|
||||
"Search in {{count}} Book(s)..._one": "Поиск в {{count}} книге...",
|
||||
"Search in {{count}} Book(s)..._few": "Поиск в {{count}} книгах...",
|
||||
"Search in {{count}} Book(s)..._many": "Поиск в {{count}} книгах...",
|
||||
"Search in {{count}} Book(s)..._other": "Поиск в {{count}} книгах..."
|
||||
"Search in {{count}} Book(s)..._other": "Поиск в {{count}} книгах...",
|
||||
"No notes match your search": "Записи не найдены",
|
||||
"Search notes and excerpts...": "Искать записи..."
|
||||
}
|
||||
|
||||
@@ -336,5 +336,7 @@
|
||||
"Enable only if you trust the file.": "Yalnızca dosyaya güveniyorsanız etkinleştirin.",
|
||||
"Sort TOC by Page": "İçindekileri Sayfaya Göre Sırala",
|
||||
"Search in {{count}} Book(s)..._one": "{{count}} kitapta ara...",
|
||||
"Search in {{count}} Book(s)..._other": "{{count}} kitapta ara..."
|
||||
"Search in {{count}} Book(s)..._other": "{{count}} kitapta ara...",
|
||||
"No notes match your search": "Eşleşen not bulunamadı",
|
||||
"Search notes and excerpts...": "Notlarda ara..."
|
||||
}
|
||||
|
||||
@@ -340,5 +340,7 @@
|
||||
"Search in {{count}} Book(s)..._one": "Шукати в {{count}} книзі...",
|
||||
"Search in {{count}} Book(s)..._few": "Шукати в {{count}} книгах...",
|
||||
"Search in {{count}} Book(s)..._many": "Шукати в {{count}} книгах...",
|
||||
"Search in {{count}} Book(s)..._other": "Шукати в {{count}} книгах..."
|
||||
"Search in {{count}} Book(s)..._other": "Шукати в {{count}} книгах...",
|
||||
"No notes match your search": "Нотаток не знайдено",
|
||||
"Search notes and excerpts...": "Шукати нотатки..."
|
||||
}
|
||||
|
||||
@@ -334,5 +334,7 @@
|
||||
"Allow JavaScript": "Cho phép JavaScript",
|
||||
"Enable only if you trust the file.": "Chỉ bật nếu bạn tin tưởng tệp.",
|
||||
"Sort TOC by Page": "Sort Mục lục theo Trang",
|
||||
"Search in {{count}} Book(s)..._other": "Tìm kiếm trong {{count}} cuốn sách..."
|
||||
"Search in {{count}} Book(s)..._other": "Tìm kiếm trong {{count}} cuốn sách...",
|
||||
"No notes match your search": "Không tìm thấy ghi chú nào",
|
||||
"Search notes and excerpts...": "Tìm ghi chú..."
|
||||
}
|
||||
|
||||
@@ -334,5 +334,7 @@
|
||||
"Allow JavaScript": "允许 JavaScript",
|
||||
"Enable only if you trust the file.": "建议仅在您信任该文件时启用",
|
||||
"Sort TOC by Page": "按页码排序目录",
|
||||
"Search in {{count}} Book(s)..._other": "在 {{count}} 本书籍中搜索..."
|
||||
"Search in {{count}} Book(s)..._other": "在 {{count}} 本书籍中搜索...",
|
||||
"No notes match your search": "未找到匹配的笔记",
|
||||
"Search notes and excerpts...": "搜索笔记..."
|
||||
}
|
||||
|
||||
@@ -334,5 +334,7 @@
|
||||
"Allow JavaScript": "允許 JavaScript",
|
||||
"Enable only if you trust the file.": "建議僅在您信任該檔案時啟用",
|
||||
"Sort TOC by Page": "依頁碼排序目錄",
|
||||
"Search in {{count}} Book(s)..._other": "在 {{count}} 本書中搜索..."
|
||||
"Search in {{count}} Book(s)..._other": "在 {{count}} 本書中搜索...",
|
||||
"No notes match your search": "找不到符合的筆記",
|
||||
"Search notes and excerpts...": "搜尋筆記..."
|
||||
}
|
||||
|
||||
@@ -9,9 +9,11 @@ import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
|
||||
const NotebookHeader: React.FC<{
|
||||
isPinned: boolean;
|
||||
isSearchBarVisible: boolean;
|
||||
handleClose: () => void;
|
||||
handleTogglePin: () => void;
|
||||
}> = ({ isPinned, handleClose, handleTogglePin }) => {
|
||||
handleToggleSearchBar: () => void;
|
||||
}> = ({ isPinned, isSearchBarVisible, handleClose, handleTogglePin, handleToggleSearchBar }) => {
|
||||
const _ = useTranslation();
|
||||
const iconSize14 = useResponsiveSize(14);
|
||||
return (
|
||||
@@ -38,7 +40,10 @@ const NotebookHeader: React.FC<{
|
||||
</button>
|
||||
</div>
|
||||
<div className='flex items-center justify-end gap-x-4'>
|
||||
<button className='btn btn-ghost left-0 h-8 min-h-8 w-8 p-0'>
|
||||
<button
|
||||
onClick={handleToggleSearchBar}
|
||||
className={clsx('btn btn-ghost h-8 min-h-8 w-8 p-0', isSearchBarVisible && 'bg-base-300')}
|
||||
>
|
||||
<FiSearch />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
@@ -18,6 +18,7 @@ import { getBookDirFromLanguage } from '@/utils/book';
|
||||
import BooknoteItem from '../sidebar/BooknoteItem';
|
||||
import NotebookHeader from './Header';
|
||||
import NoteEditor from './NoteEditor';
|
||||
import SearchBar from './SearchBar';
|
||||
|
||||
const MIN_NOTEBOOK_WIDTH = 0.15;
|
||||
const MAX_NOTEBOOK_WIDTH = 0.45;
|
||||
@@ -35,6 +36,10 @@ const Notebook: React.FC = ({}) => {
|
||||
const { setNotebookWidth, setNotebookVisible, toggleNotebookPin } = useNotebookStore();
|
||||
const { setNotebookNewAnnotation, setNotebookEditAnnotation } = useNotebookStore();
|
||||
|
||||
const [isSearchBarVisible, setIsSearchBarVisible] = useState(false);
|
||||
const [searchResults, setSearchResults] = useState<BookNote[] | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const onNavigateEvent = async () => {
|
||||
const pinButton = document.querySelector('.sidebar-pin-btn');
|
||||
const isPinButtonHidden = !pinButton || window.getComputedStyle(pinButton).display === 'none';
|
||||
@@ -135,6 +140,39 @@ const Notebook: React.FC = ({}) => {
|
||||
|
||||
const { handleDragStart } = useDrag(onDragMove);
|
||||
|
||||
const config = getConfig(sideBarBookKey);
|
||||
const { booknotes: allNotes = [] } = config || {};
|
||||
const annotationNotes = allNotes
|
||||
.filter((note) => note.type === 'annotation' && note.note && !note.deletedAt)
|
||||
.sort((a, b) => b.createdAt - a.createdAt);
|
||||
const excerptNotes = allNotes
|
||||
.filter((note) => note.type === 'excerpt' && note.text && !note.deletedAt)
|
||||
.sort((a, b) => a.createdAt - b.createdAt);
|
||||
|
||||
const handleToggleSearchBar = () => {
|
||||
setIsSearchBarVisible((prev) => !prev);
|
||||
if (isSearchBarVisible) {
|
||||
setSearchResults(null);
|
||||
setSearchTerm('');
|
||||
}
|
||||
};
|
||||
|
||||
const filteredAnnotationNotes = useMemo(
|
||||
() =>
|
||||
isSearchBarVisible && searchResults
|
||||
? searchResults.filter((note) => note.type === 'annotation' && note.note && !note.deletedAt)
|
||||
: annotationNotes,
|
||||
[annotationNotes, searchResults, isSearchBarVisible],
|
||||
);
|
||||
|
||||
const filteredExcerptNotes = useMemo(
|
||||
() =>
|
||||
isSearchBarVisible && searchResults
|
||||
? searchResults.filter((note) => note.type === 'excerpt' && note.text && !note.deletedAt)
|
||||
: excerptNotes,
|
||||
[excerptNotes, searchResults, isSearchBarVisible],
|
||||
);
|
||||
|
||||
if (!sideBarBookKey) return null;
|
||||
|
||||
const bookData = getBookData(sideBarBookKey);
|
||||
@@ -145,14 +183,8 @@ const Notebook: React.FC = ({}) => {
|
||||
const { bookDoc } = bookData;
|
||||
const languageDir = getBookDirFromLanguage(bookDoc.metadata.language);
|
||||
|
||||
const config = getConfig(sideBarBookKey);
|
||||
const { booknotes: allNotes = [] } = config || {};
|
||||
const annotationNotes = allNotes
|
||||
.filter((note) => note.type === 'annotation' && note.note && !note.deletedAt)
|
||||
.sort((a, b) => b.createdAt - a.createdAt);
|
||||
const excerptNotes = allNotes
|
||||
.filter((note) => note.type === 'excerpt' && note.text && !note.deletedAt)
|
||||
.sort((a, b) => a.createdAt - b.createdAt);
|
||||
const hasSearchResults = filteredAnnotationNotes.length > 0 || filteredExcerptNotes.length > 0;
|
||||
const hasAnyNotes = annotationNotes.length > 0 || excerptNotes.length > 0;
|
||||
|
||||
return isNotebookVisible ? (
|
||||
<>
|
||||
@@ -161,7 +193,7 @@ const Notebook: React.FC = ({}) => {
|
||||
)}
|
||||
<div
|
||||
className={clsx(
|
||||
'notebook-container bg-base-200 right-0 z-20 min-w-60 select-none',
|
||||
'notebook-container bg-base-200 right-0 z-20 flex min-w-60 select-none flex-col',
|
||||
'font-sans text-base font-normal sm:text-sm',
|
||||
appService?.isIOSApp ? 'h-[100vh]' : 'h-full',
|
||||
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)]',
|
||||
@@ -187,19 +219,47 @@ const Notebook: React.FC = ({}) => {
|
||||
className='drag-bar absolute left-0 top-0 h-full w-0.5 cursor-col-resize'
|
||||
onMouseDown={handleDragStart}
|
||||
/>
|
||||
<NotebookHeader
|
||||
isPinned={isNotebookPinned}
|
||||
handleClose={() => setNotebookVisible(false)}
|
||||
handleTogglePin={handleTogglePin}
|
||||
/>
|
||||
<div className='max-h-[calc(100vh-44px)] overflow-y-auto px-3'>
|
||||
<div className='flex-shrink-0'>
|
||||
<NotebookHeader
|
||||
isPinned={isNotebookPinned}
|
||||
isSearchBarVisible={isSearchBarVisible}
|
||||
handleClose={() => setNotebookVisible(false)}
|
||||
handleTogglePin={handleTogglePin}
|
||||
handleToggleSearchBar={handleToggleSearchBar}
|
||||
/>
|
||||
<div
|
||||
className={clsx('search-bar', {
|
||||
'search-bar-visible': isSearchBarVisible,
|
||||
})}
|
||||
>
|
||||
<SearchBar
|
||||
isVisible={isSearchBarVisible}
|
||||
bookKey={sideBarBookKey}
|
||||
searchTerm={searchTerm}
|
||||
onSearchResultChange={setSearchResults}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex-grow overflow-y-auto px-3'>
|
||||
{isSearchBarVisible && searchResults && !hasSearchResults && hasAnyNotes && (
|
||||
<div className='flex h-32 items-center justify-center text-gray-500'>
|
||||
<p className='font-size-sm text-center'>{_('No notes match your search')}</p>
|
||||
</div>
|
||||
)}
|
||||
<div dir='ltr'>
|
||||
{excerptNotes.length > 0 && (
|
||||
<p className='content font-size-base pt-1'>{_('Excerpts')}</p>
|
||||
{filteredExcerptNotes.length > 0 && (
|
||||
<p className='content font-size-base'>
|
||||
{_('Excerpts')}
|
||||
{isSearchBarVisible && searchResults && (
|
||||
<span className='font-size-xs ml-2 text-gray-500'>
|
||||
({filteredExcerptNotes.length})
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<ul className=''>
|
||||
{excerptNotes.map((item, index) => (
|
||||
{filteredExcerptNotes.map((item, index) => (
|
||||
<li key={`${index}-${item.id}`} className='my-2'>
|
||||
<div
|
||||
tabIndex={0}
|
||||
@@ -232,15 +292,22 @@ const Notebook: React.FC = ({}) => {
|
||||
))}
|
||||
</ul>
|
||||
<div dir='ltr'>
|
||||
{(notebookNewAnnotation || annotationNotes.length > 0) && (
|
||||
<p className='content font-size-base pt-1'>{_('Notes')}</p>
|
||||
{(notebookNewAnnotation || filteredAnnotationNotes.length > 0) && (
|
||||
<p className='content font-size-base'>
|
||||
{_('Notes')}
|
||||
{isSearchBarVisible && searchResults && filteredAnnotationNotes.length > 0 && (
|
||||
<span className='font-size-xs ml-2 text-gray-500'>
|
||||
({filteredAnnotationNotes.length})
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{(notebookNewAnnotation || notebookEditAnnotation) && (
|
||||
{(notebookNewAnnotation || notebookEditAnnotation) && !isSearchBarVisible && (
|
||||
<NoteEditor onSave={handleSaveNote} onEdit={(item) => handleEditNote(item, false)} />
|
||||
)}
|
||||
<ul>
|
||||
{annotationNotes.map((item, index) => (
|
||||
{filteredAnnotationNotes.map((item, index) => (
|
||||
<BooknoteItem key={`${index}-${item.cfi}`} bookKey={sideBarBookKey} item={item} />
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { FaSearch, FaTimes } from 'react-icons/fa';
|
||||
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { BookNote } from '@/types/book';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
|
||||
interface SearchBarProps {
|
||||
isVisible: boolean;
|
||||
bookKey: string;
|
||||
searchTerm: string;
|
||||
onSearchResultChange: (results: BookNote[] | null) => void;
|
||||
}
|
||||
|
||||
const SearchBar: React.FC<SearchBarProps> = ({
|
||||
isVisible,
|
||||
bookKey,
|
||||
searchTerm: term,
|
||||
onSearchResultChange,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { getConfig } = useBookDataStore();
|
||||
const [searchTerm, setSearchTerm] = useState(term);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const searchTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const iconSize16 = useResponsiveSize(16);
|
||||
const iconSize12 = useResponsiveSize(12);
|
||||
|
||||
useEffect(() => {
|
||||
handleSearchTermChange(searchTerm);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [bookKey]);
|
||||
|
||||
useEffect(() => {
|
||||
setSearchTerm(term);
|
||||
handleSearchTermChange(term);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [term]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && inputRef.current) {
|
||||
inputRef.current.blur();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
if (searchTimeout.current) {
|
||||
clearTimeout(searchTimeout.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
setSearchTerm(value);
|
||||
|
||||
if (searchTimeout.current) {
|
||||
clearTimeout(searchTimeout.current);
|
||||
}
|
||||
|
||||
searchTimeout.current = setTimeout(() => {
|
||||
handleSearchTermChange(value);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const handleClearSearch = () => {
|
||||
setSearchTerm('');
|
||||
handleSearchTermChange('');
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const filterNotes = (notes: BookNote[], query: string): BookNote[] => {
|
||||
if (!query.trim()) return [];
|
||||
|
||||
const lowercaseQuery = query.toLowerCase();
|
||||
return notes.filter((note) => {
|
||||
const textMatch = note.text?.toLowerCase().includes(lowercaseQuery) || false;
|
||||
const noteMatch = note.note?.toLowerCase().includes(lowercaseQuery) || false;
|
||||
return textMatch || noteMatch;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSearchTermChange = (term: string) => {
|
||||
if (term.trim().length >= 1) {
|
||||
handleSearch(term);
|
||||
} else {
|
||||
resetSearch();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = (term: string) => {
|
||||
const config = getConfig(bookKey);
|
||||
if (!config) {
|
||||
resetSearch();
|
||||
return;
|
||||
}
|
||||
|
||||
const { booknotes: allNotes = [] } = config;
|
||||
const validNotes = allNotes.filter((note) => !note.deletedAt);
|
||||
|
||||
const results = filterNotes(validNotes, term);
|
||||
onSearchResultChange(results);
|
||||
};
|
||||
|
||||
const resetSearch = () => {
|
||||
onSearchResultChange(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='relative px-3 py-2'>
|
||||
<div className='bg-base-100 flex h-8 items-center rounded-lg'>
|
||||
<div className='pl-3'>
|
||||
<FaSearch size={iconSize16} className='text-gray-500' />
|
||||
</div>
|
||||
|
||||
<input
|
||||
ref={inputRef}
|
||||
type='text'
|
||||
value={searchTerm}
|
||||
spellCheck={false}
|
||||
onChange={handleInputChange}
|
||||
placeholder={_('Search notes and excerpts...')}
|
||||
className='w-full bg-transparent p-2 font-sans text-sm font-light focus:outline-none'
|
||||
/>
|
||||
|
||||
{searchTerm && (
|
||||
<div className='bg-base-300 flex h-8 w-8 items-center rounded-r-lg'>
|
||||
<button
|
||||
onClick={handleClearSearch}
|
||||
className='btn btn-ghost h-8 min-h-8 w-8 rounded-none rounded-r-lg p-0'
|
||||
>
|
||||
<FaTimes size={iconSize12} className='text-gray-500' />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchBar;
|
||||
Reference in New Issue
Block a user