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 } <mark>${ hit } </mark>${ 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