Skip to content

Commit d2d3d07

Browse files
committed
feat: add metadata search form and result rendering to SearchPage; resolve #40
1 parent 1fec410 commit d2d3d07

File tree

8 files changed

+531
-7
lines changed

8 files changed

+531
-7
lines changed

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
"@mui/icons-material": "^5.14.3",
1212
"@mui/material": "^5.14.4",
1313
"@reduxjs/toolkit": "^1.9.5",
14+
"@rjsf/core": "^5.24.8",
15+
"@rjsf/mui": "^5.24.8",
16+
"@rjsf/utils": "^5.24.8",
17+
"@rjsf/validator-ajv8": "^5.24.8",
1418
"@testing-library/jest-dom": "^5.14.1",
1519
"@testing-library/react": "^13.0.0",
1620
"@testing-library/user-event": "^13.2.1",

public/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
/> -->
5151

5252
<title>NeuroJSON.io - Free Data Worth Sharing</title>
53+
<script src="https://cdn.jsdelivr.net/npm/@json-editor/json-editor@latest/dist/jsoneditor.min.js"></script>
5354
</head>
5455
<body>
5556
<noscript>You need to enable JavaScript to run this app.</noscript>

src/components/Routes.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ 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 SearchForm from "pages/SearchForm";
67
import SearchPage from "pages/SearchPage";
78
import React from "react";
89
import { Navigate, Route, Routes as RouterRoutes } from "react-router-dom";
@@ -31,6 +32,7 @@ const Routes = () => (
3132

3233
{/* Search Page */}
3334
<Route path={RoutesEnum.SEARCH} element={<SearchPage />} />
35+
{/* <Route path={RoutesEnum.SEARCH} element={<SearchForm />} /> */}
3436
</Route>
3537
</RouterRoutes>
3638
);

src/pages/SearchForm.tsx

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import React, { useEffect, useRef, useState } from "react";
2+
3+
// Define the form data type (simplified for the demo)
4+
interface FormData {
5+
keyword: string;
6+
gender: string;
7+
}
8+
9+
// Simplified schema with just two fields
10+
const schema = {
11+
title: "Metadata Search",
12+
type: "object",
13+
properties: {
14+
keyword: { title: "Search keyword", type: "string" },
15+
gender: {
16+
title: "Subject gender",
17+
type: "string",
18+
enum: ["male", "female", "unknown", "any"],
19+
default: "any",
20+
},
21+
},
22+
};
23+
24+
const SearchForm: React.FC = () => {
25+
const editorRef = useRef<HTMLDivElement>(null);
26+
const jsonEditorRef = useRef<JSONEditorInstance | null>(null);
27+
const [counter, setCounter] = useState(0);
28+
const [showEditor, setShowEditor] = useState(true);
29+
const [formData, setFormData] = useState<FormData>({
30+
keyword: "",
31+
gender: "any",
32+
});
33+
34+
useEffect(() => {
35+
console.log("React formData state:", formData);
36+
}, [formData]);
37+
38+
// Initialize jsoneditor using the global JSONEditor
39+
useEffect(() => {
40+
if (editorRef.current && typeof JSONEditor !== "undefined") {
41+
jsonEditorRef.current = new JSONEditor(editorRef.current, {
42+
schema,
43+
theme: "spectre",
44+
startval: { keyword: "visual AND memory", gender: "any" },
45+
});
46+
47+
return () => {
48+
jsonEditorRef.current?.destroy();
49+
jsonEditorRef.current = null;
50+
};
51+
}
52+
}, [showEditor]);
53+
54+
// Function to trigger a re-render
55+
const handleIncrement = () => {
56+
setCounter((prev) => prev + 1);
57+
};
58+
59+
const toggleEditor = () => {
60+
setShowEditor((prev) => !prev);
61+
};
62+
63+
return (
64+
<div className="builder-wrapper">
65+
<h3 style={{ color: "white" }}>Demo: jsoneditor DOM Conflict in React</h3>
66+
67+
{/* <button
68+
onClick={() => {
69+
const value = jsonEditorRef.current?.getValue();
70+
if (value) {
71+
console.log("User submitted:", value);
72+
setFormData(value); // Update React state
73+
}
74+
}}
75+
className="submit-btn"
76+
style={{ marginLeft: "10px" }}
77+
>
78+
Send / Submit
79+
</button> */}
80+
<button onClick={toggleEditor} className="submit-btn">
81+
{showEditor ? "Hide Editor" : "Show Editor"}
82+
</button>
83+
{/* JSONEditor container (conditionally rendered) */}
84+
{showEditor && (
85+
<>
86+
<div style={{ marginTop: "20px" }}>
87+
<h4 style={{ color: "white" }}>Simplified Search Form</h4>
88+
<div ref={editorRef} />
89+
</div>
90+
{/* Buttons */}
91+
<div style={{ marginBottom: "20px" }}>
92+
<button
93+
onClick={handleIncrement}
94+
className="submit-btn"
95+
style={{ marginRight: "10px" }}
96+
>
97+
Increment Counter (Trigger Re-render)
98+
</button>
99+
</div>
100+
101+
<p style={{ color: "white" }}>Counter: {counter}</p>
102+
</>
103+
)}
104+
</div>
105+
);
106+
};
107+
108+
export default SearchForm;

src/pages/SearchPage.tsx

Lines changed: 147 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,153 @@
1-
import { Typography } from "@mui/material";
1+
import { schema } from "./searchformSchema";
2+
import { Typography, Container, Box } from "@mui/material";
3+
import Form from "@rjsf/mui";
4+
import validator from "@rjsf/validator-ajv8";
25
import React from "react";
6+
import { useState } from "react";
7+
import { Link } from "react-router-dom";
8+
import RoutesEnum from "types/routes.enum";
9+
10+
// helper function to build query string
11+
const buildQueryString = (formData: any): string => {
12+
const map: Record<string, string> = {
13+
keyword: "keyword",
14+
age_min: "agemin",
15+
age_max: "agemax",
16+
task_min: "taskmin",
17+
task_max: "taskmax",
18+
run_min: "runmin",
19+
run_max: "runmax",
20+
sess_min: "sessmin",
21+
sess_max: "sessmax",
22+
modality: "modality",
23+
run_name: "run",
24+
type_name: "type",
25+
session_name: "session",
26+
task_name: "task",
27+
limit: "limit",
28+
skip: "skip",
29+
count: "count",
30+
unique: "unique",
31+
gender: "gender",
32+
database: "dbname",
33+
dataset: "dsname",
34+
subject: "subname",
35+
};
36+
37+
const params = new URLSearchParams();
38+
Object.keys(formData).forEach((key) => {
39+
let val = formData[key];
40+
if (val === "" || val === "any" || val === undefined || val === null)
41+
return;
42+
43+
const queryKey = map[key];
44+
if (!queryKey) return;
45+
46+
if (key.startsWith("age")) {
47+
params.append(queryKey, String(Math.floor(val * 100)).padStart(5, "0"));
48+
} else if (key === "gender") {
49+
params.append(queryKey, val[0]);
50+
} else if (key === "modality") {
51+
params.append(queryKey, val.replace(/.*\(/, "").replace(/\).*/, ""));
52+
} else {
53+
params.append(queryKey, val.toString());
54+
}
55+
});
56+
57+
return `?${params.toString()}`;
58+
};
359

460
const SearchPage: React.FC = () => {
5-
return <Typography variant="h1">Search Page</Typography>;
61+
const [result, setResult] = useState<any>(null);
62+
const [hasSearched, setHasSearched] = useState(false);
63+
64+
// const handleSubmit = ({ formData }: any) => {
65+
// console.log("submitted search query:", formData);
66+
// };
67+
const handleSubmit = async ({ formData }: any) => {
68+
console.log("submitted search query:", formData);
69+
const query = buildQueryString(formData);
70+
const url = `https://cors.redoc.ly/https://neurojson.org/io/search.cgi${query}`;
71+
// console.log("url", url);
72+
try {
73+
const res = await fetch(url);
74+
const data = await res.json();
75+
setResult(data);
76+
console.log(data);
77+
} catch (err) {
78+
console.error("Failed to fetch data:", err);
79+
}
80+
setHasSearched(true);
81+
};
82+
83+
return (
84+
<Container maxWidth="md" style={{ marginTop: "2rem" }}>
85+
<Box
86+
sx={{
87+
backgroundColor: "white",
88+
p: 3,
89+
borderRadius: 2,
90+
boxShadow: 1,
91+
}}
92+
>
93+
<Form
94+
schema={schema}
95+
onSubmit={handleSubmit}
96+
validator={validator}
97+
liveValidate
98+
/>
99+
100+
{/* {result && (
101+
<Box mt={4}>
102+
<Typography variant="h6">Datasets Found</Typography>
103+
<pre style={{ background: "#f5f5f5", padding: "1rem" }}>
104+
{JSON.stringify(result, null, 2)}
105+
</pre>
106+
</Box>
107+
)} */}
108+
{hasSearched && (
109+
<Box mt={4}>
110+
{Array.isArray(result) ? (
111+
result.length > 0 ? (
112+
<>
113+
<Typography variant="h6">
114+
{`Found ${result.length} Datasets`}
115+
</Typography>
116+
<ul>
117+
{result.map((item, idx) => {
118+
const label = `${item.dbname}/${item.dsname}`;
119+
const link = `${RoutesEnum.DATABASES}/${item.dbname}/${item.dsname}`;
120+
121+
return (
122+
<Box key={idx} mb={1}>
123+
<Link
124+
to={link}
125+
style={{ textDecoration: "none", color: "#1976d2" }}
126+
>
127+
{label}
128+
</Link>
129+
</Box>
130+
);
131+
})}
132+
</ul>
133+
</>
134+
) : (
135+
<Typography variant="h6">
136+
No matching dataset was found
137+
</Typography>
138+
)
139+
) : (
140+
<Typography color="error">
141+
{result?.msg === "empty output"
142+
? "No results found based on your criteria. Please adjust the filters and try again."
143+
: "Something went wrong. Please try again later."}
144+
</Typography>
145+
)}
146+
</Box>
147+
)}
148+
</Box>
149+
</Container>
150+
);
6151
};
7152

8153
export default SearchPage;

0 commit comments

Comments
 (0)