11import  {  Typography ,  Card ,  CardContent ,  Stack ,  Chip  }  from  "@mui/material" ; 
22import  {  Colors  }  from  "design/theme" ; 
33import  React  from  "react" ; 
4+ import  {  useMemo  }  from  "react" ; 
45import  {  Link  }  from  "react-router-dom" ; 
56import  RoutesEnum  from  "types/routes.enum" ; 
67
@@ -17,14 +18,73 @@ interface DatasetCardProps {
1718      info ?: { 
1819        Authors ?: string [ ] ; 
1920        DatasetDOI ?: string ; 
21+         [ k : string ] : any ; 
2022      } ; 
23+       [ k : string ] : any ; 
2124    } ; 
2225  } ; 
2326  index : number ; 
2427  onChipClick : ( key : string ,  value : string )  =>  void ; 
2528  keyword ?: string ;  // for keyword highlight 
2629} 
2730
31+ /** ---------- utility helpers ---------- **/ 
32+ const  normalize  =  ( s : string )  => 
33+   s 
34+     ?. replace ( / [ \u2018 \u2019 \u2032 ] / g,  "'" )  // curly → straight 
35+     ?. replace ( / [ \u201C \u201D \u2033 ] / g,  '"' )  ??  // curly → straight 
36+   "" ; 
37+ 
38+ const  containsKeyword  =  ( text ?: string ,  kw ?: string )  =>  { 
39+   if  ( ! text  ||  ! kw )  return  false ; 
40+   const  t  =  normalize ( text ) . toLowerCase ( ) ; 
41+   const  k  =  normalize ( kw ) . toLowerCase ( ) ; 
42+   return  t . includes ( k ) ; 
43+ } ; 
44+ 
45+ /** Find a short snippet in secondary fields if not already visible */ 
46+ function  findMatchSnippet ( 
47+   v : any , 
48+   kw ?: string 
49+ ) : {  label : string ;  html : string  }  |  null  { 
50+   if  ( ! kw )  return  null ; 
51+ 
52+   // Which fields to scan (can add/remove fields here) 
53+   const  CANDIDATE_FIELDS : Array < [ string ,  ( v : any )  =>  string  |  undefined ] >  =  [ 
54+     [ "Acknowledgements" ,  ( v )  =>  v ?. info ?. Acknowledgements ] , 
55+     [ 
56+       "Funding" , 
57+       ( v )  => 
58+         Array . isArray ( v ?. info ?. Funding ) 
59+           ? v . info . Funding . join ( " " ) 
60+           : v ?. info ?. Funding , 
61+     ] , 
62+     [ "ReferencesAndLinks" ,  ( v )  =>  v ?. info ?. ReferencesAndLinks ] , 
63+   ] ; 
64+ 
65+   const  k  =  normalize ( kw ) . toLowerCase ( ) ; 
66+ 
67+   for  ( const  [ label ,  getter ]  of  CANDIDATE_FIELDS )  { 
68+     const  raw  =  getter ( v ) ;  // v = parsedJson.value 
69+     if  ( ! raw )  continue ; 
70+     const  text  =  normalize ( String ( raw ) ) ; 
71+     const  i  =  text . toLowerCase ( ) . indexOf ( k ) ;  // k is the lowercase version of keyword 
72+     if  ( i  >=  0 )  { 
73+       const  start  =  Math . max ( 0 ,  i  -  40 ) ; 
74+       const  end  =  Math . min ( text . length ,  i  +  k . length  +  40 ) ; 
75+       const  before  =  text . slice ( start ,  i ) ; 
76+       const  hit  =  text . slice ( i ,  i  +  k . length ) ; 
77+       const  after  =  text . slice ( i  +  k . length ,  end ) ; 
78+       const  html  =  `${  
79+         start  >  0  ? "…"  : ""  
80+       }  ${ before } ${ hit } ${ after } ${ end  <  text . length  ? "…"  : "" } ; 
81+       return  {  label,  html } ; 
82+     } 
83+   } 
84+   return  null ; 
85+ } 
86+ /** ---------- end of helpers ---------- **/ 
87+ 
2888const  DatasetCard : React . FC < DatasetCardProps >  =  ( { 
2989  dbname, 
3090  dsname, 
@@ -40,7 +100,29 @@ const DatasetCard: React.FC<DatasetCardProps> = ({
40100  const  rawDOI  =  info ?. DatasetDOI ?. replace ( / ^ d o i : / ,  "" ) ; 
41101  const  doiLink  =  rawDOI  ? `https://doi.org/${ rawDOI }   : null ; 
42102
43-   // keyword hightlight functional component 
103+   // precompute what’s visible & whether it already contains the keyword 
104+   const  authorsJoined  =  Array . isArray ( info ?. Authors ) 
105+     ? info ! . Authors . join ( ", " ) 
106+     : typeof  info ?. Authors  ===  "string" 
107+     ? info ! . Authors 
108+     : "" ; 
109+ 
110+   const  visibleHasKeyword  =  useMemo ( 
111+     ( )  => 
112+       containsKeyword ( name ,  keyword )  || 
113+       containsKeyword ( readme ,  keyword )  || 
114+       containsKeyword ( authorsJoined ,  keyword ) , 
115+     [ name ,  readme ,  authorsJoined ,  keyword ] 
116+   ) ; 
117+ 
118+   // If not visible, produce a one-line snippet from other fields (for non-visible fields) 
119+   const  snippet  =  useMemo ( 
120+     ( )  => 
121+       ! visibleHasKeyword  ? findMatchSnippet ( parsedJson . value ,  keyword )  : null , 
122+     [ parsedJson . value ,  keyword ,  visibleHasKeyword ] 
123+   ) ; 
124+ 
125+   // keyword hightlight functional component (only for visible fields) 
44126  const  highlightKeyword  =  ( text : string ,  keyword ?: string )  =>  { 
45127    if  ( ! keyword  ||  ! text ?. toLowerCase ( ) . includes ( keyword . toLowerCase ( ) ) )  { 
46128      return  text ; 
@@ -99,7 +181,10 @@ const DatasetCard: React.FC<DatasetCardProps> = ({
99181          { highlightKeyword ( name  ||  "Untitled Dataset" ,  keyword ) } 
100182        </ Typography > 
101183        < Typography > 
102-           Database: { dbname }    |   Dataset: { dsname } 
184+           { /* Database: {dbname}   |   Dataset: {dsname} */ } 
185+           < strong > Database:</ strong >  { highlightKeyword ( dbname ,  keyword ) } 
186+           { "  " }   |  { "  " } 
187+           < strong > Dataset:</ strong >  { highlightKeyword ( dsname ,  keyword ) } 
103188        </ Typography > 
104189
105190        < Stack  spacing = { 2 }  margin = { 1 } > 
@@ -168,20 +253,35 @@ const DatasetCard: React.FC<DatasetCardProps> = ({
168253                { info ?. Authors  &&  ( 
169254                  < Typography  variant = "body2"  mt = { 1 } > 
170255                    < strong > Authors:</ strong > { " " } 
171-                     { highlightKeyword ( 
172-                       Array . isArray ( info . Authors ) 
173-                         ? info . Authors . join ( ", " ) 
174-                         : typeof  info . Authors  ===  "string" 
175-                         ? info . Authors 
176-                         : "N/A" , 
177-                       keyword 
178-                     ) } 
256+                     { highlightKeyword ( authorsJoined  ||  "N/A" ,  keyword ) } 
179257                  </ Typography > 
180258                ) } 
181259              </ Typography > 
182260            ) } 
183261          </ Stack > 
184262
263+           { /* show why it matched if not visible in main fields */ } 
264+           { snippet  &&  ( 
265+             < Stack  direction = "row"  spacing = { 1 }  flexWrap = "wrap"  gap = { 1 } > 
266+               < Chip 
267+                 label = { `Matched in ${ snippet . label }  } 
268+                 size = "small" 
269+                 sx = { { 
270+                   height : 22 , 
271+                   backgroundColor : "#f9f9ff" , 
272+                   color : Colors . darkPurple , 
273+                   border : `1px solid ${ Colors . lightGray }  , 
274+                 } } 
275+               /> 
276+               < Typography 
277+                 variant = "body2" 
278+                 sx = { {  mt : 0.5  } } 
279+                 // safe: snippet is derived from our own strings with <mark> only 
280+                 dangerouslySetInnerHTML = { {  __html : snippet . html  } } 
281+               /> 
282+             </ Stack > 
283+           ) } 
284+ 
185285          < Stack  direction = "row"  spacing = { 1 }  flexWrap = "wrap"  gap = { 1 } > 
186286            { doiLink  &&  ( 
187287              < Stack  mt = { 1 } > 
0 commit comments