Skip to content

Commit 2b6dc8e

Browse files
committed
feat: add matching database card to the search page; refs #81
1 parent 9796863 commit 2b6dc8e

File tree

3 files changed

+233
-5
lines changed

3 files changed

+233
-5
lines changed
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import {
2+
Box,
3+
Typography,
4+
Chip,
5+
Card,
6+
CardContent,
7+
Stack,
8+
Avatar,
9+
} from "@mui/material";
10+
import { Colors } from "design/theme";
11+
import React from "react";
12+
import { Link } from "react-router-dom";
13+
import RoutesEnum from "types/routes.enum";
14+
15+
type Props = {
16+
dbName?: string;
17+
fullName?: string;
18+
datasets?: number;
19+
modalities?: string[];
20+
logo?: string;
21+
keyword?: string;
22+
onChipClick: (key: string, value: string) => void;
23+
};
24+
25+
const DatabaseCard: React.FC<Props> = ({
26+
dbName,
27+
fullName,
28+
datasets,
29+
modalities,
30+
logo,
31+
keyword,
32+
onChipClick,
33+
}) => {
34+
const databaseLink = `${RoutesEnum.DATABASES}/${dbName}`;
35+
// keyword hightlight functional component
36+
const highlightKeyword = (text: string, keyword?: string) => {
37+
if (!keyword || !text?.toLowerCase().includes(keyword.toLowerCase())) {
38+
return text;
39+
}
40+
41+
const regex = new RegExp(`(${keyword})`, "gi"); // for case-insensitive and global
42+
const parts = text.split(regex);
43+
44+
return (
45+
<>
46+
{parts.map((part, i) =>
47+
part.toLowerCase() === keyword.toLowerCase() ? (
48+
<mark
49+
key={i}
50+
style={{ backgroundColor: "yellow", fontWeight: 600 }}
51+
>
52+
{part}
53+
</mark>
54+
) : (
55+
<React.Fragment key={i}>{part}</React.Fragment>
56+
)
57+
)}
58+
</>
59+
);
60+
};
61+
62+
return (
63+
<Card sx={{ mb: 3, position: "relative" }}>
64+
<CardContent>
65+
<Box
66+
sx={{
67+
display: "flex",
68+
flexDirection: "row",
69+
alignItems: "center",
70+
justifyContent: "flex-start",
71+
gap: 2,
72+
}}
73+
>
74+
{/* Logo as Avatar */}
75+
<Box sx={{ display: "flex", alignItems: "center" }}>
76+
{logo && (
77+
<Avatar
78+
variant="square"
79+
src={logo}
80+
alt={fullName || "Database Logo"}
81+
sx={{
82+
width: 60,
83+
height: 60,
84+
mb: 1,
85+
"& img": {
86+
objectFit: "contain", // show full image inside
87+
},
88+
}}
89+
/>
90+
)}
91+
</Box>
92+
{/* database card */}
93+
<Box>
94+
<Typography
95+
variant="h6"
96+
sx={{
97+
fontWeight: 600,
98+
color: Colors.darkPurple,
99+
textDecoration: "none",
100+
":hover": { textDecoration: "underline" },
101+
}}
102+
component={Link}
103+
to={databaseLink}
104+
target="_blank"
105+
>
106+
Database:{" "}
107+
{highlightKeyword(fullName || "Untitled Database", keyword)}
108+
</Typography>
109+
<Stack spacing={2} margin={1}>
110+
<Stack
111+
direction="row"
112+
spacing={1}
113+
flexWrap="wrap"
114+
gap={1}
115+
alignItems="center"
116+
>
117+
<Typography variant="body2" mt={1}>
118+
<strong>Modalities:</strong>
119+
</Typography>
120+
121+
{Array.isArray(modalities) && modalities.length > 0 ? (
122+
modalities.map((mod, idx) => (
123+
<Chip
124+
key={idx}
125+
label={mod}
126+
variant="outlined"
127+
onClick={() => onChipClick("modality", mod)}
128+
sx={{
129+
"& .MuiChip-label": {
130+
paddingX: "6px",
131+
fontSize: "0.8rem",
132+
},
133+
height: "24px",
134+
color: Colors.darkPurple,
135+
border: `1px solid ${Colors.darkPurple}`,
136+
fontWeight: "bold",
137+
transition: "all 0.2s ease",
138+
"&:hover": {
139+
backgroundColor: `${Colors.purple} !important`,
140+
color: "white",
141+
borderColor: Colors.purple,
142+
},
143+
}}
144+
/>
145+
))
146+
) : (
147+
<Typography variant="body2" mt={1}>
148+
N/A
149+
</Typography>
150+
)}
151+
</Stack>
152+
153+
<Stack direction="row" spacing={1} flexWrap="wrap" gap={1}>
154+
<Typography variant="body2" mt={1}>
155+
<strong>Datasets:</strong> {datasets ?? "N/A"}
156+
</Typography>
157+
</Stack>
158+
</Stack>
159+
</Box>
160+
</Box>
161+
</CardContent>
162+
</Card>
163+
);
164+
};
165+
166+
export default DatabaseCard;

src/pages/SearchPage.tsx

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { useTheme } from "@mui/material/styles";
1414
import useMediaQuery from "@mui/material/useMediaQuery";
1515
import Form from "@rjsf/mui";
1616
import validator from "@rjsf/validator-ajv8";
17+
import DatabaseCard from "components/SearchPage/DatabaseCard";
1718
import DatasetCard from "components/SearchPage/DatasetCard";
1819
import SubjectCard from "components/SearchPage/SubjectCard";
1920
import { Colors } from "design/theme";
@@ -30,6 +31,25 @@ import { RootState } from "redux/store";
3031
import { generateUiSchema } from "utils/SearchPageFunctions/generateUiSchema";
3132
import { modalityValueToEnumLabel } from "utils/SearchPageFunctions/modalityLabels";
3233

34+
type RegistryItem = {
35+
id: string;
36+
name?: string;
37+
fullname?: string;
38+
datatype?: string[];
39+
datasets?: number;
40+
logo?: string;
41+
};
42+
43+
const matchesKeyword = (item: RegistryItem, keyword: string) => {
44+
if (!keyword) return false;
45+
const needle = keyword.toLowerCase();
46+
return (
47+
item.name?.toLowerCase().includes(needle) ||
48+
item.fullname?.toLowerCase().includes(needle) ||
49+
item.datatype?.some((dt) => dt.toLowerCase().includes(needle))
50+
);
51+
};
52+
3353
const SearchPage: React.FC = () => {
3454
const dispatch = useAppDispatch();
3555
const [hasSearched, setHasSearched] = useState(false);
@@ -54,6 +74,17 @@ const SearchPage: React.FC = () => {
5474
const theme = useTheme();
5575
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
5676

77+
// for database card
78+
const keywordInput = String(formData?.keyword ?? "").trim();
79+
console.log("keyword", keywordInput);
80+
81+
const registryMatches: RegistryItem[] = React.useMemo(() => {
82+
if (!Array.isArray(registry) || !keywordInput) return [];
83+
return (registry as RegistryItem[]).filter((r) =>
84+
matchesKeyword(r, keywordInput)
85+
);
86+
}, [registry, keywordInput]);
87+
5788
// to show the applied chips on the top of results
5889
const activeFilters = Object.entries(appliedFilters).filter(
5990
([key, value]) =>
@@ -298,13 +329,14 @@ const SearchPage: React.FC = () => {
298329

299330
return (
300331
<Container
332+
maxWidth={false}
301333
style={{
302334
marginTop: "2rem",
303335
marginBottom: "2rem",
304336
backgroundColor: Colors.white,
305337
padding: "2rem",
306338
borderRadius: 4,
307-
width: "100%",
339+
width: "95%",
308340
}}
309341
>
310342
<Box // box for title and show filters button(mobile version)
@@ -465,6 +497,30 @@ const SearchPage: React.FC = () => {
465497
</Box>
466498
)}
467499

500+
{/* matching databases */}
501+
{keywordInput && registryMatches.length > 0 && (
502+
<Box sx={{ mb: 3 }}>
503+
<Typography
504+
variant="h6"
505+
sx={{ mb: 1.5, mt: 1.5, fontWeight: "600" }}
506+
>
507+
Matching Databases
508+
</Typography>
509+
{registryMatches.map((db) => (
510+
<DatabaseCard
511+
key={db.id}
512+
dbName={db.name}
513+
fullName={db.fullname}
514+
datasets={db.datasets}
515+
modalities={db.datatype}
516+
logo={db.logo}
517+
keyword={formData.keyword} // for keyword highlight
518+
onChipClick={handleChipClick}
519+
/>
520+
))}
521+
</Box>
522+
)}
523+
468524
{/* results */}
469525
{hasSearched && (
470526
<Box mt={4}>

src/pages/UpdatedDatasetDetailPage.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -272,10 +272,16 @@ const UpdatedDatasetDetailPage: React.FC = () => {
272272
index, // Assign index correctly
273273
})
274274
);
275-
const bytes = new TextEncoder().encode(
276-
JSON.stringify(datasetDocument)
277-
).length;
278-
setJsonSize(bytes);
275+
276+
const bytes = new Blob([JSON.stringify(datasetDocument)], {
277+
type: "application/json",
278+
});
279+
setJsonSize(bytes.size);
280+
281+
// const bytes = new TextEncoder().encode(
282+
// JSON.stringify(datasetDocument)
283+
// ).length;
284+
// setJsonSize(bytes);
279285
// Extract Internal Data & Assign `index`
280286
const internalData = extractInternalData(datasetDocument).map(
281287
(data, index) => ({

0 commit comments

Comments
 (0)