Skip to content

Commit 16d1621

Browse files
authored
Merge pull request #49 from NeuroJSON/dev-fan
Closes #40 Add Metadata-Based Search Filters to the Search Page for Dataset Querying
2 parents 2422e8e + 331fc36 commit 16d1621

File tree

14 files changed

+1293
-62
lines changed

14 files changed

+1293
-62
lines changed

package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
"@mui/icons-material": "^5.14.3",
1313
"@mui/material": "^5.14.4",
1414
"@reduxjs/toolkit": "^1.9.5",
15+
"@rjsf/core": "^5.24.8",
16+
"@rjsf/mui": "^5.24.8",
17+
"@rjsf/utils": "^5.24.8",
18+
"@rjsf/validator-ajv8": "^5.24.8",
1519
"@testing-library/jest-dom": "^5.14.1",
1620
"@testing-library/react": "^13.0.0",
1721
"@testing-library/user-event": "^13.2.1",
@@ -30,6 +34,7 @@
3034
"json-stringify-safe": "^5.0.1",
3135
"jwt-decode": "^3.1.2",
3236
"path-browserify": "^1.0.1",
37+
"pako": "^2.1.0",
3338
"query-string": "^8.1.0",
3439
"react": "^18.2.0",
3540
"react-dom": "^18.2.0",
@@ -47,6 +52,7 @@
4752
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
4853
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
4954
"@types/node": "^20.5.7",
55+
"@types/pako": "^2.0.3",
5056
"@typescript-eslint/eslint-plugin": "^5.31.0",
5157
"@typescript-eslint/parser": "^5.31.0",
5258
"eslint": "^8.21.0",
@@ -82,6 +88,7 @@
8288
"postcss": "^8.4.31",
8389
"nth-check": "^2.0.1",
8490
"@babel/runtime": "7.26.10",
85-
"@babel/helpers": "7.26.10"
91+
"@babel/helpers": "7.26.10",
92+
"3d-force-graph": "1.74.6"
8693
}
8794
}

src/components/Routes.tsx

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,36 @@ import DatabasePage from "pages/DatabasePage";
33
import DatasetDetailPage from "pages/DatasetDetailPage";
44
import DatasetPage from "pages/DatasetPage";
55
import Home from "pages/Home";
6+
import SearchPage from "pages/SearchPage";
67
import React from "react";
78
import { Navigate, Route, Routes as RouterRoutes } from "react-router-dom";
89
import RoutesEnum from "types/routes.enum";
910

1011
const Routes = () => (
11-
<RouterRoutes>
12-
{/* FullScreen Layout */}
13-
<Route element={<FullScreen />}>
14-
{/* Home Page */}
15-
<Route path={RoutesEnum.HOME} element={<Home />} />
16-
{/* Databases Page */}
17-
<Route path={RoutesEnum.DATABASES} element={<DatabasePage />} />
12+
<RouterRoutes>
13+
{/* FullScreen Layout */}
14+
<Route element={<FullScreen />}>
15+
{/* Home Page */}
16+
<Route path={RoutesEnum.HOME} element={<Home />} />
17+
{/* Databases Page */}
18+
<Route path={RoutesEnum.DATABASES} element={<DatabasePage />} />
1819

19-
{/* Dataset List Page */}
20-
<Route
21-
path={`${RoutesEnum.DATABASES}/:dbName`}
22-
element={<DatasetPage />}
23-
/>
20+
{/* Dataset List Page */}
21+
<Route
22+
path={`${RoutesEnum.DATABASES}/:dbName`}
23+
element={<DatasetPage />}
24+
/>
2425

25-
{/* Dataset Details Page */}
26-
<Route
27-
path={`${RoutesEnum.DATABASES}/:dbName/:docId`}
28-
element={<DatasetDetailPage />}
29-
/>
30-
</Route>
31-
</RouterRoutes>
26+
{/* Dataset Details Page */}
27+
<Route
28+
path={`${RoutesEnum.DATABASES}/:dbName/:docId`}
29+
element={<DatasetDetailPage />}
30+
/>
31+
32+
{/* Search Page */}
33+
<Route path={RoutesEnum.SEARCH} element={<SearchPage />} />
34+
</Route>
35+
</RouterRoutes>
3236
);
3337

3438
export default Routes;
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { Typography, Card, CardContent, Stack, Chip } from "@mui/material";
2+
import { Colors } from "design/theme";
3+
import React from "react";
4+
import { Link } from "react-router-dom";
5+
import RoutesEnum from "types/routes.enum";
6+
7+
interface DatasetCardProps {
8+
dbname: string;
9+
dsname: string;
10+
parsedJson: {
11+
key: string;
12+
value: {
13+
name?: string;
14+
readme?: string;
15+
modality?: string[];
16+
subj?: string[];
17+
info?: {
18+
Authors?: string[];
19+
DatasetDOI?: string;
20+
};
21+
};
22+
};
23+
index: number;
24+
}
25+
26+
const DatasetCard: React.FC<DatasetCardProps> = ({
27+
dbname,
28+
dsname,
29+
parsedJson,
30+
index,
31+
}) => {
32+
const { name, readme, modality, subj, info } = parsedJson.value;
33+
const datasetLink = `${RoutesEnum.DATABASES}/${dbname}/${dsname}`;
34+
35+
// prepare DOI URL
36+
const rawDOI = info?.DatasetDOI?.replace(/^doi:/, "");
37+
const doiLink = rawDOI ? `https://doi.org/${rawDOI}` : null;
38+
39+
return (
40+
<Card sx={{ mb: 3, position: "relative" }}>
41+
<CardContent>
42+
{/* Card Number in Top Right */}
43+
<Typography
44+
variant="subtitle2"
45+
sx={{
46+
position: "absolute",
47+
bottom: 8,
48+
right: 12,
49+
fontWeight: 600,
50+
color: Colors.darkPurple,
51+
}}
52+
>
53+
#{index + 1}
54+
</Typography>
55+
56+
<Typography
57+
variant="h6"
58+
sx={{
59+
fontWeight: 600,
60+
color: Colors.darkPurple,
61+
textDecoration: "none",
62+
":hover": { textDecoration: "underline" },
63+
}}
64+
component={Link}
65+
to={datasetLink}
66+
target="_blank"
67+
>
68+
{name || "Untitled Dataset"}
69+
</Typography>
70+
<Typography>
71+
Database: {dbname} &nbsp;&nbsp;|&nbsp;&nbsp; Dataset Number: {dsname}
72+
</Typography>
73+
74+
<Stack spacing={2} margin={1}>
75+
<Stack direction="row" spacing={1} flexWrap="wrap" gap={1}>
76+
<Typography variant="body2" mt={1}>
77+
<strong>Modalities:</strong>
78+
</Typography>
79+
80+
{Array.isArray(modality) && modality.length > 0 ? (
81+
modality.map((mod, idx) => (
82+
<Chip
83+
key={idx}
84+
label={mod}
85+
variant="outlined"
86+
sx={{
87+
color: Colors.darkPurple,
88+
border: `1px solid ${Colors.darkPurple}`,
89+
fontWeight: "bold",
90+
}}
91+
/>
92+
))
93+
) : (
94+
<Typography variant="body2" mt={1}>
95+
N/A
96+
</Typography>
97+
)}
98+
</Stack>
99+
100+
<Stack direction="row" spacing={1} flexWrap="wrap" gap={1}>
101+
<Typography variant="body2" mt={1}>
102+
<strong>Subjects:</strong> {subj && `${subj.length} subjects`}
103+
</Typography>
104+
</Stack>
105+
<Stack direction="row" spacing={1} flexWrap="wrap" gap={1}>
106+
{readme && (
107+
<Typography
108+
variant="body2"
109+
paragraph
110+
sx={{ textOverflow: "ellipsis" }}
111+
>
112+
<strong>Summary:</strong> {readme}
113+
</Typography>
114+
)}
115+
</Stack>
116+
<Stack direction="row" spacing={1} flexWrap="wrap" gap={1}>
117+
{info?.Authors?.length && (
118+
<Typography variant="body2" mt={1}>
119+
{info?.Authors && (
120+
<Typography variant="body2" mt={1}>
121+
<strong>Authors:</strong>{" "}
122+
{Array.isArray(info.Authors)
123+
? info.Authors.join(", ")
124+
: typeof info.Authors === "string"
125+
? info.Authors
126+
: "N/A"}
127+
</Typography>
128+
)}
129+
</Typography>
130+
)}
131+
</Stack>
132+
133+
<Stack direction="row" spacing={1} flexWrap="wrap" gap={1}>
134+
{doiLink && (
135+
<Stack mt={1}>
136+
<Chip
137+
label="DOI"
138+
component="a"
139+
href={doiLink}
140+
target="_blank"
141+
rel="noopener noreferrer"
142+
clickable
143+
sx={{ backgroundColor: Colors.accent, color: "white" }}
144+
/>
145+
</Stack>
146+
)}
147+
</Stack>
148+
</Stack>
149+
</CardContent>
150+
</Card>
151+
);
152+
};
153+
154+
export default DatasetCard;
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { Typography, Card, CardContent, Stack, Chip } from "@mui/material";
2+
import { Colors } from "design/theme";
3+
import React from "react";
4+
import { Link } from "react-router-dom";
5+
import RoutesEnum from "types/routes.enum";
6+
7+
interface SubjectCardProps {
8+
dbname: string;
9+
dsname: string;
10+
age: string;
11+
subj: string;
12+
parsedJson: {
13+
key: string[];
14+
value: {
15+
modalities?: string[];
16+
tasks?: string[];
17+
sessions?: string[];
18+
types?: string[];
19+
};
20+
};
21+
index: number;
22+
}
23+
24+
const SubjectCard: React.FC<SubjectCardProps> = ({
25+
dbname,
26+
dsname,
27+
age,
28+
subj,
29+
parsedJson,
30+
index,
31+
}) => {
32+
const { modalities, tasks, sessions, types } = parsedJson.value;
33+
const subjectLink = `${RoutesEnum.DATABASES}/${dbname}/${dsname}`;
34+
35+
// get the gender of subject
36+
const genderCode = parsedJson?.key?.[1];
37+
let genderDisplay = "Unknown";
38+
39+
if (genderCode) {
40+
if (genderCode === "000F") genderDisplay = "Female";
41+
else if (genderCode === "000M") genderDisplay = "Male";
42+
}
43+
44+
// cover age string to readable format
45+
let ageDisplay = "N/A";
46+
if (age) {
47+
const ageNum = parseInt(age, 10) / 100;
48+
if (Number.isInteger(ageNum)) {
49+
ageDisplay = `${ageNum} years`;
50+
} else {
51+
ageDisplay = `${ageNum.toFixed(1)} years`;
52+
}
53+
}
54+
55+
return (
56+
<Card sx={{ mb: 3, position: "relative" }}>
57+
<CardContent>
58+
{/* Card Number in Top Right */}
59+
<Typography
60+
variant="subtitle2"
61+
sx={{
62+
position: "absolute",
63+
bottom: 8,
64+
right: 12,
65+
fontWeight: 600,
66+
color: Colors.darkPurple,
67+
}}
68+
>
69+
#{index + 1}
70+
</Typography>
71+
72+
<Typography
73+
variant="h6"
74+
sx={{
75+
fontWeight: 600,
76+
color: Colors.darkPurple,
77+
textDecoration: "none",
78+
":hover": { textDecoration: "underline" },
79+
}}
80+
component={Link}
81+
to={subjectLink}
82+
target="_blank"
83+
>
84+
Database: {dbname} &nbsp;&nbsp;|&nbsp;&nbsp; Dataset Number: {dsname}
85+
</Typography>
86+
87+
<Typography variant="body2" color="text.secondary" gutterBottom>
88+
Subject: {subj} &nbsp;&nbsp;|&nbsp;&nbsp; Age: {ageDisplay}
89+
&nbsp;&nbsp;|&nbsp;&nbsp; Gender: {genderDisplay}
90+
</Typography>
91+
92+
<Stack spacing={2} margin={1}>
93+
<Stack direction="row" spacing={1} flexWrap="wrap" gap={1}>
94+
<Typography variant="body2" mt={1}>
95+
<strong>Modalities:</strong>
96+
</Typography>
97+
{modalities?.map((mod, idx) => (
98+
<Chip
99+
key={idx}
100+
label={mod}
101+
variant="outlined"
102+
sx={{
103+
color: Colors.darkPurple,
104+
border: `1px solid ${Colors.darkPurple}`,
105+
fontWeight: "bold",
106+
}}
107+
/>
108+
))}
109+
</Stack>
110+
111+
<Stack direction="row" spacing={1} flexWrap="wrap" gap={1}>
112+
<Typography variant="body2" mt={1}>
113+
<strong>Tasks:</strong>
114+
</Typography>
115+
{tasks?.map((task, idx) => (
116+
<Chip
117+
key={`task-${idx}`}
118+
label={task}
119+
variant="outlined"
120+
sx={{
121+
color: Colors.darkPurple,
122+
border: `1px solid ${Colors.darkPurple}`,
123+
fontWeight: "bold",
124+
}}
125+
/>
126+
))}
127+
</Stack>
128+
<Stack direction="row" spacing={1} flexWrap="wrap" gap={1}>
129+
<Typography variant="body2" mt={1}>
130+
<strong>Sessions:</strong> {sessions?.length}
131+
</Typography>
132+
</Stack>
133+
<Stack direction="row" spacing={1} flexWrap="wrap" gap={1}>
134+
{types?.length && (
135+
<Typography variant="body2" mt={1}>
136+
<strong>Types:</strong> {types.join(", ")}
137+
</Typography>
138+
)}
139+
</Stack>
140+
</Stack>
141+
</CardContent>
142+
</Card>
143+
);
144+
};
145+
146+
export default SubjectCard;

0 commit comments

Comments
 (0)