@@ -13,6 +13,87 @@ import { useThemeCtx } from '../..'
13
13
import s from './search.module.less'
14
14
import type { PageMeta } from '../../analyzeStaticData'
15
15
16
+ const recentSearchesKey = '__VITE_PAGES_RECENT_SEARCHES'
17
+
18
+ const getPagePosition = ( page : PageMetaExtended ) => {
19
+ return [ page . groupKey , page . subGroupKey , page . pageTitle ]
20
+ . filter ( ( s ) => s !== '/' )
21
+ . join ( ' > ' )
22
+ }
23
+
24
+ const hasInRecentSearches = (
25
+ page : PageMetaExtended ,
26
+ recentSearches : SearchResultItem [ ]
27
+ ) => {
28
+ return recentSearches
29
+ . map ( ( item ) => item . page . pagePath )
30
+ . includes ( page . pagePath )
31
+ }
32
+
33
+ const renderSearchResultItem = (
34
+ type : 'title' | 'heading' ,
35
+ matchedString : string ,
36
+ pagePosition : string
37
+ ) => {
38
+ if ( type === 'title' ) {
39
+ return (
40
+ < div className = { s . searchResultLable } >
41
+ < div className = { s . searchResultLableIcon } >
42
+ < NumberOutlined style = { { fontSize : '36px' , color : '#1f1f1f' } } />
43
+ </ div >
44
+ < div >
45
+ < div className = { s . searchResultMatchedText } >
46
+ Title: { matchedString }
47
+ </ div >
48
+ < div className = { s . searchResultPagePosition } > { pagePosition } </ div >
49
+ </ div >
50
+ </ div >
51
+ )
52
+ }
53
+ if ( type === 'heading' ) {
54
+ return (
55
+ < div className = { s . searchResultLable } >
56
+ < div className = { s . searchResultLableIcon } >
57
+ < ProfileOutlined style = { { fontSize : '36px' , color : '#1f1f1f' } } />
58
+ </ div >
59
+ < div >
60
+ < div className = { s . searchResultMatchedText } >
61
+ Heading: { matchedString }
62
+ </ div >
63
+ < div className = { s . searchResultPagePosition } > { pagePosition } </ div >
64
+ </ div >
65
+ </ div >
66
+ )
67
+ }
68
+
69
+ throw new Error ( 'unexpected SearchResultItem: type ' + type )
70
+ }
71
+
72
+ const calcRecentSearchesOptions = ( recentSearches : SearchResultItem [ ] ) => {
73
+ const label = < p > Recent</ p >
74
+
75
+ const options = recentSearches . map ( ( item ) => {
76
+ const { type, page, matechedString } = item
77
+
78
+ const value = [
79
+ type ,
80
+ page . pagePath ,
81
+ type === 'heading' ? item . headingId : '' ,
82
+ matechedString ,
83
+ ] . join ( ' - ' )
84
+
85
+ const rendered = ( ( ) => {
86
+ const pagePosition = getPagePosition ( page )
87
+
88
+ return renderSearchResultItem ( type , matechedString , pagePosition )
89
+ } ) ( )
90
+
91
+ return { value, label : rendered , result : item }
92
+ } )
93
+
94
+ return [ { label, options } ]
95
+ }
96
+
16
97
interface Props { }
17
98
18
99
// TODO: use https://github.com/nextapps-de/flexsearch to do full text search in browser
@@ -24,10 +105,16 @@ const Search: React.FC<React.PropsWithChildren<Props>> = (props) => {
24
105
const { staticData, resolvedLocale, pageGroups } = useThemeCtx ( )
25
106
const [ popupOpen , setPopupOpen ] = useState ( false )
26
107
const [ keywords , setKeywords ] = useState ( '' )
108
+ const [ recentSearches , setRecentSearches ] = useState < SearchResultItem [ ] > ( [ ] )
27
109
const navigate = useNavigate ( )
28
110
29
111
const allPagesOutlines = useAllPagesOutlines ( 2000 ) ?. allPagesOutlines
30
112
113
+ const recentSearchesOptions = useMemo (
114
+ ( ) => calcRecentSearchesOptions ( recentSearches ) ,
115
+ [ recentSearches ]
116
+ )
117
+
31
118
const preparedPages = useMemo ( ( ) => {
32
119
const res = [ ] as PageMetaExtended [ ]
33
120
Object . entries ( pageGroups ) . forEach ( ( [ groupKey , group ] ) => {
@@ -61,44 +148,9 @@ const Search: React.FC<React.PropsWithChildren<Props>> = (props) => {
61
148
return filteredData . map ( ( item ) => {
62
149
const { type, page, matechedString } = item
63
150
const rendered = ( ( ) => {
64
- const pagePosition = [ page . groupKey , page . subGroupKey , page . pageTitle ]
65
- . filter ( ( s ) => s !== '/' )
66
- . join ( ' > ' )
67
- if ( type === 'title' ) {
68
- return (
69
- < div className = { s . searchResultLable } >
70
- < div className = { s . searchResultLableIcon } >
71
- < NumberOutlined
72
- style = { { fontSize : '36px' , color : '#1f1f1f' } }
73
- />
74
- </ div >
75
- < div >
76
- < div className = { s . searchResultMatchedText } >
77
- Title: { matechedString }
78
- </ div >
79
- < div className = { s . searchResultPagePosition } > { pagePosition } </ div >
80
- </ div >
81
- </ div >
82
- )
83
- }
84
- if ( type === 'heading' ) {
85
- return (
86
- < div className = { s . searchResultLable } >
87
- < div className = { s . searchResultLableIcon } >
88
- < ProfileOutlined
89
- style = { { fontSize : '36px' , color : '#1f1f1f' } }
90
- />
91
- </ div >
92
- < div >
93
- < div className = { s . searchResultMatchedText } >
94
- Heading: { matechedString }
95
- </ div >
96
- < div className = { s . searchResultPagePosition } > { pagePosition } </ div >
97
- </ div >
98
- </ div >
99
- )
100
- }
101
- throw new Error ( 'unexpected SearchResultItem: type ' + type )
151
+ const pagePosition = getPagePosition ( page )
152
+
153
+ return renderSearchResultItem ( type , matechedString , pagePosition )
102
154
} ) ( )
103
155
return {
104
156
value : [
@@ -113,6 +165,14 @@ const Search: React.FC<React.PropsWithChildren<Props>> = (props) => {
113
165
} )
114
166
} , [ preparedPages , keywords ] )
115
167
168
+ useEffect ( ( ) => {
169
+ const value = localStorage . getItem ( recentSearchesKey )
170
+
171
+ if ( value ) {
172
+ setRecentSearches ( JSON . parse ( value ) )
173
+ }
174
+ } , [ ] )
175
+
116
176
return (
117
177
< div className = { s [ 'search-box' ] } >
118
178
< AutoComplete
@@ -121,13 +181,23 @@ const Search: React.FC<React.PropsWithChildren<Props>> = (props) => {
121
181
getPopupContainer = { ( trigger ) => trigger . parentElement }
122
182
dropdownMatchSelectWidth = { false }
123
183
style = { { width : 200 } }
124
- options = { options }
184
+ options = { keywords ? options : ( recentSearchesOptions as any ) }
125
185
open = { popupOpen }
126
186
onDropdownVisibleChange = { setPopupOpen }
127
187
value = { keywords }
128
188
onSearch = { setKeywords }
129
189
onSelect = { ( value : any , option : any ) => {
130
190
const result : SearchResultItem = option . result
191
+
192
+ if ( ! hasInRecentSearches ( result . page , recentSearches ) ) {
193
+ setRecentSearches ( ( prev ) => [ ...prev , result ] )
194
+
195
+ localStorage . setItem (
196
+ recentSearchesKey ,
197
+ JSON . stringify ( [ ...recentSearches , result ] )
198
+ )
199
+ }
200
+
131
201
if ( result . type === 'title' ) {
132
202
navigate ( result . page . pagePath )
133
203
} else if ( result . type === 'heading' ) {
0 commit comments