@@ -4,16 +4,14 @@ import { Search } from 'lucide-react'
44
55import styles from './SearchPage.module.css'
66import { useSearchIndex } from '../hooks/useSearchIndex.ts'
7+ import { useTaxonomyFilter , CURATED_CLADES } from '../hooks/useTaxonomyFilter.ts'
78
89import type { IndexEntry } from '../hooks/useSearchIndex.ts'
910
1011const PAGE_SIZE = 100
1112
12- function getQueryFromURL ( ) {
13- if ( typeof window === 'undefined' ) {
14- return ''
15- }
16- return new URLSearchParams ( window . location . search ) . get ( 'q' ) ?? ''
13+ function getURLParam ( key : string ) {
14+ return new URLSearchParams ( window . location . search ) . get ( key ) ?? ''
1715}
1816
1917function scoreTerm ( term : string , field : string ) {
@@ -82,11 +80,9 @@ function highlightMatch(text: string, query: string) {
8280 . split ( / \s + / )
8381 . filter ( Boolean )
8482 . map ( t => t . replaceAll ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) )
85- const regex = new RegExp ( `(${ terms . join ( '|' ) } )` , 'gi' )
86- const parts = text . split ( regex )
87- return parts . map ( ( part , i ) =>
88- regex . test ( part ) ? < mark key = { i } > { part } </ mark > : part ,
89- )
83+ return text
84+ . split ( new RegExp ( `(${ terms . join ( '|' ) } )` , 'gi' ) )
85+ . map ( ( part , i ) => ( i % 2 === 1 ? < mark key = { i } > { part } </ mark > : part ) )
9086}
9187
9288function entryHref ( entry : IndexEntry ) {
@@ -95,7 +91,9 @@ function entryHref(entry: IndexEntry) {
9591
9692export default function SearchPage ( ) {
9793 const { index, loading } = useSearchIndex ( )
98- const [ query , setQuery ] = useState ( getQueryFromURL )
94+ const cladeSets = useTaxonomyFilter ( )
95+ const [ query , setQuery ] = useState ( ( ) => getURLParam ( 'q' ) )
96+ const [ clade , setClade ] = useState ( ( ) => getURLParam ( 'clade' ) )
9997 const [ page , setPage ] = useState ( 0 )
10098
10199 useEffect ( ( ) => {
@@ -105,28 +103,34 @@ export default function SearchPage() {
105103 } else {
106104 url . searchParams . delete ( 'q' )
107105 }
106+ if ( clade ) {
107+ url . searchParams . set ( 'clade' , clade )
108+ } else {
109+ url . searchParams . delete ( 'clade' )
110+ }
108111 window . history . replaceState ( { } , '' , url . toString ( ) )
109- } , [ query ] )
110-
111- useEffect ( ( ) => {
112112 setPage ( 0 )
113- } , [ query ] )
113+ } , [ query , clade ] )
114114
115115 const results = useMemo ( ( ) => {
116116 const terms = query . trim ( ) . toLowerCase ( ) . split ( / \s + / ) . filter ( Boolean )
117117 if ( terms . length === 0 ) {
118118 return [ ]
119119 }
120+ const cladeSet = clade && cladeSets ? cladeSets . get ( clade ) : undefined
120121 const scored : { entry : IndexEntry ; score : number } [ ] = [ ]
121122 for ( const entry of index ) {
123+ if ( cladeSet && ! cladeSet . has ( entry [ 6 ] ) ) {
124+ continue
125+ }
122126 const score = scoreEntry ( entry , terms )
123127 if ( score >= 0 ) {
124128 scored . push ( { entry, score } )
125129 }
126130 }
127131 scored . sort ( ( a , b ) => b . score - a . score )
128132 return scored . map ( s => s . entry )
129- } , [ index , query ] )
133+ } , [ index , query , clade , cladeSets ] )
130134
131135 const pageCount = Math . max ( 1 , Math . ceil ( results . length / PAGE_SIZE ) )
132136 const clampedPage = Math . min ( page , pageCount - 1 )
@@ -142,30 +146,47 @@ export default function SearchPage() {
142146 return (
143147 < div >
144148 < div className = { styles . searchWrapper } >
145- < Search size = { 16 } className = { styles . searchIcon } />
146- < input
147- type = "text"
148- value = { query }
149+ < div className = { styles . inputWrapper } >
150+ < Search size = { 16 } className = { styles . searchIcon } />
151+ < input
152+ type = "text"
153+ value = { query }
154+ onChange = { e => {
155+ setQuery ( e . target . value )
156+ } }
157+ placeholder = "Search by name, species, or accession..."
158+ autoComplete = "off"
159+ autoFocus
160+ className = { styles . input }
161+ />
162+ { query && (
163+ < button
164+ type = "button"
165+ onClick = { ( ) => {
166+ setQuery ( '' )
167+ } }
168+ className = { styles . clearButton }
169+ aria-label = "Clear search"
170+ >
171+ x
172+ </ button >
173+ ) }
174+ </ div >
175+ < select
176+ id = "clade-filter"
177+ value = { clade }
149178 onChange = { e => {
150- setQuery ( e . target . value )
179+ setClade ( e . target . value )
151180 } }
152- placeholder = "Search by name, species, or accession..."
153- autoComplete = "off"
154- autoFocus
155- className = { styles . input }
156- />
157- { query && (
158- < button
159- type = "button"
160- onClick = { ( ) => {
161- setQuery ( '' )
162- } }
163- className = { styles . clearButton }
164- aria-label = "Clear search"
165- >
166- x
167- </ button >
168- ) }
181+ className = { styles . categorySelect }
182+ >
183+ < option value = "" > All clades</ option >
184+ { CURATED_CLADES . map ( ( { label, display } ) => (
185+ < option key = { label } value = { label } >
186+ { display }
187+ </ option >
188+ ) ) }
189+ </ select >
169190 </ div >
170191 { query . trim ( ) && (
171192 < div className = { styles . resultCount } >
@@ -180,6 +201,7 @@ export default function SearchPage() {
180201 < th > Scientific name</ th >
181202 < th > Common name</ th >
182203 < th > Accession</ th >
204+ < th > Assembly name</ th >
183205 < th > Assembly status</ th >
184206 < th > Category</ th >
185207 </ tr >
@@ -194,6 +216,7 @@ export default function SearchPage() {
194216 </ td >
195217 < td > { highlightMatch ( entry [ 1 ] , query ) } </ td >
196218 < td > { highlightMatch ( entry [ 0 ] , query ) } </ td >
219+ < td > { highlightMatch ( entry [ 3 ] , query ) } </ td >
197220 < td > { entry [ 4 ] } </ td >
198221 < td > { entry [ 5 ] } </ td >
199222 </ tr >
0 commit comments